Commit Graph

91 Commits

Author SHA1 Message Date
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
b4f1b32cef Record the four palette migrations as done
focus_owner, kuhylog, epopeja_karta and billsplit are all on the frozen
palette now, so the "drift to correct" table becomes a record of what was
fixed rather than a to-do. diet-guard and macro-cam remain.

Writes down the two deliberate exceptions, because both look like
oversights to anyone who did not make them:

epopeja_karta imports AppPalette rather than buildDarkTheme(). Its nine
slots were already byte-identical to the frozen palette, so only the
transcription was local, and it is one dense screen with a test asserting
nothing scrolls -- the shared theme's larger type scale overflows it by
34px. Importing tokens leaves the rendered ThemeData identical, so that
test passes by construction rather than by tuning sizes until it fits.

billsplit keeps its four category-dot colours. They encode which category
an item is, not a theme role, and a single-accent system has no four
distinct hues to lend them. That is now flagged as the open
categorical-ramp question: the design system has no answer for "N
mutually distinguishable hues", and it needs one before any chart, tag or
category UI can be built on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
2026-08-14 23:37:19 +02:00
480a8bdd82 Stop const-folding EmptyState's constructor out of coverage
Some checks failed
design_system tests / test (push) Has been cancelled
Removing the action slot made the constructor const-invocable, so the
test's `const EmptyState(...)` was evaluated at compile time and the
constructor line never executed -- 99%, caught by the pre-push gate
rather than by review, which is the point of having it.

Building the message through a helper keeps the invocation non-const.
That is also closer to real call sites, which interpolate runtime state
into the string rather than passing a literal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
2026-08-14 23:10:43 +02:00
3dab75fde5 Record which design_system exports actually have a live consumer
An extracted component with no importer is the failure mode the audit
exists to prevent, so leaving that implicit was the wrong call.
EmptyState and AppStatusColors ship ahead of their consumers -- both have
real donors waiting, and AppStatusColors has to exist for showError to
read the danger hue off the theme -- but neither is validated, and the
table now says so rather than letting the package read as fully proven.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
2026-08-14 23:09:45 +02:00
522fa1dff0 Wire design_system into CI, and drop EmptyState's speculative action slot
The pre-push coverage gate only ran for whoever had the local hook
installed, which is deployment hygiene rather than a gate. Adds the
mirrored workflow sync_settings_ui already has, so the 100% bar is
enforced on push and PR.

EmptyState.action was invented surface: no donor had it. home_inventory's
three call sites are pure placeholders, and kuhylog's action-bearing
_empty was explicitly excluded from the survey as a different element.
Shipping a parameter nobody asked for on a widget that has no consumer
yet is the failure mode this package is supposed to avoid, so it goes,
with a comment recording why rather than leaving the next reader to
rediscover it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
2026-08-14 23:08:59 +02:00
cae0870fd0 Add the Nielsen coverage audit alongside design_system v0.1.0
The grid maps Nielsen's 10 GUI elements to what actually exists per stack,
and records the deferred clusters (GitHubMirrorScreen x5, SettingsScreen x6,
~20 list tiles, filter sheets) with repo, path and line count so none of it
needs rediscovering.

It also records three corrections the measurement turned up, because each
one changes what is worth doing next:

- The "343 raw color literals" are not spread through call sites; they are
  concentrated in the hand-copied theme files. todo had 35, all 35 inside
  theme.dart. Seven repos have zero literals outside their token file, so
  deleting it removes essentially all of them. The repos with genuine
  call-site leakage are billsplit, epopeja_karta and focus_owner.
- There are 4 AppStatusColors, not 5, and they disagree about their own
  field sets; the package's version is a superset so nobody loses a field.
- There are 3 section headers, not 5, and no _SectionLabel exists.
  epopeja_karta's is a different element (an eyebrow, not a section title)
  and was deliberately left out rather than force-fitted.

Also records that workout_app, cited as a consumer in the plan and in
several skill docs, does not exist on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
2026-08-14 23:01:31 +02:00
e6c9af81e9 Add design_system: the shared Flutter component layer
unified-design-system/ froze the token layer as prose -- 1220 lines of
README, tokens.md and annotated HTML, and zero importable code. Every
Flutter repo transcribed the token table by hand, so the "shared" palette
was enforced only by discipline, and had already drifted: 10 divergent
theme.dart copies (todo and untools byte-identical, the other 8 each
unique), ~318 raw color literals, 4 hand-rolled AppStatusColors extensions
with inconsistent field sets, ~35 inlined AlertDialogs, ~38 raw SnackBar
sites with no helper anywhere.

This promotes the token layer into an importable component layer, so the
design system is enforced by the compiler and the package manager instead.
The components own the tokens: a consumer has nothing local left to edit.

Scales are the six-repo consensus, which is also what tokens.md specifies;
drifted repos get corrected to this rather than the reverse. Themes are
built from explicit ColorSchemes, never fromSeed -- the palette is
hand-picked for contrast, and secondary/tertiary are set explicitly so
SegmentedButton cannot fall back to stock Material teal.

Widgets are extracted only where two or more repos already had a
structurally similar implementation; SectionHeader reconciles untools'
trailing-action variant with billsplit's padded one as an opt-in union, so
either caller can adopt it with no visual change.

63 tests, 102/102 lines covered, gated at 100% on pre-push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XS1m7b1piBtAL22USLW6jL
2026-08-14 22:51:06 +02:00
ca81e091a1 Skip git-ignored files in the length gate, and vendored skill bundles
Three exemptions the first cut missed, all of them files no commit could
ever fix:

- Git-ignored files (todo's exported BACKLOG.md, logs, scratch dumps).
  These are rewritten wholesale by whatever produces them, so splitting one
  is meaningless, and pre-commit never sees them anyway. Applied to
  explicitly-named paths as well as --all, so the two modes cannot disagree
  about the same file.
- Vendored agent-skill bundles (.agents/skills, .claude/skills). A SKILL.md
  is a fixed-name file with frontmatter; splitting one breaks the format.
  Matched by directory name wherever it appears, since these get dropped
  into any repo.
- .utils, where CI checks out this repo. Without it a repo's lint job fails
  on utils' own long files, which that repo cannot fix.

Baseline accordingly drops from 657 to 597. Re-verified all eight fixtures:
over-cap fails, under-cap passes, generated/vendored/wordlist/git-ignored
exempt, prose still capped, and the gate still passes on its own source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7yZz45ezmzCB89Cv5gAtZ
2026-08-14 15:58:46 +02:00
c54211f112 Add file-length survey baseline (2026-08-14)
The measured distribution the 250-line cap was derived from, and the 657
files currently over it, so the baseline is reproducible rather than a
number in a commit message.

Stored as JSON, not a text list: a 651-line .txt would itself have failed
the gate, and the data belongs in a data file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7yZz45ezmzCB89Cv5gAtZ
2026-08-14 14:35:42 +02:00
c9bf289510 Add shared 250-line file-length gate
Files that cannot be read in one piece force re-reads and partial edits,
which is the largest avoidable cost in an LLM-assisted workflow. This adds
the enforcement side of a 250-line cap covering code AND prose.

- file_length/config.py: single source of truth for the cap and its
  exemptions (generated, markup, vendored, data-ish txt), shared by both the
  gate and the survey so the two cannot silently disagree.
- file_length/check.py: the check itself. Exits 1 and lists violations;
  adjudication is an exit code, not a judgement call.
- scripts/check_file_length.sh: thin wrapper pre-commit calls with staged
  files.

Verified against six fixtures: over-cap code fails, under-cap passes,
generated l10n (1417 lines) exempt, vendored pdfjs exempt, the 174800-line
wordlist exempt as data, and a 1101-line CLAUDE.md correctly fails since
prose is capped too.

Baseline at time of writing: 657 files over 250 lines across 29 repos.
Each repo carries a refactor_claude_todo.md prompt with its own list.

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