Commit Graph

17 Commits

Author SHA1 Message Date
fdce5a1031 Move the crdt-sync pin to v0.3.0
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
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
2026-08-03 20:08:53 +02:00
58e6731705 Port the MCP server to mcp 2.x, and say what each tool is allowed to do
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
2026-08-03 20:02:00 +02:00
df5916f616 Pin mcp below 2.0, which dropped the module _mcp.py imports
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
2026-08-03 17:45:50 +02:00
af81136861 Give pylint the repo root, so a script outside the package lints on its own
pylint infers sys.path from the files it is handed, so a file in scripts/
resolves its package imports only when a package file happens to be in the
same batch -- passing or failing by what else was staged. Sibling repos hit
exactly that in CI today. An init-hook puts the repo root on the path
unconditionally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
2026-08-03 16:38:20 +02:00
4a5e350baf Fit the meal gate on 1366x768 with a full day logged, and stop the auto-scroll
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
2026-08-03 16:00:13 +02:00
1702685afd Make the meal gate usable without a pointer on a 1366x768 screen
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>
2026-07-30 16:21:27 +02:00
c6663da06a Pin gatelock 0.2.1 for the recovery-loop fail-open fix
0.2.0's recovery loop scheduled the next tick after running it, so any
exception out of a tick permanently ended all re-assertion of coverage, grab
and VT for the rest of the lock -- silently, since Tk logs and swallows
exceptions from `after` callbacks. 0.2.1 moves the reschedule into a
`finally`.

Also in 0.2.1: `holds_grab` no longer reads a grab held by one of our own
transient children (a posted menu) as "the grab was lost" and takes it back.
That one was a real, user-visible bug on screen-locker's gate windows; the
same class of widget on a diet_guard gate would have behaved the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:21:19 +02:00
84bd550331 Migrate diet_guard to gatelock v0.2.0 per-output locking
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
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.
2026-07-25 16:56:13 +02:00
2a772d624f app: warm-tint the shared palette, fix Snack chip + on-fill contrast
- 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).
2026-07-21 21:16:26 +02:00
8a9c7713b9 Review fixes: specific sync exceptions, clearer names
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Address code-review feedback on the sync-before-lock change:

- pull_shared_log now catches (SyncError, GitHubSyncError, OSError) instead
  of a blanket `except Exception`. crdt_sync wraps every requests transport
  error in GitHubSyncError, so those three are the whole realistic failure
  surface; the gate still fails closed on all of them, and a genuine bug now
  surfaces instead of being hidden as "sync unavailable". Drops the
  broad-exception pylint disable and the per-file BLE001 ignore in pyproject.
- Rename _cli_gate._is_due -> _should_lock: too easily confused with the pure
  local predicate gate_is_due it wraps. The new name says what it decides.
- Rename the list `satisfied` -> `satisfied_slots` in _reconcile_after_fetch;
  it's a list of slots, not a boolean.

No behaviour change; 100% coverage retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd
2026-07-11 18:33:43 +02:00
be94b4e147 Sync before locking the gate + manual "Fetch from sync" button
The PC gate forced a re-log when a meal was already logged on the phone
but the 15-min sync had not yet propagated it. Two ways to close that gap:

- The gate now pulls the shared log right before it would lock -- and only
  when a lock is otherwise due, so the network cost is paid on the rare
  tick that matters. A meal already pushed by the phone then unlocks
  without re-entry. Fails closed: any sync error (no token, network, disk
  write-back) falls through to the local lock decision, never skips it.
- A "Fetch from sync" button on the lock screen runs the same pull on
  manual request: it auto-unlocks when the pull satisfies every pending
  slot, advances to the next meal on a partial pull, or reports no change.
  Disabled in --demo so the sandbox never triggers a real sync/push.
- Both paths share one fail-closed wrapper, _sync.pull_shared_log(), which
  also keeps the single broad-except in one place.
- _write_log is now atomic (temp file + Path.replace): the gate and the
  15-min sync timer can now both write the log, so a torn read must be
  impossible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FtSmzACzJSGyEatgK9exkd
2026-07-11 17:54:38 +02:00
e6812f7b4f Fix pre-existing CI failures (deps, lint, whitespace)
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
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
2026-07-10 23:55:56 +02:00
59d1c29dd2 Migrate sync to shared crdt_sync library (Python + Dart)
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
2026-07-10 23:49:43 +02:00
06999274d7 Add MCP server exposing diet status + a gated log_meal
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Expose diet_guard's read surface and one gated write action as typed MCP
tools (diet_guard._mcp) so Claude Code and its subagents can query today's
intake and -- with explicit confirmation -- log a meal without shelling out
to the interactive gate CLI.

- _mcp.py: FastMCP stdio server. Read tools get_status, list_today,
  get_slots expose today's CONSUMED kcal/macros and the qualitative
  consumption band only -- never the raw daily budget, remaining number,
  body weight, or protein target (those would back out the deliberately
  hidden budget). Gated write log_meal previews unless confirm=True and
  degrades gracefully on write failure. Nutrition is grouped into a Macros
  model (kcal + optional protein/carbs/fat) to keep the tool under the
  arg-count limits with no lint suppression, mirroring the CLI's grouping.
  Logs to stderr; no secret (HMAC key / sync token) is ever returned.
- .mcp.json + scripts/setup_mcp.sh: registration via a dedicated
  ~/.venvs/diet-guard-mcp interpreter; mcp is an optional-dep, kept out of
  the systemd/CLI system-python path.
- 100% coverage of the new module; verified end-to-end over stdio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQGQj8Ne6TriBExv91nG8v
2026-07-10 21:36:46 +02:00
a121da211d Point sync at the consolidated utils/syncs monorepos
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
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>
2026-07-10 14:12:16 +02:00
95acf8a376 Fix setuptools flat-layout package discovery broken by app/
Adding the Flutter app/ directory made setuptools' auto-discovery see two
top-level packages and refuse to build, breaking install.sh's pip step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RH2BHCKbDTiYJUMG3rb9nq
2026-06-22 22:51:02 +02:00
843f5e0221 Extract diet_guard from testsAndMisc as a standalone repo
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).
2026-06-22 12:18:39 +02:00