diff --git a/.prettierignore b/.prettierignore index 95cabd0..09bc019 100644 --- a/.prettierignore +++ b/.prettierignore @@ -19,5 +19,8 @@ skills-lock.json # SpecSwarm planning artifacts (generated by /ss:* commands, not hand-edited source) /.specswarm/ +# Impeccable design-hook cache (generated, gitignored) +/.impeccable/ + # GitHub Actions workflows (must stay byte-for-byte identical to main for claude-code-action validation) /.github/ diff --git a/.specswarm/features/006-today-logging/checklists/requirements.md b/.specswarm/features/006-today-logging/checklists/requirements.md new file mode 100644 index 0000000..8957f57 --- /dev/null +++ b/.specswarm/features/006-today-logging/checklists/requirements.md @@ -0,0 +1,37 @@ +# Specification Quality Checklist: Today dashboard, one-tap meal logging & meal verdicts + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-07-09 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) — seam names cited only as existing contract references +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain — all 5 resolved in clarification session 2026-07-09 +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Clarification session 2026-07-09 resolved: verdict enum shape (mockups supersede the + 4-value MealRating), append-only vs. late verdicts (annotation window), five log types + vs. two-invariant coverage (insert-time validation), undo (deferred, A-004), and the + nudge banner's independence from the reminders backend. Spec is ready for planning. diff --git a/.specswarm/features/006-today-logging/contracts/logService.md b/.specswarm/features/006-today-logging/contracts/logService.md new file mode 100644 index 0000000..04ab736 --- /dev/null +++ b/.specswarm/features/006-today-logging/contracts/logService.md @@ -0,0 +1,72 @@ +# Contract: log module + +PostgREST tables touched: `meal_logs` (new, INSERT/SELECT/UPDATE), `planned_meals` +(UPDATE status safe-flip), `portion_events` (INSERT via existing pantry consume path), +`prepped_meals` (SELECT via existing store). + +```ts +// src/lib/log/logService.ts — stateless; every fn awaits requireSession(); errors +// rethrown as LogError with calm message; no owner filters (RLS authoritative). + +/** Insert one meal log from a draft. Returns the persisted, mapped row. */ +export async function createLog(draft: MealLogDraft, loggedAt?: string): Promise; + +/** Set or clear the verdict on an existing log (annotation window, R2). */ +export async function setVerdict(logId: string, verdict: MealVerdict | null): Promise; + +/** Logs in [fromISO, toISO) for the current user, newest first. */ +export async function fetchLogsBetween(fromISO: string, toISO: string): Promise; + +/** Latest `limit` non-QUICK_LOG logs (for recents/keepers grouping). */ +export async function fetchRecentLogs(limit?: number): Promise; // default 100 + +/** Safe-flip: PLANNED → LOGGED with logged_at; returns false if already flipped. */ +export async function markPlannedMealLogged(plannedMealId: string, loggedAt: string): Promise; +``` + +```ts +// src/lib/log/logStore.svelte.ts — getter-function store (planStore pattern). +export function todayLogs(): MealLog[]; +export function recentSources(): LoggedSource[]; +export function logLoading(): boolean; +export function logError(): string | null; +export function logNotice(): string | null; // calm partial-failure notice (FR-TL-014) + +export async function loadToday(now?: Date): Promise; +export async function loadRecents(): Promise; + +/** + * Orchestrated one-tap log (R5): optimistic append → createLog → + * FROM_PREPPED: optimisticConsumePortions(preppedMealId, servings, { triggeredBy: log.id }) + * FROM_PLAN: markPlannedMealLogged(plannedMealId, log.loggedAt) + * Insert failure → rollback optimistic row + logError. + * Follow-up failure → keep log + logNotice, never throws. + */ +export async function logMeal(draft: MealLogDraft): Promise; + +/** Optimistic verdict set/clear on a log (recap card + sheet footer). */ +export async function rateLog(logId: string, verdict: MealVerdict | null): Promise; +``` + +```ts +// src/lib/pantry — additive, backwards-compatible change: +export async function consumePortions( + preppedMealId: string, + count: number, + opts?: { triggeredBy?: string } // meal_logs.id → portion_events.triggered_by +): Promise; +// optimisticConsumePortions gains the same optional opts pass-through. +``` + +## Behavioral guarantees + +| Guarantee | Backstop | +|-----------|----------| +| A log's occurrence fields never change after insert | DB trigger `meal_logs_annotation_only` (pgTAP both ways) | +| No log can be deleted (v1) | No DELETE policy + no DELETE grant (pgTAP) | +| FROM_PLAN/FROM_PREPPED/FROM_RECIPE always born with their reference; non-quick logs always born named | DB trigger `meal_logs_validate_source` (pgTAP) | +| A log can never reference another user's plan/recipe/prepped meal | SECURITY DEFINER ownership trigger (pgTAP cross-user) | +| Prepped consumption can't go negative and never writes the count directly | existing `sync_portions_remaining` trigger (pgTAP in 0004's suite) | +| Planned meal flips PLANNED→LOGGED at most once | safe-flip UPDATE predicate returns 0 rows on repeat (unit test) | +| One rapid double-tap ⇒ one log | optimistic in-flight guard on the card button (e2e) | +| Users see only their own logs | RLS owner policies (pgTAP both directions) | diff --git a/.specswarm/features/006-today-logging/data-model.md b/.specswarm/features/006-today-logging/data-model.md new file mode 100644 index 0000000..f08170f --- /dev/null +++ b/.specswarm/features/006-today-logging/data-model.md @@ -0,0 +1,167 @@ +# Data model: Today dashboard, one-tap meal logging & meal verdicts + +## Enums + +```sql +CREATE TYPE meal_log_type AS ENUM ('FROM_PLAN', 'FROM_RECIPE', 'FROM_PREPPED', 'QUICK_LOG', 'CUSTOM'); +CREATE TYPE meal_verdict AS ENUM ('KEEP', 'FINE', 'REST'); +``` + +`meal_slot` is reused from migration 0005 (`BREAKFAST | LUNCH | DINNER | SNACK`). + +## Table: meal_logs + +| Column | Type | Constraints | +|--------|------|-------------| +| id | uuid | PK, `gen_random_uuid()` | +| owner_id | uuid | NOT NULL, DEFAULT `auth.uid()`, FK `auth.users` ON DELETE CASCADE | +| household_id | uuid | NULL (no FK until Household feature) | +| log_type | meal_log_type | NOT NULL | +| planned_meal_id | uuid | NULL, FK `planned_meals` ON DELETE SET NULL | +| recipe_id | uuid | NULL, FK `recipes` ON DELETE SET NULL | +| prepped_meal_id | uuid | NULL, FK `prepped_meals` ON DELETE SET NULL | +| name_snapshot | text | NULL; CHECK 1..200 chars when present | +| meal_slot | meal_slot | NULL (unspecified allowed) | +| servings | numeric(6,2) | NOT NULL DEFAULT 1, CHECK `servings > 0` | +| logged_at | timestamptz | NOT NULL DEFAULT `now()` | +| verdict | meal_verdict | NULL | +| notes | text | NULL, CHECK ≤ 1000 chars (domain: 0..1000) | +| created_at | timestamptz | NOT NULL DEFAULT `now()` | +| updated_at | timestamptz | NOT NULL DEFAULT `now()` (shared `set_updated_at()` trigger) | + +**CHECKs (row-local):** + +- `meal_logs_servings_positive` — `servings > 0` (domain: PositiveDecimal). +- `meal_logs_name_snapshot_len` — `name_snapshot IS NULL OR char_length(name_snapshot) BETWEEN 1 AND 200`. +- `meal_logs_notes_len` — `notes IS NULL OR char_length(notes) <= 1000`. + +**Triggers (cross-row / lifecycle — not expressible as CHECKs):** + +- `meal_logs_validate_source` BEFORE INSERT — INV-XD-001/002 + spec R3: + `FROM_PLAN ⇒ planned_meal_id NOT NULL`; `FROM_PREPPED ⇒ prepped_meal_id NOT NULL`; + `FROM_RECIPE ⇒ recipe_id NOT NULL`; `CUSTOM ⇒ name_snapshot NOT NULL`; + additionally `log_type <> 'QUICK_LOG' ⇒ name_snapshot NOT NULL`. + Insert-time only, because source FKs are ON DELETE SET NULL (history durability). +- `meal_logs_check_source_ownership` BEFORE INSERT, SECURITY DEFINER — rejects references + to another user's `planned_meals` (via parent `meal_plans.owner_id`), `recipes`, or + `prepped_meals` (pattern: 0007 `check_source_planned_meal_ownership`). Blocks + cross-user probing that RLS on this table alone cannot. +- `meal_logs_annotation_only` BEFORE UPDATE — INV-CC-004 / REQ-CN-007: compares OLD/NEW + on every occurrence column explicitly (id, owner, type, refs, snapshot, slot, servings, + logged_at, created_at) and raises if any differ; only `verdict`, `notes`, and + `updated_at` may change. Carve-out: the three source references may transition + value → NULL (never to a different row) — that is what the FKs' ON DELETE SET NULL + issues when a source is deleted, and blocking it would make logged sources + undeletable. +- `trg_meal_logs_updated_at` BEFORE UPDATE — shared `set_updated_at()`. + +**Indexes:** + +- `meal_logs_owner_logged_idx` on `(owner_id, logged_at DESC)` — today window + recents. +- `meal_logs_planned_meal_idx` on `(planned_meal_id)` WHERE NOT NULL. +- `meal_logs_recipe_idx` on `(recipe_id)` WHERE NOT NULL. +- `meal_logs_prepped_idx` on `(prepped_meal_id)` WHERE NOT NULL. + +**RLS (P7, default-deny):** enabled in the same migration. +`meal_logs_owner_select` FOR SELECT USING owner; `meal_logs_owner_insert` FOR INSERT +WITH CHECK owner; `meal_logs_owner_update` FOR UPDATE USING/WITH CHECK owner. +**No DELETE policy and no DELETE grant** (append-only, A-004). + +**Grants (post-0008 world, required inline):** + +```sql +GRANT SELECT, INSERT, UPDATE ON meal_logs TO authenticated; +GRANT ALL ON meal_logs TO service_role; +GRANT SELECT ON meal_logs TO anon; +ALTER PUBLICATION supabase_realtime ADD TABLE meal_logs; +``` + +## Existing tables touched (no schema change) + +- `planned_meals` — first writer of `status = 'LOGGED'`, `logged_at` (safe-flip: + `UPDATE … WHERE id = ? AND status = 'PLANNED'`, INV-PL-005 CHECK already shipped). +- `portion_events` — CONSUMED events now populate `triggered_by` with the meal-log id + (column reserved for this in 0004). + +## State machines + +MealLog has none (append + annotate). PlannedMeal `PLANNED → LOGGED` transition is +existing (0005); this feature merely triggers it. + +## Application types (src/lib/log/types.ts) + +```ts +export type MealLogType = Enums<'meal_log_type'>; // 'FROM_PLAN' | 'FROM_RECIPE' | 'FROM_PREPPED' | 'QUICK_LOG' | 'CUSTOM' +export type MealVerdict = Enums<'meal_verdict'>; // 'KEEP' | 'FINE' | 'REST' +export type MealSlot = Enums<'meal_slot'>; + +export interface MealLog { + id: string; + ownerId: string; + logType: MealLogType; + plannedMealId: string | null; + recipeId: string | null; + preppedMealId: string | null; + nameSnapshot: string | null; + mealSlot: MealSlot | null; + servings: number; // Number(row.servings) + loggedAt: string; // ISO + verdict: MealVerdict | null; + notes: string | null; + createdAt: string; + updatedAt: string; +} + +/** Exactly-one-source drafts — invalid combinations unrepresentable. */ +export type MealLogDraft = + | { kind: 'fromPlan'; plannedMeal: PlannedMeal } // snapshot + slot + servings derived + | { kind: 'fromPrepped'; preppedMeal: PreppedMeal; slot: MealSlot | null; servings?: number } + | { kind: 'fromRecipe'; recipeId: string; name: string; slot: MealSlot | null } + | { kind: 'quick'; slot: MealSlot | null } // "I ate something" + | { kind: 'custom'; name: string; slot: MealSlot | null }; +``` + +## Derived/pure interfaces (unit-testable, no I/O) + +```ts +/** R6 — slot inference + nudge derivation (pure). */ +export function slotForTime(now: Date): MealSlot; +export interface NudgeState { slot: MealSlot; plannedMealId: string; name: string } +export function deriveNudge( + now: Date, + todaysMeals: PlannedMeal[], + todaysLogs: MealLog[], + dismissedSlots: MealSlot[] +): NudgeState | null; + +/** R7 — grouping over recent logs (pure). */ +export interface LoggedSource { + key: string; // recipe: | prepped: | name: + name: string; + recipeId: string | null; + latestVerdict: MealVerdict | null; + timesMade: number; + lastLoggedAt: string; + lastSlot: MealSlot | null; +} +export function groupLogSources(logs: MealLog[]): LoggedSource[]; +export function keepers(sources: LoggedSource[]): LoggedSource[]; // latestVerdict === 'KEEP' +export function recents(sources: LoggedSource[], limit: number): LoggedSource[]; + +/** Recap: unrated, recent, not QUICK_LOG (pure). */ +export function unratedRecent(logs: MealLog[], now: Date, limit: number): MealLog[]; + +/** Coverage: per-slot rollup of fulfillment + logged state (pure; consumes feature 004 output). */ +export interface SlotCoverage { slot: MealSlot | null; label: string; state: 'EATEN' | 'HAVE_IT' | 'CAN_MAKE_IT' | 'MUST_ACQUIRE' } +export function deriveDayCoverage(meals: PlannedMealWithFulfillment[], logs: MealLog[]): { + slots: SlotCoverage[]; + headline: string; // calm summary sentence + detail: string; +} +``` + +Invariant placement summary: servings/name/notes bounds → DB CHECKs; type↔ref presence, +ownership, annotation-only mutability → DB triggers (insert/update-time); portion +arithmetic → existing ledger trigger; PLANNED→LOGGED single-flip → safe-flip UPDATE +predicate (+ existing CHECK `status='LOGGED' ⇒ logged_at NOT NULL`); everything +display-time (coverage, keepers, nudges) → pure client functions. diff --git a/.specswarm/features/006-today-logging/plan.md b/.specswarm/features/006-today-logging/plan.md new file mode 100644 index 0000000..d8ee252 --- /dev/null +++ b/.specswarm/features/006-today-logging/plan.md @@ -0,0 +1,144 @@ +# Implementation Plan: Today dashboard, one-tap meal logging & meal verdicts + +**Feature**: 006 | **Branch**: `feat/006-today-logging` +**Spec**: [spec.md](./spec.md) | **Created**: 2026-07-09 + +## Summary + +Add `meal_logs` (migration 0009) with append-only-plus-annotation semantics; a `log` +domain module (types, service, runes store, pure derivations for slots/nudges/ +recents/keepers/coverage); the `/today` route composed per the canonical mockups; a +Quick Log sheet; and wiring into existing seams — feature 004 fulfillment for coverage, +the portion ledger for prepped consumption, and the dormant `PLANNED → LOGGED` +transition from feature 003. + +## Technical Context + +- **Language/Framework**: TypeScript, SvelteKit SPA (adapter-static), Svelte 5 runes. +- **Data access**: supabase-js via `$lib/supabaseClient`; RLS-authoritative; no owner + filters in queries. +- **State**: module-level `$state` stores with getter functions + optimistic writes with + rollback (planStore pattern). +- **Styling**: design tokens + `.nk-*` primitives; `.nk-verdict` already shipped in + `base.css`; Tailwind-with-token utility classes as used by planning/shopping + components (heed the `[font-weight:var(--weight-*)]` gotcha). +- **Testing**: vitest server project for pure logic + service specs (thenable + `makeChain` supabase mock); pgTAP for RLS/invariants; Playwright e2e with the + plan.e2e.ts stateful mock backend extended for `meal_logs`. +- No new dependencies. + +## Tech Stack Compliance Report + +### ✅ Approved Technologies (already in stack) + +SvelteKit, Svelte 5 runes, supabase-js, Tailwind (token-bound), vitest, Playwright, +pgTAP, Phosphor icons. + +### ➕ New Technologies (auto-added) + +None. + +### ⚠️ Conflicting / ❌ Prohibited + +None. + +## Constitution Check + +| Principle | Status | Notes | +|-----------|--------|-------| +| P1/P2 SPA only, no server files | ✅ | `/today/+page.svelte` only; all logic client-side | +| P3 Runes mode | ✅ | `logStore.svelte.ts` uses `$state` + getter functions | +| P4 Design tokens | ✅ | Today components use `var(--token)` / `.nk-*`; verdict UI is the shipped `.nk-verdict` primitive | +| P5 No unsanitized HTML | ✅ | No `{@html}` | +| P6 Service-role key server-only | ✅ | anon key + RLS only; ownership-guard trigger is SECURITY DEFINER in the DB, not a client secret | +| P7 RLS default-deny before exposure | ✅ | 0009 enables RLS + policies in the same migration that creates `meal_logs` | +| P8/P9 UUID PKs, timestamptz UTC | ✅ | Complies; shared `set_updated_at()` reused | +| P13 No clinical labels | ✅ | "Log it", "I ate something", "Not for me" — calm copy straight from mockups | +| P14 Append-only records | ✅ | No DELETE policy/grant; `meal_logs_annotation_only` trigger restricts UPDATE to verdict/notes; portion changes via ledger events only | +| P15 Online-first, server authoritative | ✅ | Optimistic log with rollback on insert failure; follow-up writes sequential with per-step calm failure report (research R5) | +| INV-XD-001/002/003 | ✅ | R3 insert trigger; R4 ledger reuse with `triggered_by` link | +| INV-PL-005 | ✅ | Safe-flip UPDATE predicate; CHECK already in 0005 | +| INV-CC-004 / REQ-CN-007 | ✅ | R2 annotation-only trigger + no DELETE | + +**Gate result: PASS** — no violations, no justifications needed. + +## Design Decisions (rationale in research.md) + +- D1 (R1): three-way `meal_verdict` on the log row; per-source marks derived. +- D2 (R2): append-only + verdict/notes annotation window via BEFORE UPDATE trigger. +- D3 (R3): type↔reference validation and ownership guard as insert triggers; FKs SET NULL. +- D4 (R4): prepped consumption through existing `consumePortions` + new `triggeredBy` arg. +- D5 (R5): log-first orchestration; follow-up failures reported, never rolled back. +- D6 (R6): client-local day bounds, fixed slot windows, pure `deriveNudge`, localStorage dismissals. +- D7 (R7): recents/keepers from client-side grouping of last 100 non-quick logs. +- D8 (R8): coverage card reuses feature 004 fulfillment derivation verbatim. +- D9 (R9): Quick Log sheet implements Escape/backdrop/focus-restore/`inert` properly. + +## Phases & File Map + +### Phase A — Schema & types + +| File | Change | +|------|--------| +| supabase/migrations/0009_meal_logs.sql | enums, table, CHECKs, triggers, RLS, grants, realtime, indexes | +| src/lib/database.types.ts | regenerate (`bun run db:types`) | +| supabase/tests/meal_logs_rls.test.sql | pgTAP: RLS both directions, append-only, source validation, ownership guard, ledger link | + +### Phase B — Domain module + +| File | Change | +|------|--------| +| src/lib/log/types.ts | aliases, `MealLog`, `MealLogDraft`, `toMealLog` | +| src/lib/log/slots.ts | `slotForTime`, `deriveNudge`, day-bounds helpers (pure) | +| src/lib/log/derive.ts | `groupLogSources`, `keepers`, `recents`, `unratedRecent`, `deriveDayCoverage` (pure) | +| src/lib/log/logService.ts | `createLog(draft)`, `setVerdict`, `fetchLogsBetween`, `fetchRecentLogs` | +| src/lib/log/logStore.svelte.ts | today logs + recents state; `logMeal(draft)` orchestration (optimistic, R5), `rateLog` | +| src/lib/log/index.ts | curated exports | +| src/lib/pantry/preppedMealService.ts / preppedMealStore.svelte.ts | optional `triggeredBy` param threaded to CONSUMED event | +| colocated *.spec.ts | server-project unit tests for all of the above | + +### Phase C — Today UI + +| File | Change | +|------|--------| +| src/lib/components/today/TodayDashboard.svelte | shell: greeting, coverage, nudge, meals, quick actions, recap, attention | +| src/lib/components/today/TodayMealCard.svelte | slot card: status/action per FR-TL-003/004 | +| src/lib/components/today/DayCoverageCard.svelte | coverage chips + headline | +| src/lib/components/today/NudgeBanner.svelte | gentle banner, Not now | +| src/lib/components/today/QuickLogSheet.svelte | bottom sheet per mobile-log.html (D9) | +| src/lib/components/today/RecapCard.svelte | "How were these?" | +| src/lib/components/today/AttentionList.svelte | expiring prepped items | +| src/lib/components/shared/VerdictControl.svelte | wraps `.nk-verdict` (aria-pressed, deselect) | +| src/routes/today/+page.svelte | thin shell | +| src/routes/+page.svelte | redirect target `/plan` → `/today` | +| src/routes/+layout.svelte | Today nav item gets `href` | + +### Phase D — e2e & verification + +| File | Change | +|------|--------| +| tests/today.e2e.ts | mock-backend flow: view today, one-tap log, prepped decrement, quick log, verdict | +| .specswarm/quality-analysis-006-today-logging.md | end-of-feature analysis | + +## Risks + +- **Trigger strictness vs. PostgREST updates** — supabase-js UPDATE sends only changed + columns, but PostgREST applies them to the full row; the annotation-only trigger must + compare OLD/NEW values, not "columns present in payload". Mitigated by pgTAP tests + updating verdict alone (pass) and servings (fail). +- **Cross-module store coupling** (log store → pantry store → plan store) — keep + orchestration one-directional in `logMeal`; stores never import each other circularly + (log → pantry/plan only). +- **Browser-project component tests hang in agent sandboxes** — pure logic goes in + `slots.ts`/`derive.ts` (server project); component behaviour covered by e2e + manual + quickstart. +- **Clock-dependent logic** — all time-dependent functions take `now: Date` explicitly; + no `Date.now()` inside derivations. + +## Progress Tracking + +- [x] Phase 0: research.md +- [x] Phase 1: data-model.md + contracts + quickstart +- [x] Phase 2: tasks.md +- [x] Implementation +- [x] Verification (automated gates + adversarial reviews; manual quickstart T026 pending) diff --git a/.specswarm/features/006-today-logging/quickstart.md b/.specswarm/features/006-today-logging/quickstart.md new file mode 100644 index 0000000..869d6b9 --- /dev/null +++ b/.specswarm/features/006-today-logging/quickstart.md @@ -0,0 +1,41 @@ +# Quickstart: Today dashboard & meal logging + +## Migrate + types + +```bash +bunx supabase start -x edge-runtime # local stack (563xx ports) +bunx supabase migration up # applies 0009_meal_logs.sql +bun run db:types # regen src/lib/database.types.ts (CI checks drift) +``` + +## Automated tests + +```bash +bun run check +bun run lint +bun run test:unit -- --run +bunx supabase test db # pgTAP incl. meal_logs_rls.test.sql +bunx playwright test tests/today.e2e.ts +``` + +## Manual end-to-end loop (human) + +1. `bun run dev`, open the app — root should land on **/today** with greeting + date; + "Today" nav item active. +2. Seed context: create a recipe; add a prepped meal (2 portions); plan today's lunch + from the prepped meal and dinner from the recipe. +3. Today shows coverage chips (lunch have-it, dinner per pantry) and two meal cards. +4. Tap **Log it** on lunch → instant "Eaten · time", prepped portions show 1 left in + pantry, verdict prompt appears inline. Tap **Again** → Keeper mark. +5. Planner (/plan) shows lunch as logged. +6. **Log a meal** → sheet: slot defaults sensibly; tap **I ate something** → logged, + sheet closes with calm confirmation. +7. Reopen sheet: "Recent" shows the logged items; the kept meal appears under Keepers + with a times-made count. +8. Next day (or adjust clock): recap card asks about unrated dinner; verdict from the + recap card sticks. +9. Nudge banner: within a slot window with an unlogged planned meal, banner appears; + **Not now** hides it for the day (survives reload — localStorage). +10. Responsive pass: mobile (bottom sheet, FAB), desktop (4-col meal grid) per mockups; + keyboard: sheet closes on Escape, focus returns to the opener; verdict buttons are + 44 px and announce pressed state. diff --git a/.specswarm/features/006-today-logging/research.md b/.specswarm/features/006-today-logging/research.md new file mode 100644 index 0000000..57f0ae6 --- /dev/null +++ b/.specswarm/features/006-today-logging/research.md @@ -0,0 +1,92 @@ +# Research: Today dashboard, one-tap meal logging & meal verdicts + +## R1 — Verdict shape: three-way enum on the log row + +**Decision:** `meal_verdict AS ENUM ('KEEP','FINE','REST')` as a nullable column on +`meal_logs`; no separate verdict table, no denormalised copy on recipes. +Canonical mockups + shipped `.nk-verdict` primitives use the three-way control; the +four-value `MealRating` in the domain spec is the superseded earlier design (recorded in +spec Clarifications). A verdict belongs to an eating occurrence (you can love a recipe on +Tuesday and set it aside after the fourth reheat); per-source marks (Keeper) are the +*latest* verdict per source, derived at query time. Rejected: separate `meal_verdicts` +append-only table (over-modelling — one mutable column with a trigger guard preserves +append-only occurrence facts with far less machinery); reuse of `user_recipe_meta.rating` +(different concept: deliberate 1–5 recipe review vs. instant meal verdict; also logs are +frequently not recipe-backed). + +## R2 — Append-only with annotation window + +**Decision:** RLS grants INSERT/SELECT/UPDATE (owner-scoped), **no DELETE policy and no +DELETE grant**; a `BEFORE UPDATE` trigger raises unless the only changed columns are +`verdict` and/or `notes`. This satisfies INV-CC-004/REQ-CN-007 (occurrence facts can +never be overwritten) while honouring the design's "set a verdict anytime later". +Rejected: fully immutable rows + verdict side-table (R1); app-layer-only enforcement +(defence in depth is cheap here and pgTAP-testable). + +## R3 — Type↔reference validation via trigger, not CHECK + +**Decision:** enforce INV-XD-001/002 (`FROM_PLAN ⇒ planned_meal_id`, `FROM_PREPPED ⇒ +prepped_meal_id`, plus `FROM_RECIPE ⇒ recipe_id`, `CUSTOM ⇒ name_snapshot`) in a +`BEFORE INSERT` trigger. Source FKs are `ON DELETE SET NULL` for durability of history +(name snapshot preserves display), so a table CHECK would make deleting a source row +fail its cascading UPDATE. Insert-time validation is exactly what the invariants mean. +Cross-user probing is blocked by a SECURITY DEFINER ownership-check trigger (pattern: +`check_source_planned_meal_ownership`, migration 0007); `planned_meals` ownership is +checked via its parent `meal_plans`. + +## R4 — Prepped consumption reuses the ledger, linked by `triggered_by` + +**Decision:** logging FROM_PREPPED calls the existing consume path +(`consumePortions` / `optimisticConsumePortions`), extended with an optional +`triggeredBy` parameter so the CONSUMED event carries the meal-log id — the exact use +reserved in migration 0004's comment. The DB trigger `sync_portions_remaining` stays the +single writer of `portions_remaining` and raises on negative balances (INV-INV-004). +Rejected: DB AFTER-INSERT trigger on `meal_logs` creating the portion event (hides a +cross-aggregate write, complicates optimistic UI reconciliation, and INV-CON-003 +explicitly allows client-logic orchestration). + +## R5 — Multi-write orchestration: log first, follow-ups reported + +**Decision:** insert the `meal_logs` row first (the user's truth), then run follow-ups +sequentially: CONSUMED portion event (FROM_PREPPED) or planned-meal safe-flip +(FROM_PLAN, `UPDATE … WHERE id = ? AND status = 'PLANNED'`). Follow-up failures keep the +log, surface a calm notice, and are reported per step — the established +`completeTrip` pattern from feature 005. Rejected: transactional RPC (needs a Postgres +function + loses optimistic granularity; the failure mode is benign and self-describing). + +## R6 — "Today", slots, and nudge windows are client-local + +**Decision:** "today" = the device's local calendar day; the service takes explicit +`[dayStartISO, dayEndISO)` bounds computed client-side (same approach as planning's week +math). Slot windows are fixed constants (breakfast 06:00–10:00, lunch 11:00–14:00, +dinner 17:00–21:00, otherwise snack) used for both the Quick Log default slot and the +nudge banner; `deriveNudge(now, plannedMeals, logs, dismissals)` is a pure function. +Nudge dismissals live in `localStorage` keyed `nk-nudge-dismissed::` — +device-local by design (no server round-trip for a courtesy banner). Rejected: +user-configurable windows (belongs to reminders/007). + +## R7 — Recents & keepers derivation + +**Decision:** one query over the user's last 100 logs (excluding QUICK_LOG), grouped +client-side by source identity (`recipe_id` → `prepped source` → lowercased +`name_snapshot`): *recents* = latest N distinct sources; *keepers* = sources whose most +recent non-null verdict is KEEP, with `timesMade` = occurrence count. Client-side +grouping keeps PostgREST usage trivial and the logic unit-testable; 100 rows is ample +for a personal log. Rejected: SQL view/RPC (premature; revisit when suggestion features +need server-side aggregation). + +## R8 — Coverage card reuses feature 004 fulfillment verbatim + +**Decision:** the day-coverage chips and per-card primary action call the existing +fulfillment derivation (`derivePlannedMealFulfillment` seam from feature 004) against +the same inventory snapshot the planner uses; logged meals display as "Eaten" regardless +of fulfillment. No new derivation logic. Rejected: persisting coverage (fulfillment is +display-time by design). + +## R9 — Sheet accessibility done properly this time + +**Decision:** the Quick Log sheet implements Escape-to-close, backdrop click, focus +restoration to the invoking control, and `inert` on the background app shell — the gaps +past reviews flagged on planning's sheets. Scoped to the new sheet; retrofitting +planning's sheets is separate cleanup. Verdict buttons follow the mockup contract: +`aria-pressed`, tap-again-to-clear, ≥44 px targets, icon + label always. diff --git a/.specswarm/features/006-today-logging/spec.md b/.specswarm/features/006-today-logging/spec.md new file mode 100644 index 0000000..d941ff5 --- /dev/null +++ b/.specswarm/features/006-today-logging/spec.md @@ -0,0 +1,259 @@ +--- +parent_branch: feat/006-today-logging +feature_number: 006 +status: Implemented — pending manual verification + /ss:ship +created_at: 2026-07-09T15:30:00-05:00 +references_consulted: + - nexus-kitchen-requirements.md + - nexus-kitchen-domain-specification.md + - nexus-kitchen-invariants.md + - nexus-kitchen-differentiator.md + - nexus-kitchen-design-system.md + - design/screens/web-today.html, tablet-today.html, mobile-today.html, mobile-log.html +--- + +# Feature: Today dashboard, one-tap meal logging & meal verdicts + +## Overview + +The app can plan, stock, and shop — but it has no daily surface and no way to say "I ate." +This feature adds the **Today dashboard** as the app's home screen and closes the supply +loop with **one-tap meal logging** and an optional, shame-free **meal verdict** +(Again / Fine / Not for me). Logging a prepped meal draws down the portion ledger, keeping +HAVE_IT truthful; verdicts feed future suggestion features so good options resurface +without re-deciding. + +Concretely, delivers: + +- A `/today` home surface: greeting, per-slot meal cards with status, day-coverage + summary, quick actions, and an attention list for prepped food about to turn + (Domain Specification §4.2; REQ-UX-011/012/013). +- One-tap logging of planned meals (REQ-MR-006, REQ-NT-006) — the first writer of the + `PLANNED → LOGGED` transition shipped in feature 003 (INV-PL-005). +- A Quick Log sheet: slot picker, "I ate something" zero-detail log (REQ-MR-009), + prepped meals, recents, and keepers (REQ-MR-008), per `design/screens/mobile-log.html`. +- Prepped-meal logs consume portions through the append-only portion ledger + (INV-XD-003, INV-CC-006), linking the ledger event to the log entry. +- Optional three-way verdict capture at log time or later, plus a "How were these?" + recap for recent unrated meals (REQ-MR-007 as amended below). +- A gentle, client-side mealtime nudge banner (visual cue only — push reminders are + feature 007). + +## Scope + +**In scope:** `meal_logs` table + RLS; log domain module; Today route/components; +Quick Log sheet; verdict capture/recall; client-side nudge banner; nav/home wiring. + +**Out of scope:** push/scheduled reminders (§3.6.1, REQ-MR-001..005 — feature 007); +visual timeline/history (§3.6.3, REQ-MR-010..012); nutrition capture on logs +(REQ-NT-009 opt-in, deferred); variety/rotation consumers of verdict data (§3.7); +household sharing; meal-prep sessions; recipe 1–5 star ratings (already shipped in 001, +separate concept per Domain Specification `UserRecipeMeta`). + +## Clarifications + +### Session 2026-07-09 + +- Q: The SRS/domain spec define a four-value `MealRating` (HATED/OK/LIKED/LOVED), but the + canonical mockups and shipped `base.css` primitives implement a three-value **verdict** + (`keep` "Again" / `fine` "It was fine" / `rest` "Not for me") via `.nk-verdict` and + `--verdict-*` tokens. Which wins? → A: **The design mockups win** (project rule: + `design/screens/*.html` are canonical). The verdict enum is `KEEP | FINE | REST`; + `KEEP` absorbs LIKED/LOVED, `FINE` = OK, `REST` is the shame-free framing of HATED + ("set it aside" — a verdict on the food, not the person). REQ-MR-007 is satisfied in + substance; the deviation is recorded here. +- Q: `MealLog` is append-only (INV-CC-004, REQ-CN-007), yet the design lets users set a + verdict "anytime later". → A: The log **occurrence** (what/when/how much) is immutable + after insert; `verdict` and `notes` are late-arriving annotations and the only mutable + fields, enforced by a DB trigger. No LWW overwrite of occurrence facts is possible. +- Q: Domain entity lists five log types but INV-XD-001/002 only constrain FROM_PLAN and + FROM_PREPPED. → A: Ship all five types (`FROM_PLAN, FROM_RECIPE, FROM_PREPPED, + QUICK_LOG, CUSTOM`); enforce the type↔reference rules at insert time (references may + later become NULL if the source row is deleted; the name snapshot preserves display). +- Q: Undo for accidental one-tap logs? → A: **Deferred.** No DELETE policy in v1; the + confirmation is calm, not modal. Revisit alongside the timeline feature where a log can + be reviewed. Recorded as A-004. +- Q: Does the Today reminder banner require the reminders backend? → A: No — it is a + purely client-side visual cue derived from the clock, today's plan, and today's logs + (REQ-UX-011). Push delivery is feature 007. Dismissal is local to the device and day. + +## User Scenarios + +### Primary: lunch, one tap + +Jordan planned Marinara pasta (prepped, 3 portions left) for lunch. Around noon the Today +screen shows a gentle "It's around lunchtime" banner. Jordan taps **Log it** on the lunch +card. The card flips to "Eaten · 12:04pm" immediately, portions drop to 2, and an +unobtrusive "How was it?" appears. Jordan ignores it — that's fine. + +### Quick log: "I ate something" + +Mid-afternoon, Jordan ate crackers at the desk. From Today, **Log a meal** opens the sheet; +slot defaults to Snack; one tap on **I ate something** records it with no details. Total: +3 taps, no typing, no judgment. + +### Verdict later + +The next morning the recap card asks "How were these?" for yesterday's unrated lentil +curry. Jordan taps **Again**. Lentil curry now carries a Keeper mark and appears in the +Keepers group of the log sheet with "made 3 times". + +### Dinner not covered + +Dinner's salmon is MUST_ACQUIRE. Its card offers **Add to list** (not "Log it") and the +coverage card says "Tonight's salmon is the only gap — one thing to pick up, and the +day's set." + +### Edge cases + +- No plan at all today → coverage card invites planning ("Nothing planned — that's okay. + Want to pick something easy?"); meal grid shows empty-slot invitations; Quick Log still + fully works. +- Logging a prepped meal with 1 portion left → succeeds, portions hit 0; the item stops + appearing in the sheet's prepped group. +- Two rapid taps on **Log it** → exactly one log (button disables optimistically). +- Log insert succeeds but portion consumption or status flip fails (offline blip) → log + is kept; calm notice "Logged — we'll tidy the pantry count when you're back online"; + no crash, no rollback of the log (FR-TL-014). +- Planned meal's recipe deleted after logging → log still displays via name snapshot. +- Verdict tapped again → deselects back to unrated (never required). +- Day boundary: "today" is the user's local calendar day; logs at 11:58pm belong to that + day; the dashboard rolls over naturally on next render. + +## Functional Requirements + +### Today surface + +- FR-TL-001: The system shall provide a Today dashboard as the app's home surface: the + root path redirects to `/today` and the existing "Today" navigation item activates + (REQ-UX-012, Domain Specification §4.2). +- FR-TL-002: The Today screen shall show a time-of-day greeting and the full date with + calm, pressure-free copy per the canonical mockups (design voice rules). +- FR-TL-003: The Today screen shall list today's planned meals as slot-labelled cards in + slot order, each showing the meal name, source tag (e.g. "Prepped · N left"), and + status: logged ("Eaten · h:mm") vs not yet (REQ-UX-013, Domain Specification §4.2). +- FR-TL-004: Each unlogged planned-meal card shall present exactly one primary action + derived from its fulfillment state: **Log it** (HAVE_IT / CAN_MAKE_IT) or **Add to + list** (MUST_ACQUIRE, navigating to shopping) (REQ-UX-001; feature 004 derivation). +- FR-TL-005: The Today screen shall show a day-coverage card summarising each planned + slot's fulfillment as chips plus one supportive sentence (REQ-UX-011/013). +- FR-TL-006: Empty meal slots shall show a low-pressure invitation ("Add a snack — + optional") linking to the planner; absence of a plan is never framed as failure + (REQ-UX-014, design voice rules). +- FR-TL-007: The Today screen shall offer quick actions: Plan the week, Build shopping + list, Browse the pantry (Domain Specification §4.2). ("Start a prep session" waits for + the prep feature.) +- FR-TL-008: The Today screen shall show an attention list of prepped meals with portions + remaining that expire within 3 days, each linking to the planner ("Plan it in") + (differentiator: surface "what's about to turn"). + +### One-tap logging + +- FR-TL-009: Users shall log a planned meal with a single tap; the log records source + reference, name snapshot, slot, and the planned servings, applied optimistically + (REQ-MR-006, REQ-NT-006, REQ-CN-003). +- FR-TL-010: Logging a planned meal shall transition that planned meal `PLANNED → LOGGED` + with `logged_at` set (INV-PL-005, Domain Specification §5.4); an already-LOGGED planned + meal shall not be double-flipped. +- FR-TL-011: A Quick Log sheet shall be reachable from Today, offering: a slot picker + defaulting to the current mealtime, "I ate something" zero-detail logging (REQ-MR-009), + today-ready prepped meals with portion counts, recent meals, and keepers (REQ-MR-008), + per `mobile-log.html`. +- FR-TL-012: Logging a prepped meal shall consume the logged servings from the portion + ledger as a CONSUMED event linked to the log entry (`triggered_by` = log id), never + writing the portion count directly (INV-XD-003, INV-INV-004/010/011, INV-CC-006). +- FR-TL-013: Every log path reachable from Today shall complete in at most 3 taps with + immediate visual feedback and a calm confirmation (REQ-UX-003, REQ-UX-015). +- FR-TL-014: If the log record persists but a follow-up write fails (portion event or + planned-meal flip), the log shall be kept and a calm non-blocking notice shown; partial + failure shall never crash or silently disappear (pattern precedent: shopping trip + completion). + +### Verdicts + +- FR-TL-015: At log time the user may optionally set a three-way verdict — KEEP + ("Again"), FINE ("It was fine"), REST ("Not for me") — always skippable and + deselectable, labels always paired with icons (REQ-MR-007 as amended; design system + valence rules). +- FR-TL-016: A verdict (and notes) may be set or changed on an existing log at any later + time; all other log fields are immutable after insert (INV-CC-004, REQ-CN-007). +- FR-TL-017: Today shall show a "How were these?" recap of up to 3 recent unrated logs + (last 48 h, excluding zero-detail QUICK_LOG entries) with inline verdict capture; the + module disappears when there is nothing to ask. +- FR-TL-018: Wherever previously logged meals resurface (recents, keepers), the system + shall show the source's current verdict mark; the Keepers group lists sources whose + most recent verdict is KEEP, with a times-made count. + +### Gentle nudge + +- FR-TL-019: During a mealtime window whose slot has a planned, unlogged meal, Today + shall show a gentle banner ("It's around lunchtime… No rush.") with **Log** and **Not + now**; "Not now" dismisses that slot for the rest of the local day on that device. No + notification is sent (REQ-UX-011; push is feature 007). + +### Data & integrity + +- FR-TL-020: Meal logs shall be stored in a `meal_logs` table: UUID PK, owner-scoped + default-deny RLS, `timestamptz` UTC, insert-time validation that the log type's + required reference and name snapshot are present (INV-XD-001/002), cross-user + reference guards, and no DELETE capability in v1 (INV-SEC-004/006, INV-DB-001/002/011). +- FR-TL-021: Database types shall be regenerated and mapped to camelCase app types; UI + code shall never import generated row types directly (established convention). + +## Success Criteria + +- From a cold open of the app, logging today's planned lunch takes 1 tap on the meal card + (≤3 taps including any scrolling context), and the card reflects "Eaten" in under + 200 ms perceived (optimistic). +- Logging a prepped meal visibly decrements its portion count everywhere (Today, pantry) + without any direct write to the portion column; the ledger gains exactly one CONSUMED + event referencing the log. +- A planned meal logged via Today reads LOGGED with a timestamp in the planner. +- "I ate something" produces a log with no name, no reference, and no verdict — and zero + validation errors. +- A user who never sets a verdict experiences no prompts beyond the passive recap card. +- Another user's meal logs are invisible and unwritable (RLS proven both directions by + pgTAP). +- All automated gates green: `check`, `lint`, unit, build, pgTAP; e2e for the today/log + flow passes locally. + +## Key Entities + +- **MealLog** — one eating occurrence. `id`, `ownerId`, `logType` (FROM_PLAN / + FROM_RECIPE / FROM_PREPPED / QUICK_LOG / CUSTOM), optional source refs + (`plannedMealId`, `recipeId`, `preppedMealId`), `nameSnapshot`, `mealSlot?`, + `servings` (default 1), `loggedAt`, `verdict?` (KEEP/FINE/REST), `notes?`. + Deferred fields from the domain entity: `nutritionInfo` (opt-in nutrition, later), + `householdId` FK enforcement (household feature). +- **PortionEvent (existing)** — gains its intended `triggered_by` link: CONSUMED events + created by prepped-meal logs carry the log's id. +- **PlannedMeal (existing)** — first writer of `status = LOGGED` + `logged_at`. + +## Assumptions + +- A-001: **Slot windows** for nudges/defaults are fixed client constants (breakfast + 06–10, lunch 11–14, dinner 17–21, else snack); personalisation belongs to the + reminders feature. +- A-002: **Keepers/recents derive from logs** at query time; no denormalised verdict on + recipes. `user_recipe_meta.times_cooked` is not written by logging (cooking ≠ eating); + times-made counts come from log aggregation. +- A-003: **Low-stock attention items** ship only if the pantry module already exposes a + low-stock signal; otherwise the attention list is expiring-prepped only (matches + mockup content, avoids scope creep into pantry). +- A-004: **No undo/delete of logs in v1** (append-only); accidental logs are tolerated + until the timeline feature adds review affordances. +- A-005: Deferred per corpus but out of this feature: reminders backend (007), visual + timeline (§3.6.3), nutrition on logs, variety consumers, household visibility of logs. + +## Sources + +| Source | Sections informing this spec | +|--------|------------------------------| +| nexus-kitchen-requirements.md | §3.6.2 (REQ-MR-006..009), REQ-NT-006, §6 (REQ-UX-001/003/011/012/013/014/015), REQ-CN-003/007, Appendix A P1 | +| nexus-kitchen-domain-specification.md | MealLog entity + MealLogType + MealRating, §4.2 Home Screen Flow, §4.8 Log flow, §5.4 PlannedMeal state machine | +| nexus-kitchen-invariants.md | INV-XD-001/002/003, INV-PL-005, INV-CC-004/006, INV-CON-003/004, INV-SEC-004/006, INV-DB-001/002/011, INV-INV-004/010/011, INV-MOD-001/002 | +| nexus-kitchen-differentiator.md | capture feeds the operations loop; "what's about to turn" | +| design/screens/*.html + base.css | canonical Today/Log layouts, `.nk-verdict` three-way control | + +No section was fabricated without a corresponding source citation or a resolved +Clarification entry. diff --git a/.specswarm/features/006-today-logging/tasks.md b/.specswarm/features/006-today-logging/tasks.md new file mode 100644 index 0000000..54eab88 --- /dev/null +++ b/.specswarm/features/006-today-logging/tasks.md @@ -0,0 +1,89 @@ +# Tasks: Today dashboard, one-tap meal logging & meal verdicts + +**Feature**: 006 | **Branch**: `feat/006-today-logging` +**Plan**: [plan.md](./plan.md) | **Spec**: [spec.md](./spec.md) + + + + +User stories (from spec, priority order): + +- **US1 (P1)** — See my day: Today dashboard with meal cards, coverage, quick actions. (FR-TL-001..008) +- **US2 (P1)** — One-tap logging: planned meals, Quick Log sheet, prepped draw-down. (FR-TL-009..014) +- **US3 (P2)** — Verdicts: capture at log time or later, recap, keeper marks. (FR-TL-015..018) +- **US4 (P3)** — Gentle mealtime nudge banner. (FR-TL-019) + +## Phase 2: Foundational (blocks all stories) + +- [X] T001 Create migration: `meal_log_type` + `meal_verdict` enums, `meal_logs` table with CHECKs, `meal_logs_validate_source` + SECURITY DEFINER ownership + `meal_logs_annotation_only` triggers, RLS enable + owner SELECT/INSERT/UPDATE policies (no DELETE), grants (no DELETE to authenticated), realtime publication, indexes, `set_updated_at` trigger per data-model.md — supabase/migrations/0009_meal_logs.sql +- [X] T002 Apply migration locally and regenerate DB types (`bun run db:types`) — src/lib/database.types.ts +- [X] T003 pgTAP suite: RLS isolation both directions; UPDATE of verdict/notes allowed, occurrence columns rejected; DELETE rejected; source-validation per log type both ways; cross-user planned/recipe/prepped refs rejected; QUICK_LOG nameless allowed — supabase/tests/meal_logs_rls.test.sql +- [X] T004 [P] Log domain types: enum/row aliases, `MealLog`, `MealLogDraft` union, `toMealLog` mapper — src/lib/log/types.ts +- [X] T005 [P] Pure slot/time helpers: `slotForTime`, local day bounds, `deriveNudge` + unit tests — src/lib/log/slots.ts, src/lib/log/slots.spec.ts +- [X] T006 [P] Pure derivations: `groupLogSources`, `keepers`, `recents`, `unratedRecent`, `deriveDayCoverage` + unit tests — src/lib/log/derive.ts, src/lib/log/derive.spec.ts +- [X] T007 Thread optional `{ triggeredBy }` through `consumePortions` → CONSUMED event `triggered_by`, and through `optimisticConsumePortions`; extend existing specs — src/lib/pantry/preppedMealService.ts, src/lib/pantry/preppedMealStore.svelte.ts, src/lib/pantry/preppedMealService.spec.ts + +## Phase 3: US2 — One-tap logging (MVP core) + +- [X] T008 [US2] `logService`: `createLog(draft)` (draft→insert mapping incl. snapshots), `setVerdict`, `fetchLogsBetween`, `fetchRecentLogs`, `markPlannedMealLogged` safe-flip; `LogError` calm errors + unit tests (makeChain mock) — src/lib/log/logService.ts, src/lib/log/logService.spec.ts +- [X] T009 [US2] `logStore`: today/recents state + getters, `loadToday`, `loadRecents`, orchestrated `logMeal` (optimistic append, insert-failure rollback, follow-up consume/flip with calm `logNotice` on partial failure, in-flight double-tap guard), `rateLog` optimistic + unit tests — src/lib/log/logStore.svelte.ts, src/lib/log/logStore.spec.ts +- [X] T010 [US2] Public exports barrel — src/lib/log/index.ts + +**Checkpoint**: `logMeal` fully unit-tested against mocks; DB invariants pgTAP-proven; no UI yet. + +## Phase 4: US1 — Today dashboard + +- [X] T011 [US1] Route shell + home rewire: `/today` page, root redirect → `/today`, Today nav item `href` enabled — src/routes/today/+page.svelte, src/routes/+page.svelte, src/routes/+layout.svelte +- [X] T012 [US1] `TodayDashboard` shell: greeting/date, section order per web-today.html, loads plan-for-today + inventory + logs via existing stores + logStore — src/lib/components/today/TodayDashboard.svelte +- [X] T013 [P] [US1] `TodayMealCard`: slot label, tile, name, prepped tag with portions, states (Eaten · time / Have it / Need to get), single primary action Log-it / Add-to-list (FR-TL-004), double-tap guard — src/lib/components/today/TodayMealCard.svelte +- [X] T014 [P] [US1] `DayCoverageCard`: chips + calm headline from `deriveDayCoverage` — src/lib/components/today/DayCoverageCard.svelte +- [X] T015 [P] [US1] `AttentionList`: prepped ≤3 days to expiry with portions, "Plan it in" links; hidden when empty — src/lib/components/today/AttentionList.svelte +- [X] T016 [P] [US1] Quick actions grid + empty-slot invitation cards per mockups — src/lib/components/today/TodayDashboard.svelte + +**Checkpoint**: Today renders real plan/pantry data; planned-meal one-tap logging works end-to-end. + +## Phase 5: US2 — Quick Log sheet + +- [X] T017 [US2] `QuickLogSheet` per mobile-log.html: slot picker (default `slotForTime`), "I ate something", prepped group (portions), Recent, Keepers groups; proper dialog a11y (Escape, backdrop, focus restore, `inert` background — research R9); mobile FAB + desktop entry — src/lib/components/today/QuickLogSheet.svelte, src/lib/components/today/TodayDashboard.svelte + +## Phase 6: US3 — Verdicts + +- [X] T018 [P] [US3] `VerdictControl`: wraps `.nk-verdict` (Again / It was fine / Not for me), aria-pressed, tap-again-to-clear, ≥44px targets — src/lib/components/shared/VerdictControl.svelte +- [X] T019 [US3] Verdict wiring: inline "How was it?" on just-logged card, sheet footer capture, verdict marks + times-made in Recent/Keepers groups — src/lib/components/today/TodayMealCard.svelte, src/lib/components/today/QuickLogSheet.svelte +- [X] T020 [US3] `RecapCard`: up to 3 unrated last-48h logs with inline VerdictControl; hidden when empty — src/lib/components/today/RecapCard.svelte + +## Phase 7: US4 — Nudge + +- [X] T021 [US4] `NudgeBanner`: `deriveNudge` + localStorage day/slot dismissal, Log + Not now actions — src/lib/components/today/NudgeBanner.svelte + +## Phase 8: Polish & verification + +- [X] T022 e2e: extend mock backend with `meal_logs` (+ planned_meals status flip, portion_events); flow test — view today, one-tap log flips card + decrements portions, quick "ate something", verdict set/clear, RLS-shaped isolation not applicable (mock), double-tap yields one log — tests/today.e2e.ts +- [X] T023 `bun run check` + `bun run lint` + `bun run test:unit -- --run` + `bun run build` + `bunx supabase test db` all green; svelte-autofixer clean on new components — src/lib/log/index.ts +- [X] T024 Adversarial reviews: supabase-rls-reviewer (0009 + pgTAP) and svelte5-reviewer (today components); triage + fix criticals — .specswarm/quality-analysis-006-today-logging.md +- [X] T025 Quality analysis + metrics entry — .specswarm/quality-analysis-006-today-logging.md, .specswarm/metrics.json +- [ ] T026 Manual quickstart verification (quickstart.md loop) incl. responsive + a11y pass — .specswarm/features/006-today-logging/quickstart.md + +## Dependencies + +T001 → T002 → {T003, T004} ; T004 → {T005, T006, T008} ; T007 ∥ after T002 ; +T008 → T009 → T010 → T011 → T012 → {T013..T016} → T017 → {T018 → T019 → T020} → T021 → +T022 → T023 → T024 → T025 → T026. [P] tasks within a phase parallelize. + +## Implementation strategy + +MVP increment = Phases 2–4 (Today + planned-meal one-tap logging). Sheet, verdicts, and +nudge layer on without schema churn. Pure logic (T005/T006) and pantry threading (T007) +parallelize against service work. Component behaviour is covered by e2e + manual +quickstart because the browser vitest project hangs in agent sandboxes. + +## Verification notes (2026-07-09) + +All automated gates green: svelte-check 0/0, lint clean, 266/266 unit (45 new), +99/99 pgTAP (26 new), 25/25 e2e (8 new), static build clean, types regenerated. +Adversarial reviews done and criticals fixed — (1) annotation trigger now permits the +FKs' ON DELETE SET NULL transition (logged sources are deletable again; regression +pgTAP added), (2) portion_events.triggered_by gained a real FK + SECURITY DEFINER +ownership guard, (3) the Quick Log sheet stays open when a log insert fails and shows +the error inline. T026 (manual quickstart: responsive + a11y pass per quickstart.md) +remains open for a human session. diff --git a/.specswarm/metrics.json b/.specswarm/metrics.json index 170470e..5e82324 100644 --- a/.specswarm/metrics.json +++ b/.specswarm/metrics.json @@ -40,6 +40,21 @@ "reviews": "supabase-rls-reviewer + svelte5-reviewer dispatched; criticals fixed (cross-user FK guard, inert/Escape on sheets)", "infra_repair": "0008_data_api_grants.sql (CLI auto_expose_new_tables flip 2026-05-30)", "completed_at": "2026-06-11" + }, + "006-today-logging": { + "quality_score": 91, + "tasks_total": 26, + "tasks_completed": 25, + "tasks_outstanding": "T026 manual browser quickstart (needs human session)", + "unit_tests": { + "server": "266/266", + "new": 45 + }, + "pgtap_tests": "99/99 (26 new)", + "e2e_tests": "25/25 (8 new, tests/today.e2e.ts)", + "reviews": "supabase-rls-reviewer + svelte5-reviewer; criticals fixed (SET NULL cascade vs annotation trigger, triggered_by FK + ownership guard, sheet stays open on failed log)", + "infra_repair": ".prettierignore /.impeccable/ cache exclusion", + "completed_at": "2026-07-09" } } } \ No newline at end of file diff --git a/.specswarm/quality-analysis-006-today-logging.md b/.specswarm/quality-analysis-006-today-logging.md new file mode 100644 index 0000000..ac8bedf --- /dev/null +++ b/.specswarm/quality-analysis-006-today-logging.md @@ -0,0 +1,70 @@ +# Quality analysis — 006 Today dashboard, one-tap meal logging & meal verdicts + +**Date:** 2026-07-09 · **Branch:** `feat/006-today-logging` · **Score: 91/100** + +## Evidence + +| Gate | Result | +|------|--------| +| `bun run check` (svelte-check) | 518 files, 0 errors, 0 warnings | +| `bun run lint` (prettier + eslint) | clean (3 justified `svelte/prefer-svelte-reactivity` disables for transient timestamps / non-reactive Set) | +| Unit (vitest, server project) | 266/266 across 28 files — 45 new (slots 14, derive 13, logService 10, logStore 8) | +| pgTAP (`supabase test db`) | 99/99 — 26 new in `meal_logs_rls.test.sql` | +| e2e (Playwright, mock backend) | 25/25 — 8 new in `tests/today.e2e.ts` | +| `bun run build` (adapter-static) | clean | +| svelte-autofixer | 0 issues on TodayDashboard; suggestions triaged (2 acted on, see below) | +| Types | `database.types.ts` regenerated from 0009 (drift gate satisfied) | + +## Adversarial review outcomes + +**supabase-rls-reviewer** — +1. CRITICAL (also caught by self-test before the report landed): the annotation-only + trigger rejected the FKs' own `ON DELETE SET NULL` cascade, making any logged + recipe/planned meal/prepped meal undeletable. **Fixed**: reference columns may + transition value → NULL (never to a different row); pgTAP regression tests added + (delete-with-log succeeds, snapshot survives, ref-rewrite still rejected). +2. WARN: `portion_events.triggered_by` became load-bearing with no FK/ownership + validation — arbitrary UUIDs (incl. another user's log id) accepted. **Fixed** in + 0009: real FK (`REFERENCES meal_logs ON DELETE SET NULL`) + SECURITY DEFINER + ownership guard; pgTAP cross-user test added. + All other posture confirmed sound (default-deny, DEFINER safety, no upsert bypass, + grants, realtime, REVOKE DELETE effective for authenticated and anon). + +**svelte5-reviewer** — +1. WARN: the Quick Log sheet closed even when the insert failed (logMeal resolves + null rather than throwing), hiding the error behind the closed sheet. **Fixed**: + sheet closes only on a truthy saved log; `logError()` now also renders inside the + sheet (`role="alert"`). +2. NIT (inherited): sheet close button is 36 px < `--tap-min`, but exactly matches the + canonical `mobile-log.html` mockup — flagged to design, not changed. + Runes usage, SPA compliance, tokens, `{@html}` absence, dialog a11y (Escape / + backdrop / focus restore / `inert`), and optimistic rollback logic confirmed clean. + +**svelte-autofixer suggestions acted on**: load `$effect` re-keyed to the civil date so +minute ticks don't refetch; localStorage-backed dismissals moved to `$derived.by` with +a version counter. + +## Infra repair + +- `.prettierignore`: added `/.impeccable/` (design-hook cache tripped `bun run lint`). +- `bun run db:types` invokes bare `supabase` (not `bunx supabase`) and truncates + `database.types.ts` before failing when the CLI isn't on PATH — worked around + locally; script fix left for a chore PR. + +## Deductions (−9) + +- −3 T026 manual quickstart (responsive + a11y pass) still open — needs a human session. +- −2 nudge banner has no e2e (clock-window dependent); covered by 6 unit tests on + `deriveNudge` only. +- −2 component behaviour relies on e2e + manual pass rather than browser-project vitest + (project-wide constraint: browser lane hangs in agent sandboxes). +- −1 sheet close button under `--tap-min` (canonical-mockup tension, recorded above). +- −1 `check_meal_log_source_ownership`/`check_portion_event_trigger_ownership` block + service-role-context inserts that carry references (auth.uid() is NULL there) — + consistent with the 0007 precedent, but a future Edge Function writer must know. + +## Constitution + +P1–P15 walked in plan.md Constitution Check: PASS (SPA-only, runes, tokens, no +{@html}, anon-key only, RLS-before-exposure, UUID/timestamptz, calm copy, append-only +ledgers, online-first optimistic-with-rollback all verified by gates above). diff --git a/.specswarm/verify-queue/T001.pending b/.specswarm/verify-queue/T001.pending deleted file mode 100644 index 34f6bc6..0000000 --- a/.specswarm/verify-queue/T001.pending +++ /dev/null @@ -1,6 +0,0 @@ -task_id=T001 -queued_at=2026-06-11T20:43:58-05:00 -feature_dir=/home/rghamilton3/workspace/nexus-kitchen/.claude/worktrees/feat+sub-picker-ui/.specswarm/features/005-sub-picker-ui -tasks_md=/home/rghamilton3/workspace/nexus-kitchen/.claude/worktrees/feat+sub-picker-ui/.specswarm/features/005-sub-picker-ui/tasks.md -task_desc=[US1] Add substitute `