Markdown across every repo had accumulated a dozen ad-hoc names (refactor_claude_todo.md x23, DESIGN_AUDIT_TODO.md, NEXT_SESSION, HANDOFF, PLAN, SPEC) with nothing distinguishing a live task from a finished record. An audit found docs asserting work was pending that shipped months earlier, and one asserting completion while the work was open. Four namespaces now: README.md, CLAUDE*.md, DOCS*.md, TODO*.md. Task files carry the literal "REMOVE ME AFTER FINISH" and are deleted when the work lands. Enforced bidirectionally -- a TODO without the marker fails, AND any file carrying the marker that is not named TODO*.md fails. Without the reverse rule a task can be renamed into the permanent namespace and quietly live forever, which is the failure being fixed. md_naming/ mirrors the file_length topology and imports its tables (EXCLUDED_DIRS, THIRD_PARTY_REPOS, VENDORED_ANYWHERE, GENERATED_PATTERN) rather than restating them, so the two gates cannot disagree about the same file. 71 tests, 100% branch coverage. stale.py warns on TODOs past 90 days but never fails: no machine can prove a task is finished, so the honest mechanism is a nudge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
2.7 KiB
The scrollable lock viewport
Design rationale for gatelock/_scrollable.py and its collaborators
(_scrollfit.py, _scrollkeys.py, _userintent.py). Kept here rather
than in the module docstring so the module itself stays readable in one
piece; the behaviour it describes is load-bearing, not historical.
A keyboard-scrollable, focus-following viewport for lock-surface content.
Lock surfaces cannot solve "content taller than the screen" the obvious way.
A scrollable dialog would be a second Toplevel, and on an
override-redirect lock surface a second toplevel steals the Tk grab, which the
recovery tick then kills a second later -- the 2026-07-26 failure where a
frozen sport selector logged a walk as table tennis. So the viewport has to
live inside the surface's own widget tree, which is what this provides.
Three defects this exists to prevent, all of them Tk defaults rather than oversights in any one app:
- Centered content clips symmetrically. A
place-centered frame takes its requested size, so when it is too tall the parent shears equal amounts off the top and the bottom -- losing the header and the submit button together, with no scrollbar and no hint anything is missing. This top-anchors content inside a scroll region instead. - A Canvas viewport is not keyboard-scrollable.
tk.Canvashas no class-level key bindings at all, so::tk::FocusOKrejects it as a focus stop and the only way to move the view is dragging the scrollbar thumb with a pointer. This makes the canvas focusable and binds PageUp/PageDown, Home/End and arrows. - Focus walks into invisible widgets. Canvas clipping does not unmap a
child, so every scrolled-out-of-view field stays
winfo viewableand stays in the tab chain, while Tk never scrolls to follow focus. Tabbing therefore lands on fields the user cannot see and cannot bring into view. :meth:ScrollableSurface.scroll_into_viewfixes that, but only for focus the user moved.
Wheel scrolling is bound too, but as an addition -- never as the only path.
Scrolling is the fallback, not the design. A lock screen is supposed to
fit its screen; content that overflows is a layout defect, and :meth:finalize
logs it at warning so it gets fixed rather than lived with. While the
content fits -- the normal case -- the scroll region is pinned to the viewport
and the scrollbar is hidden, so the view cannot move at all. When it does
overflow, the view still only moves in response to a keypress, a click or the
wheel: an earlier version followed programmatic focus too, and since the apps
re-focus a widget on every repaint, the screen scrolled itself to mid-content
and back while the user was doing nothing (reported 2026-08-03, 1366x768).