This repo had only the pre-push hook wired into .git/hooks, never pre-commit,
so every pre-commit-stage hook here silently never ran locally -- including the
version bump added in the previous commit, which is why that commit did not
bump its own version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aoa1nKWRrAbowz7Hfd3tgL
The AUR package sources a clean vX.Y.Z tag, which only moves when the pubspec
version changes by hand -- so the packaged version drifts behind HEAD. Bumping
the patch per commit keeps the tag tracking HEAD. The Flutter app lives in
app/, so the hook is pointed at app/pubspec.yaml explicitly.
Patch, not minor: minor/major are the "how much broke" signal, and
auto-incrementing them on every commit throws that away.
Two guards keep the number honest: re-running the hook over the same staged
change is idempotent (the bump is computed from the last COMMITTED version, not
the working tree), and a rebase/merge/cherry-pick in progress is skipped so
replayed commits don't each inflate it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aoa1nKWRrAbowz7Hfd3tgL
app/install_arch.sh generates a PKGBUILD into a mktemp dir with no source=() and
a package() that copies from hardcoded absolute paths under /home/kuhy. That
only ever works on one machine, so it cannot be published. This PKGBUILD builds
from the GitHub release tarball (v1.0.1) instead, and fixes what AUR review
would reject: license=('MIT') with the text actually installed to
/usr/share/licenses, real depends (glibc/bash/hicolor-icon-theme, found by
namcap), and the desktop entry plus icons installed into the package rather than
into ~/.local/share after the fact.
The app lives in app/ while LICENSE is at the repo root, so build()/package()
cd into the subdir and install the license from an absolute $srcdir path.
v1.0.0 was already taken by an older commit predating the LICENSE, hence v1.0.1.
The README documents the tagging hazard: this clone carries grafted upstream
Flutter SDK tags that must never be pushed with `git push --tags`.
install_arch.sh keeps its working-tree dev loop and is deliberately not wired to
this file. Verified: makepkg -Cf from a clean dir with only packaged Flutter on
PATH, namcap clean, pacman -U, and `diet-guard-app` serving on port 8732.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aoa1nKWRrAbowz7Hfd3tgL
diet_guard pushed as "pc" (Python), "phone" (Android) and "desktop" (web).
A role constant collides the moment a second machine takes the same role,
and after a reinstall the new install inherits the previous one's CRDT
identity -- the same reasoning home_inventory already records for choosing a
uuid. Both halves now mint a uuid once and persist it: the PC in
~/.local/share/diet_guard/.device_id, the app under SharedPreferences
'crdt.nodeId' (the key todo and home_inventory already use).
The old constant survives as the legacy id and is passed to sync_log /
syncLog, so devices/<legacy>/ is skipped as this device's own. Without that
every tick would re-download and re-merge this device's own pre-migration
log as though a peer had written it -- idempotent under LWW, but hundreds of
KB, 96 times a day. It drops to None once that path is reclaimed; a test
covers both states, including a negative control proving the skip is the
legacy id's doing.
All four documents move together (food_log, food_bank, food_bank_manual,
budget), and the hist:<date> fields riding on the budget record are
untouched -- merge_record unions field names, so they relay through
unchanged.
The resolver lives in its own module (_device.py / sync_device_id.dart)
because _sync_merge needs it for HLC stamping too and importing _sync there
would be a cycle. Dart resolves it asynchronously at startup, before
LogStorageService.init(); currentSyncDeviceId falls back to the compile-time
role constant until then, so a too-early Hlc stamp gets a valid id rather
than crashing.
conftest redirects the new id file in the same change that introduces it --
without that a test mints a uuid into the real state dir and this machine's
live sync identity is decided by whichever test ran first. Verified by
checking ~/.local/share/diet_guard/ before and after: untouched.
Verified live, not just against mocks: the sync timer ran mid-change and
migrated this machine to f8f46da6. All 358 food_log records and all 100
food_bank records are present under the new path (0 missing vs devices/pc/),
and the new log's HLCs carry only its own id -- confirming it did not
re-merge the old path as a peer.
Python: 687 tests, 100% branch coverage. App: 518 tests, analyzer clean.
Pins bumped to crdt-sync-v0.6.0 / crdt_sync_dart-v0.7.0 (pyproject,
requirements.txt, .pre-commit-config.yaml, pubspec), and the new library is
installed into /usr/bin/python's user site-packages so the production timer
does not ImportError on its next tick.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZH89WEqAjuJT8WZdqc7AP
The largest consumer moves last on purpose: diet_guard was 71% of all sync
traffic, and both its surfaces (the PC timer and the phone app) share one
prefix, so they move together.
Firebase becomes the primary backend when it is set up, with GitHub kept as a
mirror so a device that has not moved yet still converges. An unconfigured
device keeps syncing exactly as before, which is also the rollback.
Revision tracking is new here. The PC hand-rolled its own pull-merge-push
instead of calling sync_log, so it had none: every tick re-downloaded every
peer's whole food log -- hundreds of KB, 96 times a day -- and re-pushed its
own regardless of change. It now reads one small revision map first, skips any
peer whose hash is unchanged, and suppresses a no-op push. Every sub-sync
(log, budget, food bank, manual bank) takes the client as a parameter, so one
swap covers all four.
The app could not have reached Firebase at all: nothing called saveAccount(),
so loadAccount() always returned null and openFirebase() always returned null.
Settings now has a Firebase section that stores the account in the OS keystore
and signs in immediately, so a typo surfaces on the screen where it can be
fixed rather than as a silently failed background sync hours later. The email
and password never touch the repo -- it is public.
Platform reaches now degrade instead of failing: an unavailable keystore or
application-support directory costs one tick of extra traffic, where before it
would have taken sync down entirely. Caught by `on Object` rather than
`on Exception`, because an uninitialised binding raises an Error.
Verified end to end against the real database, not just in tests: a live PC
sync merged 57 days / 339 entries and put 221 KB of food log plus 97 food-bank
records into Firebase; a second unchanged tick issued zero pushes; and the
Dart client signed in and read back all 339 meals and 97 foods, then completed
a push / publish-revision / suppress-no-op round trip under a throwaway prefix.
diet_guard: 618 tests at 100% branch coverage.
diet_guard_app: 477 tests. Two assertions moved from 4 pushes to 5 -- the
extra one is this device's revision, published after the log so a peer can
never cache "seen rev X" against a log it never received.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
Your in-progress bump, committed as its own change so it is not buried in
anything of mine. All three declaration sites move together -- the manifest,
the lockfile-equivalent and pylint's hook env -- because a pin that lags the
others lints the code against a library it does not actually run with.
Verified in a clean venv that v0.3.0 still provides every symbol this repo
imports: GitHubSyncClient, GitHubSyncError, Log, merge_logs, Hlc and Record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
mcp 2.0.0 removed mcp.server.fastmcp; MCPServer is the same object under a
new name, so @mcp.tool() and mcp.run() are untouched and every tool keeps its
name, signature and returned shape. Verified against a clean 2.0.0 venv: all
four tools list, and the suite passes at 100% coverage, 609 tests.
Two things the port buys, neither of which existed in 1.x:
* Tool annotations, which matter more here than in the sibling repos because
this server has a tool that writes. The three readers are marked read-only,
idempotent and closed-world; log_meal is marked not-read-only but
explicitly NOT destructive -- it appends and never rewrites or deletes --
not idempotent, since a second confirmed call logs a second meal, and
open-world, because it reaches Open Food Facts when macros are not given.
A client can now tell the difference without reading the docstring.
* Structured output: 2.x derives an output schema from the return types and
sends structured content alongside the text. No code change -- it falls out
of the type hints that were already there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
CI has been red since 2026-07-30 for a reason unrelated to any commit here:
mcp 2.0.0 removed mcp.server.fastmcp, which _mcp.py imports at module scope,
and the manifests asked for `mcp>=1.9` with no upper bound. Verified
directly -- 2.0.0 raises ModuleNotFoundError on that import, 1.29.0 provides
it. It failed pylint here rather than only the test job, because the repo
root now reaches pylint's import resolution.
Pinned `<2` in all three places the dependency is declared until the server
is ported to the 2.x API.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
gatelock 0.4.0 (pinned here) stops the viewport following the app's own
focus_set(): on 2026-08-03 that moved the screen locker's lock screen to
mid-content and back with nobody touching the keyboard, and this gate uses
the same viewport. Scrolling now happens only on the user's own keypress,
wheel or click.
That makes fitting the screen the gate's own responsibility rather than
something a scrollbar covers for, and with a day of meals logged this one did
not: 770px of content in a 739px pane. Trimmed the vertical rhythm (the
title is one step down the scale, and the gaps around the description,
suggestions, macros and footer are one step tighter) for 718px, with room for
the dashboard to keep growing.
Its layout test now requires the gate to *fit* rather than merely be
reachable by scrolling, empty and populated. Dropped the 1024x600 tier: no
machine runs it, this gate cannot reach it without dropping content, and a
target nobody uses is a constraint that only ever costs.
The probe presses Tab before moving focus, because a bare focus_set() no
longer scrolls -- it would have been testing a path no user can take. The
Tk fakes learned pack_forget(), which the viewport calls to take the
scrollbar down whenever the content fits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
The Log Meal tab did not fit this laptop, and had not for some time. Measured
under a real 1366x768 X server: it required 866px inside a 739px content pane
with every field EMPTY, and 938px with a day of meals logged.
Because the content frame was place()-centred it was sheared at BOTH edges at
once -- the "Diet Gate" title and the slot header (which meal you are being
asked to log) off the top, the calorie headline and the whole dashboard off the
bottom -- with no scrollbar anywhere in the package and nothing to indicate
content was missing. This is a hard lock: overrideredirect, a global grab, VT
switching disabled. Unreachable content is unrecoverable.
Two independent causes, both needed fixing:
- Every font size was handed to Tk as *points*. Tk encodes the unit in the
SIGN of the size, and _gatelock_typography carried a comment asserting the
opposite ("Tk has no separate px/pt distinction"). At ~100 DPI that made
every string ~30% oversized. Negative sizes now; the docstring records why.
- There was no scroll container. build_layout now uses gatelock's
ScrollableSurface, which top-anchors, scrolls by keyboard, and scrolls to
follow focus.
Font sizes alone were not enough: they take the layout to 702px, still over the
571px pane at 1024x600. Both fixes are load-bearing.
The grams/items selector was mouse-only. tk.OptionMenu's Menubutton defaults to
takefocus=0, so ::tk::FocusOK rejects it and it never entered the tab ring --
verified by walking the real ring, which ran Entry -> Text -> Listbox -> Button
-> TNotebook and skipped it entirely. Switching units rescales the whole entry,
so a pointerless user could not correct "250 grams" to "3 items" inside a lock
they cannot leave without logging a meal. It is now two Radiobuttons, which are
in the ring by default and take both Space and Return -- and which post nothing,
unlike a menu, whose grab-stealing is why screen-locker bans OptionMenu on lock
surfaces outright.
Also: the focus ring was invisible (Tk's default is 1px BLACK on #211D1B) and
_build_desc actively inverted it by setting highlightbackground, the UNfocused
ring, so the box outlined while idle and went black the moment it took focus.
Enter now activates buttons (X11 Tk binds only <space>). Text widgets no longer
trap Tab. notebook.enable_traversal() is called, so Ctrl+Tab and Ctrl+PageDown
work. wraplength drops 900 -> 640, the line-length cap, which also stops a
label forcing a 900px-wide layout onto a 1024px screen.
The Flutter app had two controls with no keyboard route at all. Its desktop
target is the web build in a Chrome --app window, so a touch gesture is not a
fallback there, it is the only interaction:
- Deleting a history entry was bound solely to onLongPress. Long-press has no
keyboard equivalent whatsoever. It now has a focusable IconButton; the
long-press stays as a redundant pointer shortcut.
- Calendar day cells were a bare GestureDetector, so navigating to
date-filtered history was pointer-only and invisible to a screen reader.
Now Semantics(button: true) over an InkWell.
Plus: the filter sheet built a TextEditingController inside build() with the
caret forced to end-of-text, so every keystroke discarded it and silently undid
mid-string Home/End/arrow editing. And nothing capped width, so on the 1366px
desktop window prose ran ~180 characters per line (twice the readable limit)
and a four-digit kcal field spanned ~1334px. AppWidth adds those caps.
Tests: the render check is a failing test, not a script -- ruff runs with
--unsafe-fixes, whose T201 fix DELETES print statements, which would silently
turn a diagnostic into a no-op. test_gate_layout_fits.py was verified red on
the pre-fix code ("content 866 in viewport 768 -> NO SCROLL VIEWPORT ->
UNREACHABLE CONTENT") and green after. The Flutter suite gains its first
keyboard-reachability assertions; it previously had ~109 tester.tap calls and
zero checks that anything was focusable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate is laid out once per live output instead of once on the primary,
and claims the screen through an Arbiter at RANK_DIET_GUARD so the wake alarm
and the workout locker can both outrank it.
Three kinds of state needed three different answers. The display fields
already fanned out for free -- GateVars holds root-mastered StringVars bound
to labels. The typed-in fields did not: amount, per, meal name and the four
macro cells were bare tk.Entry widgets, so N copies would have been N
independent forms. They are variable-backed now, which is what makes logging
work from whichever screen the user is actually looking at. The description
box and the suggestion list cannot be shared at all -- tk.Text and tk.Listbox
have no variable -- so those genuinely diverge and reads answer from the copy
the user touched.
The History tab is per-surface too: its grid, budget field and edit button
are driven across every monitor, with the budget likewise variable-backed so
the value cannot differ between screens mid-edit.
Laying the gate out on the primary monitor only was considered and rejected:
not having to hunt for the screen the form landed on is the entire point of
per-output locking.
Tests: the suite hung rather than failed on first run -- diet_guard patches
its own `tk`, but LockWindow builds Toplevels through gatelock's, and real
tkinter over a MagicMock root recurses without bound. `_hermetic_gatelock`
closes that and redirects the runtime dir so no test can stand a production
locker down. Every other test runs single-output, where fan-out is
unobservable, so test_gatelock_fanout runs on two; each assertion was checked
by cropping the fan-out loops to the first surface and confirming it goes red.
_gatelock_ui and _gatelock_calendar were each split in two to stay under the
500-line limit, and GateVars' typed-in fields moved into a nested
GateEntryVars rather than raising pylint's attribute cap.
470 tests, 100% branch coverage, pre-commit clean, no lint suppressions.
- theme.dart: warm unified-design-system palette, on-fill for
onPrimary/onError, and inputDecorationTheme/dividerTheme (previously
unset -> every TextField/Divider fell back to Material defaults).
- slot_selector_row.dart: the Snack ChoiceChip had no explicit
backgroundColor/side/labelStyle like the hour chips, so it fell through
to Flutter's own default -- a literal pure-white border.
- Regenerated launcher icon assets (warm charcoal + warm gold accent) via
python_pkg/app_icons.
- Bump gatelock pin to the released v0.1.1 carrying this same palette.
Verified: flutter analyze clean, diet_guard pytest 443/443 @ 100% coverage,
phone-installed APK pixel-checked (Snack chip border, on-fill contrast).
Mirrors the CI-green safeguards rolled out repo-wide (see screen-locker):
- scripts/check_requirements_sync.py: pre-commit hook that fails if a
package import isn't declared in requirements.txt. On rollout it caught
a real latent gap — diet_guard/_mcp.py imports pydantic but it was only
present transitively (via mcp); now declared explicitly.
- scripts/ci_mirror.sh: pre-push gate = requirements.txt-only venv
(hash-gated) running `pre-commit run --all-files` + `pytest`, replacing
the plain pytest-coverage pre-push hook.
Only the tooling files + the pydantic declaration are committed; unrelated
in-flight work in the tree is left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PMRRVVkeRyrSaaqueMZKG
CI on main was already red before the crdt_sync migration. Make the
pre-commit --all-files run and the test job green:
- requirements.txt: add crdt-sync (now a runtime dep) and mcp so CI's
`pip install -r requirements.txt` can import diet_guard._sync and
collect test_mcp.py (both were ModuleNotFoundError in CI).
- .pre-commit-config.yaml: add mcp + pydantic to the pylint hook deps so
it can resolve diet_guard._mcp's imports (was E0401).
- pyproject.toml: raise pylint min-similarity-lines to 8 — the shared
nutrition-field keys in _foodbank/_mcp mirror one domain schema, not
real duplicate code.
- scripts/setup_mcp.sh: split declare/assign for SCRIPT_DIR/REPO_DIR
(shellcheck SC2155).
- codespell: fix "packge"/"unparseable" typos; trailing-whitespace
auto-fixes across app/.agents/skills docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
Replace the bespoke GitHub-Gist sync with the shared crdt-sync /
crdt_sync_dart libraries so diet-guard uses the same CRDT transport as
the other apps.
- Python: delete _sync_github.py (+ its test); rewrite _sync.py,
_cli_sync.py, _sync_merge.py against crdt-sync. Add the crdt-sync git
dependency (pyproject + pylint deps) and S107 to the test ignore list.
- Dart: rewrite app sync_service.dart / sync_merge.dart against
crdt_sync_dart (new git dep + crypto); update log_meal and settings
screens and their tests.
Verification: pytest 347 passed at 100% coverage; flutter analyze clean;
flutter test 306 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RRCdR4D8SaVrN2xraMQxM
gatelock and diet-guard-sync were folded into shared monorepos:
gatelock -> github.com/kuhyx/utils (subdirectory), diet-guard-sync ->
github.com/kuhyx/syncs (subdirectory). Update the gatelock pip URL,
SYNC_REPO_NAME, the devices-dir path prefix (PC and phone), and add a
persisted-value migration so an already-configured phone picks up
'syncs' on next load.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rewrites python_pkg.diet_guard imports to diet_guard, vendors the
shared as_float coercion helper, drops the monorepo PYTHONPATH from
install.sh and the systemd unit (package is now pip-installed), and
scaffolds standalone lint/test config matching testsAndMisc's real
enforced bar (pylint --fail-under=10 with tests excluded and the
use-implicit-booleaness/consider-using-with disables, mypy's actual
disabled-error-code set, ruff ALL, bandit, 100% branch coverage).