Shipping log

Changelog

What changed, when. The five newest releases below; the full history lives in CHANGELOG.md on GitHub.

  1. v0.9.523

    2026-09-16

    September enterprise-audit remediation, third release: the last 13 of the 33 work packages from the 2026-09-12 audit (report: https://claude.ai/code/artifact/4491c841-9879-4859-86b3-a56c84a44a7f). Every package was built in its own worktree, independently reviewed, fixed and then re-reviewed at a ship gate that blocks only on a regression against production, a data-integrity or security problem, or a failing test; three packages were blocked and sent back before they passed. With v0.9.521 and v0.9.522 this closes the audit apart from the deferred `coach.py` module split and the generated OpenAPI types.
    
    This release is about the coach being right about food and about you: it learns your usual portions from logs you actually stood behind, keeps one identity per food, logs the last two days when you say "kagabi", asks one honest question instead of praising a number it just had to correct, and finally knows what time it is where you are. It is also the release where chat stops being slow under load.
    
    ### Accuracy and memory
    
    - **WP9:** Repeat foods stop swinging. The coach now learns your usual for a food from logs you actually stood behind (corrected, confirmed, or settled and unflagged), and a new estimate far above that usual is pulled down to it with a visible note that names the real numbers, while anything below it only asks. An automatic pull-down needs at least five accepted logs, three of them consistent and one you personally backed, plus a readable portion; a bigger stated count, size or weight always asks instead, and two higher logs in a row stop the pull-down so a genuine change in how much you eat moves your usual. One inflated snap can no longer become your usual or ride into the next photo's prompt, the quick-repeat chip offers your newest accepted, in-band log with its protein and a "varies" dot, and a repeat of an unreviewed or implausible log is disclosed instead of replayed.
    
    - **WP10:** The coach now remembers how you log a food. One canonical identity (base food + preparation + unit) replaces the five incompatible keys the camera used to invent, so fried, paksiw and sinigang tilapia are one food with variants and "your usual" finally builds on every log of it. A per-user ledger records only the logs you stood behind - never a number the app derived from your own history - and a correction always outranks an earlier estimate, so fixing a meal the coach matched to your usual moves your usual instead of being ignored. The coach can also answer from it: "my usual tilapia" reports your own numbers and says which preparations it used, and a preparation you have never logged is never priced from a different one.
    
    - **WP13:** Meal cards became editable per item: tapping a food opens a portion sheet (quarter to double, "Didn't eat this", or exact grams and counts) with a live macro preview, and saving goes through the same user-edit chokepoint as every other correction, so the header, the band and the item rows stay consistent and the card says exactly what changed. Two edits to one meal can no longer race each other into a duplicated breakdown. Snap cards also gained a one-tap "Looks right": it changes no number, it records that you vouched for the estimate - the user-backed evidence repeat-food consistency needs before it may quietly correct a later log - and confirming an implausible estimate still cannot make it your usual.
    
    - **WP15:** Snap replies stopped praising numbers the guards just had to correct. A flagged, uncertain or whole-item snap now says what it assumed and asks exactly one tappable portion question - Palm-size / Bigger / Smaller, or With bones / Meat only - about the item that is actually in doubt, and the answer applies as a correction in one tap. The question is decided once and recorded with the meal, so the chips, the reply and the tap always agree; a tap can never make a guarded number worse or contradict a portion you stated in your caption, the bone-in answer is remembered in the same place your typed answer goes (so the photo path stops re-asking), and tapping "Looks right" closes the question. Asking why a number looks wrong ("bakit ang taas ng protein?") now reaches the reasoning model with the meal's real portion assumptions as evidence, and the coach no longer counts its own snap replies as conversation depth.
    
    - **WP8:** Typed meals and meals the coach logs now keep their item breakdown: each confirmed food is saved as its own row, the rows always add up to the meal total, and item-level fixes ("half the rice") work on typed meals. Coach meal proposals list every food with its own macros; when the items cannot be reconciled with the totals the meal still logs exactly as before, header-only, and the mismatch is logged. A proposal that fails to stage can no longer leave a "Reply YES" ask alive that would commit an older card.
    
    - **WP25:** A snap captioned "kinain ko", "just had" or "2 pcs chicken" is always logged as food, never as a 0-kcal menu, while a menu board captioned "planning lunch", "lunch mamaya" or "ano order ko" still reads as a menu. Order confirmations, receipts and nutrition labels count as meals, menu picks carry real carbs and fat plus an allergy check, a second dish can be picked from the same menu, and "second cup of rice" is no longer hijacked by the pick lane.
    
    - **WP17:** Chat can log and fix the last two days. "Kumain ako ng adobo kagabi", "kahapon almusal" or "2 days ago" now land on that day, update that day's totals and status, and restore that day's streak and quests through the usual chokepoints, while anything older is refused in plain language instead of landing on today. A correction that names a day targets that day's meal and can never fall back to today's latest, the coach asks which day or which meal only when it genuinely cannot tell (and any other message releases the question and is answered normally), and everyday Taglish like "may 2 eggs ako", "sabado nag gym ako" or "the 15 pcs siomai" is never mistaken for a date. Filipino staples the intent gate used to miss - pancit, lumpia, siomai, pandesal, lugaw, paksiw na bangus - now log.
    
    - **WP12:** Photo results are cached per estimator version, so a fix to the food AI retires every stale cached answer instead of serving last month's numbers: the cache key now follows the same fingerprint the accuracy gate uses (vision prompts, escalation, the deterministic guards, the reference table and the model ids), rows from before the whole-fish fix are deleted on upgrade, and a cached hit is re-checked by today's guards without discounting twice. Only image-only passes are shared, never one conditioned on your profile, memories or caption, and every stored meal now records which estimator produced it.
    
    ### Speed
    
    - **WP26:** Chat is fast again under load, and a busy coach no longer slows the rest of the app: a coach turn never holds a database connection across a model call (it commits what it has, never rolls back, before each call), memory extraction and coach images moved off the reply path, and the OpenAI client is reused. On a 12-turn load test a turn's p95 fell from 24.5 s to 3.5 s, total pool wait from 135 s to 2.4 s, and an unrelated request during heavy chat from 12.2 s to 5.5 ms. The duplicate-send lock became a heartbeat lease, so a server restart mid-chat frees your next message in about five seconds instead of three minutes, while a retry still collapses to one reply, one card and one charge.
    
    - **WP27:** A photo analysis no longer holds a database connection while it waits on the AI: the worker claims the snap, makes its external calls with no transaction open, then commits once in a short finalize that re-reads the meal under a lock, so a recalculation running at the same time can never drop a meal from the day's total and a user edit mid-analysis is never clobbered. The day's celebration push is sent after the commit instead of while holding the day's row, the app stops polling the moment a photo is done (one query per tick instead of three), and a snap that crashes or is cancelled mid-flight leaves a recoverable, idempotent state. Connection hold per snap falls from 914 ms to 98 ms once the reply-rewriting pass is live; with today's behavior the phase split is roughly neutral and the win is the integrity work.
    
    - **WP29:** Today stopped re-rendering the whole conversation on every keystroke and every streamed word: typing a character now re-renders nothing but the composer (it used to repaint every chat bubble), a streaming reply no longer repaints meal cards or the header at all, and there are no long tasks while typing even on a throttled phone. A timeline refresh can no longer erase or duplicate the live reply - including after a dropped connection, which used to leave the half-finished answer stuck at the bottom of the thread - and the pending card survives until its snap really finishes. A failed photo can now be retried, typed instead or removed from the card, and a retry after our own failure is free (three a day) rather than costing a photo credit. Accessibility: 44 px targets everywhere, muted text contrast raised from 3.9 to 6.0 (dark mode 8.1), a dark-mode workout row from 1.07 to 16.1, night-mode tokens, and the design-token debt ratchets down.
    
    ### The coach knows you
    
    - **WP16:** The coach now knows what time it is for you and what you actually do. It reads your local clock ("last meal 2h10m ago"), keeps today's meals separate from earlier dated days, computes every average from finished days only - so a partial morning never reads as under-eating - and carries a compact, bounded profile of your habits: your usual meal times, your own foods and portions taken from your accepted logs, the weekday that genuinely slips, late-night eating, and whether the scale is following your logged deficit. Every claim needs enough evidence to be true, so it stays quiet rather than guessing. It suggests your own foods in Taglish, never nudges you away from something you actually eat (kanin, manok, itlog, isda included), and can forget a fact when you ask it to.
    
    ### Engineering and observability
    
    - **WP11:** Estimator prompt or model changes can no longer ship unmeasured: a zero-token fingerprint gate in pnpm check fails the build when the vision or text prompts, the deterministic guards or the model defaults change without a fresh measured benchmark, and a path-filtered weekly live 3x accuracy lane records a baseline on its first keyed run and skips cleanly (green, never blocking) while no eval key is configured. The text golden set is source-bound to the nutrition reference (75 g chicken breast, 250 g bangus with bones, one medium fried tilapia, Taglish phrasings), and production nutrition drift is watched nightly by a strictly read-only, UUID-only report with deduped signals.
    
    - **Integration (v0.9.523):** the eval CLIs (`scripts/coach_eval.py`, `scripts/meal_accuracy_benchmark.py`) load the developer `.env` when they RUN, not when they are imported, and the CLI tests restore `os.environ`; importing a CLI used to inject local settings (CORS origins, keys) into every `Settings()` built afterwards, which failed ten production-config tests only when the whole suite ran together. The vision estimator fingerprint carries a dated, reviewed waiver for WP25's meal-vs-menu prompt change (visible debt until the first keyed live run).
    
    - **Triage hygiene (v0.9.523):** a crawler asking for a URL this API has never served can no longer mint a Sentry issue — a 404 transaction that matched no route is dropped at capture, while a 404 raised by one of our own endpoints ("that meal is not yours") is still reported. The site also serves `/robots.txt` and `/sitemap.xml` for the first time (production answered 306 crawler requests for them with 404s in one week); the signed-in app, the API and account pages are disallowed, the public marketing and legal pages are listed.
    
    ### Migrations
    
    - Three additive, online-safe Alembic revisions in one linear chain: `20260912_0062` (vision-cache versioning + `meals.estimator_source/model/version`, and a one-time clear of pre-fix cached analyses), `20260912_0063` (`user_component_observations`, the per-user accepted-log ledger, plus an index on `meal_components.slug`), `20260912_0067` (the per-user daily behavior-profile cache).
    
    ### Fixed after the first keyed live run
    
    - **The "don't praise an implausible number" gate had never actually run.** WP11 put the rubric in the judge's user prompt, but the judge's exact JSON shape is declared in the shared system prompt and `plausibility_ok` was not in it — so the judge obeyed the shape, never returned the field, and every ranged scenario was recorded as a silent failure that measured nothing. The key is now requested for scenarios that carry reference ranges only (the 24 baseline scenarios keep a byte-identical system prompt, so the coach gate's comparison with the v0.9.516 runs stays honest), a judge verdict is read tolerantly (`true`/`"true"`/`1`), and a verdict the judge declines to give is recorded as `plausibility_unjudged` — still capped, but no longer counted as evidence against the reply.
    - **What the working gate immediately found (pre-existing, not from this wave):** in CHAT, the coach still endorses an implausible number the user states itself — it praised a logged 81 g protein fried tilapia and agreed with 78 g and with 50 g for 250 g bone-in bangus. Production (v0.9.522) replies the same way on the same scenarios, verified by running them against `main`, so this release is no worse; the deterministic edible-portion guard covers the PHOTO lane, and a matching rail for user-stated numbers in chat is the top follow-up.
    
    ### Known debt
    
    - The text and vision estimator lanes both carry a dated, reviewed "unmeasured" waiver: five changes across WP12, WP16, WP17 and WP27 moved a hashed estimator input, and no live 3x benchmark can run until `OPENAI_EVAL_API_KEY` is configured. The offline corpora, the deterministic guards and the 353-case battery all pass; what is unmeasured is the live model's behavior under the new prompts.
    - The coach reply "tone pass" has never run in production: `_split_reply_marker_suffix` compares against a whitespace-collapsing strip, so every multi-paragraph reply falls back to its deterministic template. Users therefore see the template copy this wave ships, which is why that copy is written to stand on its own. Pinned as a strict xfail; turning it on is its own scoped change with its own measurement.
  2. v0.9.522

    2026-09-16

    Nutrition-accuracy core of the September audit: the five packages behind the founder's report that a whole fried tilapia logged 570 kcal / 78 g protein ("why is it bigger than chicken breast?"). Root cause: v0.9.454's vision rule multiplied the whole fish's weight (head, bones, skin) by fillet protein density, and typed logs could save 100 g values against a 75 g portion. Each package passed a final ship-gate review that blocks on any regression against production. On a 353-case vision battery, protein error falls from 22.0 g (production) to 6.8 g (whole fish 43.7 → 7.4 g) with no food class worse and boneless foods unchanged. Also carries v0.9.521 (first 16 packages), which reached `main` but was not yet deployed.
    
    ### Fixed
    - **Whole and bone-in foods are priced from the edible part.** Photo logs of whole or bone-in foods are now priced from the edible part only (the whole-tilapia bug): the vision prompt estimates edible cooked weight first and a deterministic plausibility guard (`vision_plausibility.py`, inside `parse_meal_analysis` for both vision passes, the offline gate and cache hits) applies edible yields, per-piece protein caps, a density ceiling and a direction-safe calorie/macro check that never cuts real calories (drinks, label foods and oil-cooked dishes keep theirs; hidden oil is added as fat), keeps the user's stated counts and grams (caption authority; a remembered "with bones / meat only" answer is used, otherwise bones are assumed, disclosed and asked once), derives confidence instead of trusting the model's, and replaces generic praise with a short disclosure when it adjusts. The 2026-09-07 fried tilapia logged at 570 kcal / 78 g now logs about 180 kcal / 25 g; across a 353-case battery protein error falls from 22.0 g (production) to 6.8 g with no class worse, and boneless foods are unchanged. The guard fails open (model numbers at MEDIUM) if it ever errors, and the offline gate now asserts protein, macros, Atwater and components on 117 fixtures.
    - **Typed weights are honored correctly.** Typed logs with a stated weight are recomputed from that weight when the reference is confidently better than the model: "75g pan fried chicken breast" is ~141 kcal / 23 g protein (was saved at the 100 g values, 165 / 31, as HIGH) and the proposal shows the arithmetic. Only deterministic numbers can be HIGH; model-only numbers are capped at MEDIUM; multipliers ("150g x2"), raw weights, sauces and coatings, bony cuts and other qualifiers ("tuna panga", "salmon head", "smoked", "chicken tocino"), plate totals and pending-card revisions (including "sige/tama/nope, ..." corrections) keep or correct the model's number instead of forcing a plain row. Bone-in weights ("paksiw na bangus 250g") ask "with bones or meat only?" once per food and remember the answer, and the weighed-food gate never turns questions, purchases, plans or other people's meals into log cards.
    - **The fallback meal table reads weights as weights.** The deterministic meal table (the fallback when the AI parser is unavailable, and the source of the coach's "can I fit X?" answers) now reads weights as weights: every row declares its basis and uses the shared nutrition reference, so "75g chicken breast" is ~124 kcal (was ~1,900), "can I fit 150 g salmon?" adds ~309 kcal (was ~2,250), "1 and 1/2 cup of rice" is 1.5 cups, servings/orders/rolls/boxes price as servings, "coke zero burger" is not a zero count, body weights never attach to food, chicken is never counted twice, powder weights price as powder unless a drink is named, "chicken wings" defaults to a disclosed 6-piece order, and Filipino staples and rice variants are priced; an implausible single item asks instead of guessing.
    - **Meal cards agree with themselves after an edit.** Meal cards no longer contradict themselves after an edit: Calendar edits, chat and agent corrections and "add an item" share one user-edit chokepoint (`meal_header_edit.py`) that keeps the total, the calorie range and the plate breakdown in agreement, shows calories the items cannot explain as one "Not itemized" row, and (founder decision) rescales unstated carbs and fat on calorie-only corrections while the protein the user stated stays exact. Fixing yesterday's meal in chat now recalculates yesterday, a meal cannot be edited while its photo is still being analyzed (409), accuracy signals are tagged by source and edit kind, and a fraction next to the user's own number ("half, 250 cal lang") keeps that number (migration `20260912_0070`: `meal_accuracy_signals.source/edit_kind`, `meals.user_edited_at`).
    - **Target math is honest and consent-only.** Target math is honest and consent-only: protein now comes from a healthy reference weight (the lower of your weight and your BMI-27 weight, times your goal rate) capped (founder decision) at 35% of the day's calories, with a one-time, deduped note for users whose stored protein is above the new rule, and a weigh-in never changes targets without the "Update my targets?" confirmation. That applies to Calendar weigh-ins and weights told to the coach alike: the card shows old -> new first, a typed "yes" commits only the card the user actually saw, and FREE users get a direct preview-then-save path from the weight tile, Settings and Calendar. Target replies show the effective daily adjustment when the calorie floor binds (TDEE -500 = 920 -> 1,200 floor, so the real cut is 220 kcal a day), onboarding and Settings check body metrics for plausibility (a likely pounds-for-kg entry asks to confirm instead of saving silently, with cm/ft-in and kg/lb entry), and the weight projection passes quality gates before it projects (early days, holding steady, at goal). Migration `20260912_0069` adds `daily_logs.protein_target_g` so past days keep their protein target; no stored target changes at deploy, and affected users see the new protein only after their next explicit target save.
    
    ### Tests
    - Gate on the integrated branch: 7,086 API unit tests, 183 real-PostgreSQL integration tests (sequence corpus included), 126 offline accuracy-gate tests (117 fixtures, floor raised), ruff, web lint/type green. Integration notes: after the merge, the add-to-meal resolver returned WP14's pinned proposal before WP4 attached the anchor's "Math:" line, so a weighed add-on item lost its arithmetic; the pinned proposal now carries it. `_propose_action`'s per-kind binding (WP24 targets, weigh-in jump check, WP4 meal anchor) moves into one helper to stay under ruff's complexity cap (no behavior change); AGENTS_CORE.mdc gains the text-stated-weight-anchor invariant.
    
    - Launcher-shortcut spec waits for the photo sheet's deferred `history.back()` before reloading (a timing race that aborted the reload in CI; 8/8 locally).
    
    ### Operations
    - Deployed exact SHA `9cd216a` (this release plus v0.9.521) after PR #40 CI and main CI + Release Images were green. Alembic applied all six revisions in order; api and web healthy; `/readyz` 200; origin lock holds; live bundle bakes `9cd216a`; 0 API warnings/errors and 0 web errors in the post-deploy window; no new Sentry issues.
    
    ### Migrations
    - Two additive revisions: `20260912_0070` (`meal_accuracy_signals.source/edit_kind`, `meals.user_edited_at`) and `20260912_0069` (`daily_logs.protein_target_g` snapshot). No stored targets change at deploy.
  3. v0.9.521

    2026-09-16

    September enterprise-audit remediation, first release: 16 of 33 work packages from the 2026-09-12 audit (179 findings, every serious one adversarially verified; report: https://claude.ai/code/artifact/4491c841-9879-4859-86b3-a56c84a44a7f). Each package was built in its own worktree, independently reviewed (up to three rounds), and merged as one commit on `audit/accuracy-wave`. The nutrition-accuracy core — the vision edible-portion guard behind the whole-tilapia 78 g protein report (WP3), the typed-weight anchor (WP4), the deterministic fallback table (WP5), the meal-edit chokepoint (WP7) and target-math honesty (WP24) — is still closing its last review findings and ships in v0.9.522; this release lays its foundations (reference data, weight-basis memory, snap harness, estimate transparency).
    
    ### Security
    - **Photo uploads and paid AI quotas are hardened (public-launch gate).** Photo uploads can no longer exhaust server memory — only JPEG/PNG/WebP whose magic bytes match the declared type are decoded, per-format pixel caps are checked from the header before any decode (baseline JPEG 40 MP, progressive JPEG and PNG 16 MP, WebP 8 MP; worst accepted decode ~250 MB, a 1.6 KB "WebP bomb" that peaked at 689 MB is now refused at +3 MB), decoding runs off the event loop with a bounded per-worker queue (fast 503 + Retry-After), and a subprocess memory regression test pins it. Paid AI quotas (meal vision and photos, chat incl. stream, voice, body assessment, support chat, kickoff) plus a new 20/day share-card cap and the bug-report cap are reserved atomically under a Postgres advisory lock before any work, refunded on failure or cancellation, and per-IP limits cover every decode and paid-AI POST.
    - **Payments, auth tokens and privacy.** Subscription tier now follows the plan actually purchased (Lemon Squeezy variant / Stripe price), so buyer-echoed checkout data cannot upgrade it and an unknown variant keeps a subscriber's stored tier. Auth tokens, JWTs, email addresses and free text no longer reach Sentry, logs or PostHog (per-event property allow-list with a drift test; linear-time redaction), the service worker never caches private photos or signed URLs and sign-out clears the cache, account deletion now also erases bug-report screenshots and sweeps every per-user media prefix (best-effort, audited, never blocks deletion), and the dev impersonation header only works in development/test. A token-free sign-in link (`/auth/confirm`) ships behind `AUTH_EMAIL_TOKEN_HASH_LINK_ENABLED`, default off.
    
    ### Fixed
    - **One lifecycle for pending coach actions** on every path, including `/chat/stream`: staging a card retires every older open card (marker and ledger families) in the session that commits it, under one ordered lock, so a replaced card cannot come back. A typed "yes/sige/oo" confirms only the latest card within 10 minutes (otherwise the coach re-asks with the same card), correction/add cards carry a snapshot hash re-checked under an owner-filtered row lock, receipts say "committed" only after a real row change, model-written replies cannot carry live `[gfc-*]` markers, the agent's meal hint understands "last meal", "lunch/tanghalian" and "kape", and cards preview the meal and before -> after numbers (66-sequence real-Postgres corpus, zero violations).
    - **A dropped chat stream no longer duplicates a turn.** When the app retries a coach message after a dropped connection, attempts of the same send now wait for each other instead of running the coach twice, and a retry that finds the answer already saved returns it without spending extra daily chat. Each message gets one free re-run; further re-runs count like a new message. A retry that loses a race no longer cancels the meal card the other attempt saved, and a message re-sent after midnight or after its answered copy was deleted starts fresh instead of failing. The app also no longer mistakes a coach nudge for a "connection dropped" note, and slow sign-in refreshes now time out with Retry instead of hanging.
    - **Workouts are no longer invented from everyday Taglish:** one word-bounded English + Tagalog vocabulary, idioms ("running late", "the meeting ran 2 hours", "sports bar"), negation, plans, companion/spectator clauses ("hinatid ko si anak sa swimming") and traffic time are ignored, hours and spelled-out durations parse, past-day reports defer (hook left for the 2-day backfill), and anything ambiguous goes to the agent (371-message corpus: phantoms 27 -> 0, misses 150 -> 33 vs the old parser). Agent and manual workout log/edit/undo now go through `workout_commit` and keep the activity-ring contribution in sync without re-estimating device burns, and workout lists, burn sums and `/range` follow the local day.
    - **"kcal left" credits one net workout burn.** "kcal left" now counts only the workout burn your activity level's calorie target does not already include. The day's color uses the same number, so eating exactly to "0 left" can never turn a day red. Rings and step totals are counted once against your sessions, while a chat-reported session with its own kcal or step count stays a session. A tracker screen from another day, or a total-calories screen with no separate active number, is not added to today, and an agent edit never replaces a burn you stated yourself. `routes/meals.py` drops its now-unused `sqlalchemy.func` import: WP30 moved the meal-photo and rate-limit counts out of the module and WP22 replaced the workout-burn sum with `load_day_burn`, so ruff flagged F401 after the merge (no behavior change).
    - **Streaks, calendar and weigh-ins follow the local day.** A streak freeze now protects only a genuine single missed day between two active days (never pre-signup days, never an unfinished today, race-safe under a savepoint), the header shows an "at risk" streak instead of 0 the morning after a miss (with the evening recovery push), a 7 AM Manila weigh-in lands on today (`weigh_ins.local_date`, migration `20260912_0066`), days before signup show gray, a new user reads "2/2 logged days" instead of "2/11", and a skipped yesterday reads as missed. An audited, dry-run-by-default repair (`scripts/repair_streak_freezes.py`) cleans up wrongly spent freezes without removing XP; it has not been run against production.
    - **Today opens honestly and survives a reconnect.** Today no longer shows "0 kcal left" and then a fake "+1,450 kcal" count-up, chime and buzz every time Coach opens: a number-free placeholder header shows until today's real numbers exist (no hydration error, midnight-safe). Reconnecting after a dead zone no longer reloads the app (Serwist reloadOnOnline off), so a half-typed caption, an attached photo and a live walk or run survive; a new `gfc:online` signal lets Today, Progress, Calendar and Settings refetch failed reads in place, and the offline screen returns you to the page you were opening. the offline→online draft test now waits for the boot to paint before flipping; a boot still in flight when the network drops is healed as a real `open` (covered by its own test), which the heavier merged Today page exposed as a race.
    - **Fewer false "fitness only" refusals.** Ordinary fitness turns containing "new", "stock", "movie" or "relationship" are no longer refused, and coach replies like "Your new total is 1,450 kcal" or "Good news: you still have 400 kcal left" are no longer swapped for a toolless answer (a 78-phrase corpus went from 45 wrong to 2). Off-topic words are matched as whole raw words; food-sense collisions refuse only without food/training context, while coding, crypto, politics, news requests, stock-market and tax questions stay refused, and the URL-token regex is linear-time (12k-char CJK reply: 1.5 s -> 21 ms). "election"/"elections" join the hard off-topic terms, so the statement-form news idiom ("Big news: the election results are in.") cannot carry election news through the output check.
    - **Voice notes understand Taglish.** They are no longer transcribed as English. The backend picks the language hint from the profile (Taglish, auto and default profiles auto-detect) and sends a fixed Filipino food and workout vocabulary, with a guard against prompt echoes. Silence now returns a friendly "didn't catch any speech" (422) instead of an outage message, and each chat turn records whether it was typed, spoken, edited, tapped or scanned (`coach_messages.input_source`, migration `20260912_0065`), with a PII-free `voice_transcribed` event. the voice vocabulary prompt follows the new Filipino dish labels (Tagalog linkers, English cut names and rare inflections filtered) and stays inside its 200-token budget (estimated 195); the voice-route test reads the same skip set.
    
    ### Added
    - **Sourced nutrition reference and one quantity parser.** New sourced nutrition reference (`nutrition_reference.py`, 45 rows, per 100 g EDIBLE cooked food with edible yields, per-size edible grams and per-piece protein caps) so a whole fish or bone-in piece can no longer be priced as all flesh (a medium whole tilapia is ~25 g protein, capped at 38 g), plus one shared quantity parser (`food_quantity.py`: glued units, fractions, lb/oz/kg, Tagalog numbers/units, per-food quantity in multi-item messages; macro claims never read as portions). Filipino dish rows added (pritong/inihaw/paksiw na tilapia, sinigang na isda split from pork, paksiw/daing na bangus, galunggong, lechon manok, inasal, pork chop, silog meals); "lechon manok" no longer matches pork lechon. New invariant `2026-09-12-edible-portion-before-density`.
    - **"With bones or meat only?" is asked once and remembered.** A pure `weight_basis` module reads English and Tagalog/Taglish answers ("may buto", "laman lang", "walang tinik", "boneless") and refuses anything uncertain; `coach_memories` stores the answer per user and food class as a user-visible, deletable memory with supersede semantics (no migration). New invariant `2026-09-12-bone-in-weight-basis-ask-once`.
    - **Meal cards show how each estimate was made:** "You said" (the caption or chat message), "Assumed" (the portion the AI used, with More for the full math), per-item protein and kcal in the breakdown, a neutral confidence indicator instead of a reassuring green shield, and no generic praise as the only note. After a correction the card shows "You corrected: calories 570 → 250" and labels the old assumption as the first estimate. UI only; no new endpoint.
    - **Observability and release gates.** API logs are now one JSON object per line, keep every structured field, and are redacted the same way as Sentry; LOG_FORMAT=text keeps a readable, equally scrubbed line for local development. scripts/deploy.sh refuses any SHA whose CI (every job) or Release images run is not green and pins the droplet to that exact SHA; ALLOW_RED_CI=1 is a recorded emergency override that never changes a CI_GATE_ONLY verdict. docs/SLOS.md defines four 7-day SLOs measured by a read-only weekly scripts/slo_report.py, whose implausible-nutrition check counts only physically impossible meals, treats alcohol calories as real and never flags the app's own reference foods; CI also lints scripts/, runs pyright on core modules and walks every Alembic migration down and back up.
    
    ### Tests
    - **Snap persistence is tested end to end.** `analyze_meal_now` (every photo snap's path to stored numbers) had 0% unit coverage and was pinned only by source-text asserts; a new `tests/snap_harness.py` runs it with a fake vision client and asserts stored macros, components, status, header deltas, daily totals and the snap message, plus a real-Postgres lane. Known open defects are pinned as strict xfails for later packages, and `scripts/coverage_floors.py` adds ratcheting per-module coverage floors to CI. Release gate on the integrated branch: 5,006 API unit tests, 96 real-PostgreSQL integration tests (including the coach sequence corpus), 465 Playwright tests, ruff, web lint/type/build, design-token, handler and secret audits all green.
    
    ### Migrations
    - Four additive, online-safe Alembic revisions in one linear chain: `20260912_0065` (`coach_messages.input_source`), `20260912_0066` (`weigh_ins.local_date` + deterministic backfill), `20260912_0068` (`workouts.is_daily_total` / `burn_source` + bounded backfills), `20260912_0064` (`coach_messages.client_turn_id` + partial unique index).
  4. v0.9.520

    2026-09-05

    Sentry triage: GOODFITCOACH-WEB-B closed at the root, not at the filter.
    
    ### Fixed
    - **The app now owns service-worker registration** (`register: false` in
      `next.config.ts` + `lib/service-worker-registration.ts` mounted via
      `<ServiceWorkerRegistrar />`). GOODFITCOACH-WEB-B was
      `TypeError: Cannot read properties of undefined (reading 'waiting')` with
      `function: o.register` on `/`. Verified chain in the installed dependency:
      `@serwist/next/dist/sw-entry.mjs:9` calls `window.serwist.register()` with
      **no `.catch()`**, and `@serwist/window`'s `register()` reads
      `this._registration.waiting` five lines after assigning it — unguarded.
      `_registerScript()` rethrows real failures, so `_registration` is
      `undefined` only when `navigator.serviceWorker.register()` RESOLVES with
      `undefined`, which is what stubbed/no-op service workers do in in-app
      webviews, privacy extensions, and locked-down enterprise browsers.
      The floating call makes this a CLASS, not one bug: private mode, an
      insecure origin, a 404 `sw.js` after a bad deploy, or navigating away
      mid-register all became unhandled rejections in Sentry the same way. Our
      registration is guarded (support + secure-context + dev), awaited, and
      caught, so every one of those degrades to "no service worker" — which
      only costs the offline shell — instead of paging.
      Deliberately NOT fixed by adding a scrubber pattern: the existing
      WEB-2/WEB-3 SW filters exist for failures we cannot control, and muting a
      bug we can delete would have hidden the whole class.
      Nothing regressed: no app code reads `window.serwist`, the SW uses
      `skipWaiting`/`clientsClaim` (no update prompt depends on serwist's
      lifecycle events), and `cacheOnNavigation`/`reloadOnOnline` are wired by
      `sw-entry` independently of the flag. `service-worker.spec.ts` still
      proves a real controller, precache, and the API-privacy guarantee.
    
    ### Added
    - `tests/service-worker-registration.spec.ts` — pins the never-throws
      contract against each hostile environment (stubbed `register` resolving
      undefined, a rejecting `register`, a successful one, and a missing
      diagnostic callback), plus the precondition matrix. The module takes an
      injected-dependency seam because `navigator` is getter-only in the test
      runner.
  5. v0.9.519

    2026-08-28

    Production-evidence coach accuracy pass: a frequent-food repeat now means
    “repeat this accepted stored item,” explicit pending nutrition corrections are
    heard and remembered, and old confirmation state cannot revive after a newer
    action wins.
    
    ### Fixed
    
    - **Frequent-food chips preserve the nutrition they display.** The endpoint
      now returns the most recent qualifying source Meal ID for each canonical
      food. `/today` sends that ID as `repeat_meal_id`; the backend verifies user
      ownership, 60-day age, complete analysis state, non-carrier food identity,
      and then copies the stored calories/protein/carbs/fat into the existing
      propose→confirm path. The old title-only path re-estimated a user-accepted
      107-kcal branded coffee as 250 kcal even though its chip visibly said 107.
      Foreign, missing, incomplete, expired, and menu/workout carrier sources fail
      closed instead of falling back to a guess. The frequent list itself now
      excludes incomplete and non-food carrier rows.
    - **Explicit pending-card macro corrections bypass model reinterpretation.**
      “107 kcal only” and labeled protein/carbs/fat values are recognized as
      revisions, deterministically replace only the stated numbers, collapse the
      calorie range to the accepted point value, and retain unstated macros. Data
      questions remain read-only. When the revised proposal is confirmed, its
      exact user utterance flows through `remember_nutrition_correction` under a
      SAVEPOINT, preserving the user-visible/deletable, exact-title, named-fields-
      only memory contract.
    - **One pending action now owns confirmation across both storage systems.** A
      native-agent ledger proposal transactionally retires loaded marker-backed
      proposals; a new marker proposal discards older open ledger rows. A
      source-bound repeat also retires every stale loaded family (including an
      unrelated clarification) before routing. This closes the production chain
      where a newer 107-kcal proposal committed, then an older 250-kcal marker card
      resurfaced and falsely claimed success while database totals stayed put.
    
    ### Operations
    
    - Ran `python scripts/fetch_sentry_errors.py --limit 5`: both Sentry projects
      are clean with zero unresolved issues, so no issue was falsely resolved. The
      five reported GitHub failures are old Aug 20 SHAs from the already-documented
      TWA-v4 red streak, not failures on current main.
    - Narrow read-only production queries confirmed the reported account's Aug 27
      and Aug 28 daily-log calories/protein exactly match sums from stored meals;
      the latest accepted coffee before the failed repeat was 107 kcal. No live
      data mutation was needed or performed.
    - Deployed exact SHA `1768ce8` after CI run `33158354258` passed all four jobs
      (including 19 real-PostgreSQL integrations) and Release Images run
      `33158354381` published both immutable images with exact-image SBOM and
      provenance. Alembic completed; API and web are healthy on `:1768ce8`;
      `/readyz` returned 200 on the first probe; the Cloudflare-only origin check
      passed; the post-deploy API/web log window had no errors; and a fresh Sentry
      sweep still found zero unresolved issues.
    
    ### Tests
    
    - Added source-row repeat, missing-source fail-closed, latest frequent-source,
      explicit macro revision, durable-memory handoff, marker-codec, and
      cross-storage supersession regressions. Full gate: 316 Playwright tests and
      3,041 API unit tests passed; 19 real-PostgreSQL integrations skipped locally
      without `TEST_DATABASE_URL` and remain required in CI. Ruff, web lint/type/
      production build, secret scan, invariant and handler audits all passed.
      Regenerated and audited the v0.9.519 Play assets; version-ratcheted the
      canonical physical-device and manual-gate ledgers while leaving all external
      evidence truthfully DEVICE_REQUIRED/OPEN. No schema, migration, or dependency
      change.
Changelog — GoodFitCoach AI - GoodFitCoach AI