Commit Graph

100 Commits

Author SHA1 Message Date
966ec60f17 CI: fail the build on any file over 250 lines
Some checks are pending
file length / file-length (push) Waiting to run
palette drift / palette (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
Requirement 4 of "make it permanent": the pre-commit hook only ever sees the
files in a commit, so it cannot catch a violation that arrives by a path the
hook never ran on — a merge, a push from a machine without hooks installed, a
rebase that resurrects a file. This runs `check_file_length.sh --all` over the
whole tree on every push and PR to main.

Deliberately no `paths:` filter, unlike the other ten workflows: the cap
applies to every file in the repo, so scoping this job to a subdirectory is
the one change that would reintroduce exactly what it exists to catch.

Added last, because until the final doc split it would have failed on push.
Verified against a clean checkout of HEAD in a throwaway worktree — the exact
command the job runs exits 0 — and the commit gate re-tested in both
directions after file_length's own internals were refactored: a staged
261-line file fails (exit 1, no commit object), a 200-line one commits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:43:22 +02:00
b9e2312779 unified-design-system: split the three docs — repo now fully under the cap
tokens.md        260 -> 219 + operability.md 69
  nielsen-audit.md 258 -> 154 + shipped-record.md 122
  README.md        257 -> 172 + operability-patterns.md 95

Worked the requested order rather than cutting straight to a split.

(a) Automate: the categorical ramp's four constraints were restated in prose
    with the measured margins hand-copied (ΔE 34.6/26.0/25.6/27.4, etc).
    ramp_check.py prints every one of those numbers and fails if any
    regresses, so the prose now points at the command instead of duplicating
    its output. The two consequences the numbers do not state — why the ramp
    is mid-toned, and why lightness falls monotonically — stay in prose.

(b) Dedup: measured, not assumed. Zero paragraphs are shared between the
    three files; README already delegates the rules to tokens.md and shows
    only per-stack code. Nothing to remove.

(c/d) Split on meaning: tokens.md is "frozen values", so the behavioural
    requirements that *use* those tokens moved to operability.md, with the
    per-stack code for them in operability-patterns.md. nielsen-audit.md is a
    live grid, so completed phases moved to shipped-record.md — following the
    existing phase3-record.md convention, not a new one.

The focus-ring token row had to stay in tokens.md: palette_check.py parses
that file, and moving the row made the check fail with "MISSING md:focus-ring"
— a token it cannot see is a token nothing compares across the four stacks.
The rationale lives in operability.md and the row links to it.

palette_check and ramp_check both at 0 failing checks; every relative link
between the docs resolves.

`bash scripts/check_file_length.sh --all` now exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:42:27 +02:00
06ca32ce6d sync_settings_ui: split sync_settings_screen.dart 382 -> 192
sync_settings_widget.dart   68  — the public StatefulWidget and its callbacks
  sync_settings_sections.dart 132 — the two sections the screen composes
  sync_settings_screen.dart   192 — the State: loading, connecting, build

Uses `part`/`part of`, not imports: _FirebaseSection and _BackupSection are
library-private, so a sibling file cannot import them at all — extracting them
as ordinary files would not compile.

Two shapes tried and rejected on the way: an `extension _BackupActions` on the
State (an extension cannot call the protected setState), and moving build()
out of the class (it is an @override and has to stay). Both reverted from HEAD
rather than patched around.

flutter analyze clean, 29 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:38:58 +02:00
ea0dc9348c crdt_sync_dart: split four library files — package now under the cap
firebase_auth_rest.dart 333 -> 238 + firebase_credentials 71
                                     + firebase_auth_error 31
  firebase_client.dart    283 -> 207 + firebase_keys 60 + firebase_errors 19
  sync.dart               259 -> 141 + sync_state 121

Mirrors the split just made on the Python side, module for module, so the two
implementations stay legible against each other: credentials with their store,
key escaping with its escape table, the error type with the response parser
that builds its message.

Every new file is added to lib/crdt_sync.dart. A src/ file that is not in the
barrel compiles and tests green while silently dropping the symbol from every
consumer's import, so the export list is part of the split, not an afterthought.

280 tests pass and `dart analyze` is clean — including the four consumers
(firebase_config, secure_credential_store) whose imports had to follow the
moved types.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:36:03 +02:00
3589d166cd crdt-sync: split five source files — package now fully under the cap
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
_sync.py         456 -> 245 + _syncstate 131 + _pull 106 + _revisions 34
  _firebase_auth   412 -> 242 + _credentials 146 + _autherrors 69
  _firebase.py     336 -> 250 + _fbkeys 100
  google_id_token  316 -> 232 + tool/_oauth_callback 105
  _github.py       255 -> 238 + _gherrors 30

revision_of became its own leaf module because both the push and pull halves
need it; without that the two would have imported each other. _fbkeys holds
the RTDB key escaping — the escape tables moved with the functions that read
them, since the pair has to round-trip exactly or a device silently stops
seeing its own writes.

crdt-sync/tool/ is outside the coverage source, so the split there has no test
net: verified by running it the documented way instead
(`python3 -m tool.google_id_token --help` prints its usage, and
`from tool.google_id_token import TokenError, fetch_id_token` — what
seed_session.py does — still resolves).

An AST pass over each pair caught what the suite would not have: _is_revoked
needs the error type, so it stayed with it rather than following the
credentials; _TERMINAL_AUTH_REASONS and _CONSENT_TIMEOUT_SECONDS had to be
carried to their new callers.

259 passed at 100% branch coverage. crdt-sync/tool/seed_session.py and
firebase-debug.log are pre-existing local changes and are deliberately not in
this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:30:52 +02:00
f5f9db21dc gatelock: split _arbiter.py and _escape.py — package now fully under the cap
_arbiter.py 397 -> 246 + _claims 241
  _escape.py  300 -> 230 + _escape_types 82

_claims holds the claim document and everything that decides whether one is
still owned: taking the advisory lock IS the liveness proof, so reading a
claim and reaping a dead one are the same operation and belong together.
_arbiter keeps the policy — who outranks whom, and whether this app may arm.

An AST pass over every module in the package (names loaded vs. names defined
or imported) caught two things a green suite would not have:
  - _try_lock and fcntl moved with the locking code, so the tests' patch
    targets had to move too;
  - OutputSource was left behind by the earlier _outputs split and only
    resolved because of `from __future__ import annotations` — a latent
    NameError the moment anything evaluated that annotation.

gatelock is now at 0 files over 250 lines: 389 passed, 100% branch coverage,
the same 58 public names, and palette_check still at 0 failing checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:21:46 +02:00
40f94833c1 gatelock: split _outputs.py 401 -> 137 across four modules
_output_types.py 62  — Output, OutputRect, OutputScan (data only)
  _randr.py        157 — the optional python-xlib backend and its Protocols
  _xrandr.py       112 — the `xrandr --query` subprocess fallback
  _outputs.py      137 — the enumerator that picks between them

The two backends both needed the value types, which would have made the
dependency circular. The first attempt papered over that with function-local
imports; extracting the types into a leaf module removes the cycle instead, so
every import is top-level again — and, importantly, the tests' existing
`patch("gatelock._outputs.scan_xrandr")` targets keep working, which the
lazy-import version had silently broken.

_RR_CONNECTED moved with the RandR code and _XRANDR_TIMEOUT_S/_OUTPUT_LINE/
_GEOMETRY with the xrandr code, each carrying its comment. An earlier pass
tried to relocate those constants with a regex, which ate part of an unrelated
function body; that attempt was reverted and redone with asserted line
boundaries plus an AST check for unresolved names in both halves.

389 passed at 100% branch coverage; gatelock.__all__ still the same 58 names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:15:55 +02:00
02659e0abf gatelock: split _recovery.py and _surfaces.py, and fix two fail-open guards
Some checks failed
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
github_device_auth tests / test (push) Has been cancelled
_recovery.py  321 -> 242 + _recovery_types 47 + _grabwatch 84
  _surfaces.py  380 -> 250 + _placement 136 + _surface_types 54 + _textmirror 70

Two guards had to be repaired first, because both would have gone quiet
rather than red:

1. palette-drift CI was ALREADY BROKEN by the earlier _window.py split.
   palette_check.py reads LockConfig's colour defaults from a hardcoded
   gatelock/_window.py, and LockConfig had moved to _config.py — so it parsed
   0 tk tokens and reported "10 failing check(s)" where HEAD reports 0. Fixed
   the path, and made parse_tk raise instead of returning {} when LockConfig
   is not found: the empty dict is what let every tk comparison pass
   vacuously while the drift guard was off for the whole stack.

2. test_recovery_invariants' symbols() read one hardcoded filename, so
   splitting _recovery.py would have narrowed the "no weakening call"
   invariant — the one that exists because of the 2026-07-25 unlock — to
   whichever half kept the name, with the suite still green. It now globs the
   whole _recovery*/_surfaces* family and fails if the glob matches nothing.
   Verified it discriminates: a throwaway _recovery_probe.py calling
   restore_vt_switching made the test fail, and removing it restored green.

GrabWatch is a small object rather than a free function because holds_grab
and its once-per-cause warning share state; _placement holds the geometry
questions and the re-assertion pass, which can only ever add visibility.
_surfaces keeps an explicit __all__: the autofixer pruned the mirror_text_widgets
re-export once and `import gatelock` broke, so the intent is now stated.

389 passed at 100% branch coverage; gatelock.__all__ is the same 58 names as
HEAD; palette_check back to 0 failing checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:08:50 +02:00
085d08b5c9 Split the two shell scripts and the gate's own config
aur-publish/publish.sh   439 -> 148 + lib/aur_registration 107
                                      + lib/aur_verify 122 + lib/aur_publish_one 107
  guard-lib/lib/file_guard.sh  350 -> 140 + file_guard_unlock 214
  file_length/config.py        252 -> 45 + _tables 202 + exemptions 77

The libs are sourced, never executed: they read the readonly config the entry
script declares, and a `readonly` re-run inside a twice-sourced lib aborts
under `set -e`. Verified by function parity rather than by eye — publish.sh
resolves the same 17 functions as before the split, guardctl the same 15 —
plus `shellcheck -x` clean on every file and a real `--help` / bad-option run.

file_length is the gate's own code, so a broken import there turns the gate
into a no-op. Split three ways to avoid an import cycle without a `# noqa`:
_tables holds the data, exemptions the rules that read it, config re-exports
both so `from file_length.config import is_generated` still resolves. The
first attempt was wrong — EXCLUDED_DIRS was left behind and every invocation
raised — caught because the gate was re-run in both modes rather than
assumed. Now verified: exit 0 on a clean file, exit 1 on an over-cap one,
--explain intact, and the same 61 exemption decisions as HEAD.

Also deletes baseline-2026-08-14.json and SURVEY-2026-08-14.txt: the task
forbids a baseline file, and nothing in the tree read either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:59:27 +02:00
200432a2b4 dart: split ten oversized test files by group
firebase_auth_rest_test.dart  525 -> 153 + _google 158 + _tokens 206 + _uid 141
  sync_revisions_test.dart      451 -> 223 + _peers 228 + _backends 158
                                       + _order 165 + _persistence 215
  sync_settings_screen_test     457 -> 207 + _backup 143 + _google 145
  mirror_store_test.dart        452 -> 209 + _reads 172 + _fallback 196 + _revisions 244
  firebase_client_test.dart     374 -> 198 + _writes 227
  firebase_config_test.dart     360 -> 138 + _client 148 + _client_google 164
  device_auth_test.dart         307 -> 128 + _poll 201
  github_client_test.dart       291 -> 143 + _writes 201
  sync_test.dart                269 -> 160 + _skips 175

Two traps this hit, both invisible to a line count:

  - _FakePersistence is declared *after* main(), so a naive cut at the last
    brace dropped it; sync_revisions_persistence_test.dart failed to load
    until it was carried across. Audited every other split file for trailing
    top-level declarations — this was the only one.
  - Copying the preamble into each sibling left helpers the sibling never
    uses. `dart analyze` flagged 12; all removed rather than left as dead
    code, and the analyzer is now clean.

crdt_sync_dart 280 passed, sync_settings_ui 29 passed, github_device_auth
analyze clean — all matching a clean HEAD worktree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:54:35 +02:00
9d897c13f4 crdt-sync: split seven oversized test files by test group
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
test_firebase_auth.py    439 -> 170 + _google 159 + _tokens 159 + _session 175
  test_sync_revisions.py   406 -> 163 + _peers 162 + _backends 216
  test_firebase.py         369 -> 212 + _writes 221
  test_mirror.py           364 -> 178 + _fallback 229
  test_config.py           355 -> 139 + _credentials 112 + _client 233
  test_github.py           301 -> 127 + _writes 219
  test_sync.py             271 -> 195 + _skips 130

Cut at concern boundaries, not line counts: test_sync_skips.py holds the five
cases where a peer's pushed file is missing, corrupt, wrong-shaped or our own
— one story about what sync_log declines to merge — rather than an arbitrary
half of the original class.

259 passed at 100% branch coverage, verified against a clean HEAD worktree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:47:58 +02:00
389832314a gatelock: split seven oversized test files by test group
Each file split at a class or concern boundary, never mechanically:

  test_arbiter.py       432 -> 222 + _lifecycle 183 + _release 201
  test_outputs.py       418 -> 187 + _randr 204 + _enumerator 126
  test_scrollable.py    374 -> 231 + _into_view 197
  test_escape.py        343 -> 175 + _budget 216
  test_window_surfaces  341 -> 110 + _lifecycle 104 + _preempt 182
  test_surfaces.py      282 -> 167 + _focus 154
  test_window.py        269 -> 143 + _grab 139

Test counts verified against a clean HEAD worktree, per file group rather
than in aggregate: arbiter 49/49, outputs+escape 84/84, window+surfaces
72/72, scrollable 18/18. The whole suite is 389 passed at 100% branch
coverage, identical to before.

The one non-mechanical part: _reply/_randr_display are module-level helpers
defined *between* classes, so they did not travel with the header and had to
be moved to the RandR file by hand — six tests failed until they did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:45:08 +02:00
408c7a2585 gatelock: split test_recovery.py 477 -> 208 lines by test layer
The file's own docstring described three layers; they are now three files:

  test_recovery_invariants.py  (52)  — the static AST check that _recovery.py
                                       contains no weakening call at all
  test_recovery_reassertion.py (218) — what a tick re-asserts: grab, focus, VT
  test_recovery.py             (208) — the 2026-07-25 monotonic-sequence walk,
                                       tick mechanics, and scheduling

Splitting the classes made pylint's R0801 fire: both halves built a
RecoveryLoop with the same RecoveryCollaborators block. Rather than suppress
it (banned) the shared construction became conftest.build_loop(root, config),
which the `loop` fixture now uses too — so the tests that need a non-default
config ask for one instead of restating the wiring. The output scans both
halves drive moved to conftest alongside it.

389 passed, 100% branch coverage — same tests, same count, three files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:37:47 +02:00
48f6c7420f Gate the 250-line cap on every commit, repo-wide
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
design_system tests / test (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
web_ui tests / test (push) Waiting to run
A refactor without a gate silently regrows, so this is the part that makes the
cap stick. Until now the repo had no root .pre-commit-config.yaml at all --
`git commit` ran zero hooks, and the five subproject configs only covered their
own directories.

Adds a root config wiring scripts/check_file_length.sh as a local hook, and
carries the same hook in each of the five subproject configs so committing
with one of those is gated too. Staged files only, so an unrelated commit
never breaks on a violation it did not introduce; exit 1, not a warning; no
baseline and no allowlist, since those are suppressions.

Verified in a throwaway worktree, both directions:
  - a staged 261-line file fails the commit (exit 1, HEAD unchanged, no
    commit object created)
  - a staged 200-line file commits normally (exit 0)

The 44 files still over the cap cannot block this: the hook only ever sees
what a commit touches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:31:35 +02:00
e651b1af17 gatelock: split _scrollable.py 495 -> 243 lines across four modules
Cut along concerns rather than line counts, so each file answers one question:

  _scrollfit.py   (180) — does the content fit, and what layout follows?
  _scrollkeys.py  (176) — key/wheel bindings and the six scroll commands
  _userintent.py  (106) — was this focus move the user's doing, or the app's?
  _scrollable.py  (243) — the widget wiring and the public surface

_scrollfit takes an explicit FitTarget bundle rather than the surface, matching
the ArmingCollaborators pattern; UserIntentTracker owns the two fields that
answer "did the user just act?", which is the guard that keeps the viewport
from scrolling itself under someone who touched nothing.

The 45-line module docstring — the three Tk defaults this viewport exists to
prevent, and the failure behind each — moved to docs/scrollable-viewport.md
rather than being trimmed. It is load-bearing rationale, so it is referenced,
not deleted.

389 passed, 100% branch coverage — unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:28:21 +02:00
940bf19d54 gatelock: split _window.py 571 -> 243 lines across four modules
_window.py was the repo's largest file and could not be read in one piece.
Split along the seams the file already marked out, keeping the public API
and every existing import path intact:

  _config.py  (215) — LockConfig, the design tokens and preset resolution
  _arming.py  (178) — the way up: VT-disable, backdrop, surfaces, input grab
  _preempt.py (118) — naming a blocked grab's holder, standing a weaker one down
  _hooks.py   (51)  — the LockWindowHooks Protocol, pure interface
  _window.py  (243) — construction, focus, and teardown

_arming takes an explicit ArmingCollaborators bundle rather than the window
itself, mirroring the existing RecoveryCollaborators; setup() returns whether
VT was disabled instead of mutating the window. That leaves no reach-back into
private members (ruff SLF001 clean) and makes the arming sequence testable
without a window.

LockConfig is re-exported from _window, so `from gatelock._window import
LockConfig` — which five sibling modules and the tests already do — still
resolves. Tests that patched moved symbols were retargeted to their new home;
the dead _maybe_preempt delegate, whose only callers were tests, is gone.

389 passed, 100% branch coverage — identical to before the split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:20:11 +02:00
376e5d4634 file_length: exclude Mapperatorinator from the 250-line cap
Some checks are pending
palette drift / palette (push) Waiting to run
Upstream clone at ~/Mapperatorinator is a third-party sibling (MIT,
github.com/OliBomby/Mapperatorinator), driven by subprocess from
~/osu-automapper and never vendored or modified.

Verified both directions: is_vendored() returns True for a 2538-line upstream
file (which would otherwise fail the cap) and False for ~/osu-automapper's own
sources, which stay gated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iJHHN18dud8dXpjd7ff3R
2026-08-22 13:18:10 +02:00
c3260c26f5 Add a session prompt for taking gatelock's pylint to 10.00
Some checks are pending
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
gatelock is not the docstring problem its siblings are -- 312 of its 394
findings are pytest fixture idiom (redefined-outer-name, protected-access),
so the prompt makes that the explicit fork to settle with the user first,
citing screen-locker's tests hook as the existing precedent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
2026-08-21 14:40:28 +02:00
2005c40baa Group sync_log's 11 arguments into three bundles
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
BREAKING: sync_log's flat keyword arguments are replaced by SyncTarget,
LogCodec and RevisionTracking. Callers change from eleven loose keywords to
four positional arguments; every consumer repo is updated in the same pass.

Eleven parameters on one function was the last real pylint finding in the
package, and the grouping is the fix rather than a workaround: the three
bundles are the three things a tick actually needs -- where to sync, how to
serialize, and whether to track revisions -- and each documents its own
fields. SyncTarget.own_ids also moves the "this device plus its pre-migration
id" rule onto the type that owns it, instead of recomputing it inline.

Internals follow: _pull_remote_logs takes a _PullContext instead of eight
positionals, and the push half splits into _merge_peers/_push, which is what
clears the 21-locals finding.

The two duplicated in-memory fakes merge into one FakeStore in conftest, as
does the no-bulk-read wrapper around it -- the shared half was the fiddly
prefix slicing, which is exactly where two copies drift.

crdt_sync now scores a clean 10.00/10 on pylint. Two suppressions remain,
both scoped and explained at the line: asserting on a private attribute IS
the timeout test, and reversing merge_record's operands IS the commutativity
test.

259 tests, still 100% branch coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
2026-08-21 14:22:25 +02:00
c96d179fb7 Prefer implicit booleaness in test assertions
pylint's C1803/C1804: `assert not merged` over `assert merged == {}`, in the
eight places where the comparison was to an empty literal. Takes the package
from 9.90 to 9.93.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
2026-08-21 14:11:50 +02:00
1b8c99d2e1 Document every test, taking pylint from 8.58 to 9.90
pylint scored the suite at 8.58/10, and 277 of the ~300 findings were missing
docstrings -- every one of them in crdt_sync/tests/, none in library code.
ruff exempts tests from the docstring rules, so the two linters disagreed and
the score sat well below the bar.

Rather than silence pylint to match ruff, every test class, test function and
stub method now says what it pins. Stub arguments that exist only to match a
real signature are underscore-prefixed, which is the documented way to say
"deliberately unused" and clears the unused-argument findings too.

No behaviour change: 259 tests, still 100% branch coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
2026-08-21 14:10:23 +02:00
62da2102d6 Bump to 0.8.1 so the tag covers the timeout tests
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
crdt-sync-v0.8.0 was already pushed at the previous commit; moving a
published tag is worse than spending a patch number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
2026-08-21 14:04:16 +02:00
60b7f7c02f Assert firebase_client_for actually forwards its timeout
The keyword has a default, so it adds no branch and 100% branch coverage
stays green whether or not it is threaded through to the client at all --
the gate is blind to exactly the thing the change was for. Two tests pin it:
an explicit value reaches the client, and omitting it keeps the shared
default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
2026-08-21 14:03:26 +02:00
4468df0e13 Pool HTTP connections, and let callers set the Firebase timeout
Every request went through the module-level requests.get/put/post/delete
helpers, so each one paid a fresh DNS+TCP+TLS handshake. Measured today
against the real backends that is 95ms/req to api.github.com and 52ms to
Realtime Database -- on a 27-request diet-guard tick, most of the wall clock.

Each transport module now binds a pooling requests.Session to the name
`requests`. A Session is a drop-in for those helpers, so all 9 call sites and
all 16 tests that patch `<module>.requests.<verb>` are untouched; the real
module stays available as `_requests` for the RequestException/Response
references. Sessions are per-module rather than shared because the tests use
sequence-consuming side_effect lists, where one module could otherwise eat
another's queued response.

Transient failures now retry, restricted to GET: a PUT here carries a
Contents-API sha, and replaying one whose response was merely lost would
write against a stale precondition on a remote the phone also reads.

Also thread timeout_seconds through firebase_client_for, which accepted none,
so a caller's tighter budget silently reached the GitHub backend only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
2026-08-21 13:55:01 +02:00
a4b72a21c8 Make lock preemption opt-in: it could evict the armed workout lock
Some checks are pending
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
preempt_weaker_holder defaulted to True, so *any* app building a plain
hard LockConfig would SIGTERM a lower-ranked incumbent. wake_alarm
(RANK_WAKE_ALARM=300) builds exactly that and outranks screen_locker
(200), so on any morning wake-alarm-trigger.timer fired while the workout
locker was armed, it stood the enforcement lock down: machine unlocked,
no workout logged, next re-arm up to 30 minutes away.

Default to False and let screen_locker opt in explicitly, which is the
direction that actually motivated the feature (diet_guard held the grab
for hours on 2026-08-21). Nothing can now evict an enforcement lock
without asking for that power by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:31:23 +02:00
7e7c34a073 gatelock: make fit/scroll geometry tests immune to a real WM
Some checks are pending
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
Under i3 (not Xvfb), a managed tk.Tk() toplevel's geometry() request
was silently rewritten to the tiled pane size instead of the
requested WxH, so the content never actually overflowed and every
assertion built on "does this overflow" failed -- 11 tests across
test_fitcheck.py/test_scrollable.py. Fix: overrideredirect the
throwaway root, matching how every production lock window is already
built and for the identical reason (a WM must not be able to rewrite
geometry a fit/scroll check depends on).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 13:00:20 +02:00
24abb22de0 gatelock: preempt a weaker holder blocking a stronger claim's grab
wait_for_turn only helps a weaker locker arriving after a stronger
one; it does nothing when the weaker one is already the incumbent.
On 2026-08-21 diet_guard (rank 100) held the grab for ~3 minutes
against a newly-arriving screen_locker (rank 200), which just
retried and logged instead of ever making progress.

Fix: when a grab is blocked by a holder that outranks-us-not, SIGTERM
it once. The holder's own signal handler already converts SIGTERM
into its normal close() teardown (VT restore, arbiter release,
app-specific on_close) -- this reuses that path rather than adding a
new one, and never signals upward (never disturbs an incumbent that
outranks us), so it cannot become a way to bypass a stronger lock.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 12:57:41 +02:00
06aae48e24 gatelock: promote the priority queue from leetcode_guard
Some checks are pending
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
wait_for_turn/stronger_claims already fixed leetcode_guard's grab-race
with screen_locker; screen_locker and diet_guard never got the same
fix, so a stray re-arm race between them was still live (confirmed
2026-08-21: diet_guard held the global grab 07:04-09:07 while
screen_locker built a competing window and retried its own grab
forever instead of queueing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M5gKyYzxWG6QkCaj1wWfk
2026-08-21 12:33:43 +02:00
1533eec081 Distribute dopamine-ux prompts into their target repos
Some checks failed
palette drift / palette (push) Has been cancelled
Each prompt is now also copied to <repo>/prompts/dopamine-ux-*.md so a repo can
be opened and the prompt run directly. ~/utils/dopamine-ux/ remains the source
of truth; corrections go here and are re-distributed.

Adds the completion protocol every copy now carries: on finishing, the session
deletes its repo copy in the implementation commit and appends a DONE row to the
completion log in 00-INDEX.md. Partial completion is never recorded as done --
the file is edited to state what remains instead, since a stale prompt is
indistinguishable from a pending one.

Also fixes a codespell violation (re-declared -> redeclared) caught by
diet-guard's pre-commit hooks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FdApz7EZ1zDfvtvvi1idDv
2026-08-16 21:13:19 +02:00
35415de225 Add dopamine-ux session prompts for the UX feedback work
Nine copy-pasteable per-repo prompts derived from "The Dopamine Architecture"
(london-post.co.uk), plus an index and a verification gate.

The four mechanics (variable rewards, micro-animations, sound, near-miss) are
aimed at kuhy's own stated goals and gated by two tests: is the displayed state
true, and is the variance real. Corollary that shapes every prompt: reward the
recording, never the number recorded -- rewarding a calorie target teaches
under-eating rather than honest logging.

Surveys found the fleet has exactly one animation across four Flutter apps, no
motion tokens in any stack, and a populated screen-locker streak that no
renderer reads. Two constraints are recorded as must-not lines because a naive
implementation would violate them: screen-locker rewards must never reduce
enforcement (a banked skip credit was deliberately removed), and wake-alarm's
UI sound layer must never touch the desktop alarm daemon.

verify_prompts.py checks all 146 cited paths and 30 cited symbols, so a prompt
pointing at a renamed function fails loudly instead of wasting a session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FdApz7EZ1zDfvtvvi1idDv
2026-08-16 21:09:33 +02:00
f8dded33c7 Exempt frozen session artifacts from the 250-line cap
An implementation plan under docs/superpowers/plans/ and an append-only log
under docs/superpowers/sessions/ are frozen records of one past session. They
are read whole or not at all, so splitting one rewrites a historical record
for no reading benefit -- the same reason generated files are exempt.

Scoped deliberately: specs/ and contracts/ stay capped, because those are
living documents that get re-read and edited.

is_session_artifact() is kept separate from is_vendored() so the gate reports
an accurate reason -- these are kuhy's own files, not third-party code.

Relaxes the gate only, so no already-wired repo can newly fail. Verified:
~/todo still exits 0 on --all; testsAndMisc drops 198 -> 197 violations
(plans/ exempt, specs/ still caught) in both absolute and relative modes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-16 15:06:42 +02:00
682eaf6f8c unified-design-system: record the two extractions this pass shipped
Some checks failed
github_device_auth tests / test (push) Has been cancelled
palette drift / palette (push) Has been cancelled
`ts_core` (@kuhyx/ts-core) and `github_device_auth` close the two clusters
the audit flagged as genuinely missed. Six shared packages now, not four.

Both extractions turned up the same lesson, worth writing down: the copy
that looks canonical is not always the one to take. Each cluster had a
behaviour living in exactly one repo that a naive "extract the tidiest
version" would have silently deleted --

  * diet-guard's configurable device-flow endpoints, without which its
    desktop web build cannot authenticate at all (no CORS headers on
    GitHub's endpoints)
  * wake-alarm's transient-socket retry, without which an approved grant
    is lost if GitHub closes the connection at the wrong moment

Both are now in the shared package, so all four apps gained the fix the
other three never had. Same shape in ts_core: the RNG ships two interfaces
rather than one reconciled shape, because the generated sequence is the
contract and re-rolling every saved world would be a worse outcome than
the duplication.

sims3-clone is recorded as the one consumer still pending -- another
session had uncommitted work there, so migrating it would have entangled
two sets of changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 16:17:34 +02:00
254e3b00c7 github_device_auth: retry transient network failures (v0.3.0)
wake-alarm's copy carried a fix the other three lacked: `pollForToken`
swallows a transient socket failure and keeps polling, because GitHub can
close the connection at the exact moment the user authorizes and a phone
can drop its network mid-poll. Without it, an approved grant is lost to a
blip and the user has to start the whole flow again.

Extracting only todo's version would have silently deleted that fix from
wake-alarm. Taking it into the package instead gives it to all four apps.

Catching `http.ClientException` is sufficient, and avoiding `dart:io` here
is deliberate: diet-guard's desktop **web** build imports this package, and
naming `SocketException` would break that compile. Checked rather than
assumed -- a probe against a dead port shows package:http raises
`_ClientSocketException`, which implements *both* `ClientException` and
`SocketException`, so the single catch covers IO and web alike.

24 tests, 100% line coverage, clean analyze.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 16:05:12 +02:00
14a50b44e2 github_device_auth: configurable endpoints for web builds (v0.2.0)
Some checks are pending
github_device_auth tests / test (push) Waiting to run
palette drift / palette (push) Waiting to run
diet-guard's copy was not cosmetic drift from todo's: it takes the
device-code and token URLs as parameters, because GitHub's device-flow
endpoints send no CORS headers and its desktop **web** build has to route
both through a local proxy (`github_client_factory_web.dart` passes them).
Shipping only todo's hardcoded version would have broken that build the
moment diet-guard adopted the package.

So the package takes the more capable variant: `deviceCodeUrl`/`tokenUrl`
default to GitHub's own URLs, leaving every other consumer unaffected, and
`githubDeviceCodeUrl`/`githubTokenUrl` are exported so callers can
reference the defaults rather than retyping the literals.

23 tests (two new ones pin both the defaults and an override actually
being used for the request), 100% line coverage, clean analyze.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 16:01:21 +02:00
795a8eae62 github_device_auth: extract the GitHub device flow from four apps
todo, home_inventory, diet-guard and wake-alarm each carried the OAuth
device-flow client and the modal that drives it -- ~650 lines, with two
pairs byte-for-byte identical (todo/home_inventory's clients, and
diet-guard/wake-alarm's dialogs).

Deliberately its own package rather than part of `sync_settings_ui`. That
package is Firebase-only by an explicit 2026-08-12 decision recorded as
"do not re-add a GitHub section without kuhy asking again", so folding a
GitHub flow into it would reverse a standing decision. The GitHub mirror
in these apps is legacy-but-load-bearing code that was explicitly not
authorised for deletion, so it gets its own home instead of being merged
into the Firebase path or dropped.

One behaviour change, made once here instead of four times: the dialog
catches `on Object`, not `on Exception`. An `Error` is not an `Exception`
in Dart, so `ArgumentError` -- notably from URI parsing -- escaped
`_poll()` and left the dialog spinning on "Waiting for authorization…"
forever with nothing shown. That is the same trap that silently killed the
Firebase sync tick across these apps in 2026-08. A test pins it with a
mock that throws an Error rather than an Exception, so it cannot regress
back to `on Exception` unnoticed.

20 tests, 100% line coverage, clean under very_good_analysis, with the
same CI gate the other Dart package holds. Consumers are migrated
separately so each app's own suite gates its own swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:57:19 +02:00
41f014e7a7 ts_core: a framework-free TypeScript utility package
Some checks failed
palette drift / palette (push) Waiting to run
ts_core tests / test (push) Has been cancelled
`Clock` and a seeded mulberry32 RNG had each been reimplemented across the
game and web repos -- Clock twice (konbini-67, iron-and-anvil, doc comments
near-verbatim), the RNG four times. None of it is UI, so no existing shared
package owned it: `web_ui` ships `tokens.css` and React components, and a
game importing a UI package to get a seeded RNG is exactly the coupling
that makes people vendor their own copy instead.

The RNG deliberately exports **two** interfaces rather than reconciling
them into one. konbini-67 threaded mutable state through free functions;
sims3-clone and europe-county-map returned an object of closures. The
sequence is the contract here -- these repos have seeded tests and "same
seed -> identical world" guarantees, so changing how many times a helper
advances the generator silently re-rolls every saved world. A shared
package that did that would be worse than the duplication it replaces. The
core is shared, so both interfaces yield the same float sequence; that is
asserted, not assumed.

`rng.test.ts` pins the first five floats for seed 12345, captured from the
pre-extraction konbini-67 implementation rather than from this one -- a
golden test generated from the code under test only asserts that the code
matches itself. It earned its place immediately: the first constants I
wrote came from a truncated `toFixed(9)` probe and the test failed, which
is precisely the class of silent precision drift it exists to catch.

`hotline3d` is not a consumer and should not become one. Its generator uses
`%` and `Math.trunc` where mulberry32 uses `>>>`/`>>`; measured, it emits a
different sequence, so adopting this package would change its output.

29 tests, 100% branch/function/line/statement coverage, type-aware eslint,
and a committed dist/ with a CI freshness check -- the same bar web_ui
holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:42:41 +02:00
0dd4229432 unified-design-system: record the extraction audit, unstale the phase headings
Answers "did we extract every component we actually use?" now that Phases
1-3 have shipped.

The headline: the extractions hold up. No shipped component was a false
positive, and the reverse check -- were the donor copies actually deleted?
-- is clean except for billsplit's `_SectionHeader`, fixed separately in
testsAndMisc@54054e7. The one clear miss is `DeviceCodeDialog` x3, two
copies of which are byte-for-byte identical; it belongs in
`sync_settings_ui` rather than `design_system` because it depends on
GitHub auth types.

Also corrects two things the audit surfaced:

`nielsen-audit.md` described Phase 2 *and* Phase 3 as "not started". Both
shipped -- gatelock 0.5.0 and web_ui-v0.3.1. A planning doc that says "not
started" about finished work is worse than no doc, because the next
session reads it as a backlog and rebuilds what already exists. Both
headings now state what shipped, and the plan text is kept below as the
record of why. The Phase 2 note also spells out that screen-locker and
wake-alarm keeping local fan-out copies is deliberate (only two consumers
were bumped, to avoid restarting live systemd services) so it does not
read as a regression to be "fixed".

The audit also found a fourth shared package the phase briefs omit:
`sync_settings_ui`, consumed by four apps. It resolves most of the
`SettingsScreen` cluster, which the grid still listed as outstanding.

The remaining true duplicates (`_PrefsPersistence`, `Clock`, mulberry32
RNG) are non-UI utilities outside every package's remit, and are recorded
as such rather than as a reason to widen web_ui into a general utility
package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:29:53 +02:00
ccef40e817 unified-design-system: check one palette across all four token sources
`tokens.md` freezes the palette as prose and three packages re-express it
as code -- `web_ui/src/tokens.css`, `design_system`'s `tokens.dart`, and
gatelock's `LockConfig`. Nothing compared them, so "one palette
everywhere" held only for as long as four hand-maintained copies happened
to agree. A hex edited in one stack drifted silently in the other three.

`palette_check.py` parses all four and runs two halves:

  * agreement -- every mapped token carries the same hex in every
    non-exempt stack
  * completeness -- every colour parsed from any source must be mapped or
    exempted with a stated reason

The second half is the load-bearing one. A pure agreement check passes
happily when someone adds a token to one stack and never touches the map,
which is the drift most likely to actually happen; an unmapped token is
therefore a failure, not a skip. Absences are declared per token per stack
with a reason (Tk has no light theme and no borders; the categorical ramp
is web-only) so "this stack doesn't have it" is on the record rather than
a silent pass.

It found one real gap on its first run: `tokens.css` sets `--surface-2` to
`paper-raised` in the light block -- the light theme collapses both
elevation steps onto one surface -- which `tokens.md` documented only in
prose and no map covered. Now an explicitly compared row, so if either
half of that pair moves, it fails.

Proven to fail, not just to pass: drift injected into each of the four
sources in turn, plus an unmapped token, was caught every time with the
offending stack named; re-proven after the 250-line-cap split into
`palette_map.py`. Runs from its own workflow with deliberately no `paths:`
filter -- drift is a disagreement *between* packages, so a path-scoped
trigger would skip exactly the edit that causes it. That was not
hypothetical: `ramp_check.py` was reachable only from the
`web_ui/**`-filtered workflow, so a ramp edit in `tokens.md` or
`design_system` never ran it. It moves here too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:27:17 +02:00
5045f20c40 unified-design-system: record the Phase 3 gate
Some checks failed
web_ui tests / test (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:11:03 +02:00
6714d13198 web_ui: v0.3.1 — scope check-json past JSONC tsconfigs
The tsconfigs carry comments (TypeScript allows them); check-json does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:08:48 +02:00
eaa5683ff1 web_ui: add the CI + pre-commit gate, and make pnpm lint actually run
Matches the Phase 1 design_system pattern the brief points at: own lint config,
own tests, own coverage gate enforced in CI.

The lint script was declared but unrunnable -- there was no eslint flat config,
and the build tsconfig excludes tests (correctly; they must not ship in dist/),
so type-aware linting had no project for them. Adds eslint.config.js and a
lint-only tsconfig that widens the include without emitting. Running it surfaced
one real defect: nth() interpolated a number into a template literal without
String(), which the consumers' configs would have flagged.

The gate also checks that the committed dist/ matches a fresh build. This is not
theoretical -- the check caught the stale dist/ produced by that very String()
fix on its first run. A stale dist/ in a tag ships old code to every consumer
with no error anywhere, which is the one failure mode this packaging choice
introduces. dist/ is excluded from the whitespace hooks: they would "fix"
generated output that the next build immediately reverts.

CI additionally re-runs the categorical ramp's constraint proof, so the palette
cannot regress silently either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:08:18 +02:00
56d0c3d420 unified-design-system: record Phase 3
Documents why FilterBar was not extracted (a name collision over disjoint
domains, not a duplicate), the categorical ramp decision, the npm-cannot-consume
finding, and the index-vs-distinct-value bug that unit tests could not see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 15:04:01 +02:00
086791f4f9 web_ui: document distinct-value stepping
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 14:59:41 +02:00
133c210b2e web_ui: step the slider to the next DISTINCT value (v0.3.0)
Caught by driving the real browser, not jsdom: on awesome-mcp-explorer's star
distribution the arrow keys did nothing at all. ~1500 of the ~2981 values are
0, so an index step landed inside the run of duplicates -- the index moved, the
value did not, and the thumb, the readout and the filter all stayed put.

steppedValue now walks to the next distinct value, so one arrow press is one
visible change on any distribution, however lumpy. The regression test uses the
shape that actually broke rather than the evenly-spaced fixture that hid it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 14:59:27 +02:00
2574b21acc design-system: add a categorical ramp; web_ui v0.2.0
awesome-mcp-explorer needs six mutually distinguishable colours for its A-F
grades, and the design system had no answer -- one accent, and semantic roles
that deliberately cluster in one hue wedge. Raised as its own decision (per
PHASE3-PROMPT) and resolved by designing the ramp rather than mapping grades
onto success/warning/danger, which would have collapsed six steps into three.

Chosen by search, not by eye, under four hard constraints:
  - pairwise CIE dE >= 20 under normal vision AND deuteranopia, protanopia and
    tritanopia (worst observed 26.0)
  - >= 3:1 against BOTH backgrounds, since the ramp does not re-theme. This is
    the binding constraint: such colours only exist in L* 44-59.
  - monotonic lightness 58 -> 44, so hue and lightness encode redundantly and
    the ramp survives greyscale as an ordinal scale
  - muted saturation, to sit with the rest of the palette

scripts/ramp_check.py re-runs the whole proof and exits non-zero on a
regression, so this is a gate rather than a paragraph. A seventh hue is not
available (best candidate reaches dE 17.9); group the tail into "other".

This ramp is the only token group that leaves the warm wedge, deliberately:
hue separation is the entire point of a categorical scale.

Also: tokens.test.ts guards every frozen value against drift, and
tokens.md was split (screen-size.md) to hold the 250-line cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 14:50:43 +02:00
03bf1ed08b web_ui: shared web token + component layer (Phase 3)
The web counterpart to design_system (Dart): one :root block of frozen tokens
plus the components that were duplicated across repos. Extracted from
dufs-cloud and awesome-mcp-explorer, which both now consume it.

- tokens.css: the frozen palette, spacing/radius/type scales, shadow policy and
  focus ring, under the canonical variable names from unified-design-system.
  Also ships the global :focus-visible rule, rather than leaving each consumer
  to remember it.
- RangeSlider: reconciled from the two drifted copies. Takes the accessible DOM
  from awesome-mcp-explorer (real <button> thumbs with role="slider" and the
  aria-value* trio) and grab-the-nearer-thumb from dufs-cloud. Keyboard support
  (arrows/PageUp/PageDown/Home/End) is new: both donors were pointer-only, which
  the design system rules out, and an arrow-key-deaf role="slider" is an ARIA
  role that lies.
- quantile.ts + fuzzyMatch: the genuinely duplicated helpers.

FilterBar is deliberately absent. The two same-named components share no
structure -- a horizontal <select> strip over files vs a vertical tri-state chip
sidebar over servers, with disjoint FilterState, props and sort keys. It is the
name-collision-is-not-a-duplicate trap, so extracting it would ship a widget
nobody imports.

dist/ is committed (and un-ignored against the root rule) because Vite does not
transpile dependencies; consumers stay at zero config. Consumed via pnpm's
&path:/web_ui suffix -- npm cannot install from a subdirectory of a git repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 14:40:45 +02:00
d25f2885da gatelock: 0.5.0
Some checks failed
gatelock pre-commit / pre-commit (push) Has been cancelled
gatelock tests / test (3.10) (push) Has been cancelled
gatelock tests / test (3.11) (push) Has been cancelled
gatelock tests / test (3.12) (push) Has been cancelled
Phase 2 adds four exports (make_button, heading, row, WidgetGroup) plus a
wheel-scroll fix in ScrollableSurface. Minor, not patch: the surface grew.
Purely additive -- nothing was removed, so consumers still on the 0.4.1 pin
keep working unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 14:21:10 +02:00
d82107ab9a gatelock: bind the wheel per descendant in ScrollableSurface
Wheel-over-text did not scroll. Tk dispatches through *bindtags*, not the
parent chain -- a child's tags are (itself, its class, the toplevel, "all") --
so binding <Button-4>/<Button-5> on `content` was never consulted for an event
over a Label sitting on top of it. Content that fills its viewport covers that
frame completely, which is every screen this class exists for, so the mouse
wheel was dead exactly where a user points it.

Found while retiring leetcode-guard's hand-rolled _scrollable, which got this
right by accident: it used bind_all, which works but installs the handler on
every widget in the application, including other windows' scroll regions.
Binding per descendant inside the existing _track_focus walk keeps it scoped,
and reuses that walk's WeakSet so a repaint cannot stack duplicate handlers.

Verified both directions under xvfb (Button-5 scrolls down, Button-4 back up)
from an event generated on a Label, not on the canvas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 14:12:30 +02:00
c963cd88a8 gatelock: add shared button, heading/row and WidgetGroup
Phase 2 of the shared-component work. gatelock owned the tokens but stopped
short of composite widgets, so four gate apps each built the same ones and
drifted apart doing it.

widgets.py:
- make_button(), from diet-guard's _gatelock_buttons. Emphasis is a variant,
  not a colour pair, so the text colour is picked *from* the fill and a call
  site structurally cannot produce the fg-vs-on_fill contrast bug. Binds
  <Return>/<KP_Enter>, which Tk does not give tk.Button on X11, and takes its
  focus ring from LockConfig (Tk's default 1px black is invisible on #211D1B).
- heading() and row(), from leetcode-guard's _status_sections. row()'s wrap
  width is a parameter rather than the donor's module-level dict, which made
  every row's wrapping depend on whichever window rendered most recently.

widget_group.py:
- WidgetGroup, replacing ~916 lines of per-output fan-out across four repos.
  Takes the union of what they needed and the fixes only some of them had:
  first -> W|None (three donors could IndexError on zero outputs), TclError
  suppressed per copy (one donor did not guard at all), focus deliberately
  singular rather than fanned out, and kwargs typed `object` so command= is
  expressible. Stores (output_name, widget) pairs, which designs out a latent
  strict-zip ValueError in the donor that kept two parallel lists.

Additive only: nothing is removed from the consumers in this commit, so all
four gates keep working on their current pin. 100% branch coverage, real-Tk
tests under xvfb.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1qGVUMjmtppci6Fjxs9xS
2026-08-14 23:54:52 +02:00
b9e54380db Add copy-paste session prompts for Phase 2 and Phase 3
Each is written to be pasted into a fresh session with nothing added.
That means carrying the answers the spec gate would otherwise demand --
goal, the decisions already made, boundaries, and a measurable done
condition -- and opening with an explicit override so the gate does not
fire a question round on a spec that is already complete.

Both point at nielsen-audit.md for context but do not depend on it to be
actionable, since a fresh session will not have read it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
2026-08-14 23:38:17 +02:00