Skip to content

Progress flow: provenance read model, owned range state, wired UI + a11y evidence - #70

Draft
SamGu-NRX wants to merge 5 commits into
obv/fl-next-historical-nutrition-r1from
obv/fl-l4-progress-flow
Draft

SamGu-NRX wants to merge 5 commits into
obv/fl-next-historical-nutrition-r1from
obv/fl-l4-progress-flow

Conversation

@SamGu-NRX

@SamGu-NRX SamGu-NRX commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Progress flow: provenance read model, owned range state, wired UI (M1–M4)

Draft PR — work continues in milestones. Base obv/fl-next-historical-nutrition-r1, head obv/fl-l4-progress-flow.

What changed and why

The Progress tab aggregates day/7/30/90-day intake from the local cooking journal and Apple Health, but the surfaces that own those numbers do not tell Progress when their details change: the shared cooking-history observer tracks row count, max id, a rating checksum, and a servings checksum, so a portion edit or a date shift changes aggregates while leaving that token untouched, and profile/goal changes are invisible to it entirely. Progress cards would go silently stale. Separately, the repository's daily-total windows zero-fill missing days and averages divide by every window day, so "no data" days present as 0 intake; and nothing distinguishes a confirmed personal target from the app default.

M1 — provenance read model (commit d605b8e)

  • apps/ios/Feature/Progress/ProgressReadModel.swift — typed readings (ProgressTodayReading, ProgressRangeReading, ProgressRecentJournalEntry, ProgressStats, ProgressGoalTarget) that carry each number's provenance: localJournal vs appleHealth source per reading, optional per-day values (value == nil means no data, never 0), suggestedTarget vs confirmedPersonalTarget goal provenance, and a visible fallbackReason when the requested source failed. Averages cover days with data only. ProgressRevisionToken tracks portion multipliers (milli-units), cooked-at epoch checksum + max, and a health-profile digest over the same shared database, exposed as a ValueObservation so Progress invalidates on portion edits, date shifts, and profile/goal changes.
  • apps/ios/Feature/Progress/ProgressRangeState.swift — pure presentation-state transitions for day/7/30/90-day selection: stale results for a superseded range are dropped, failures keep the last good reading and surface an error.
  • apps/ios/Feature/Progress/ProgressFlowPolicy.swift — routing decisions for end-to-end wiring: tapped meals route to the existing journal detail, correction requests hand off when a correction flow is already active, goal editing is offered through the app's existing goal-editor callback only.
  • apps/ios/Tools/progress-flow-check/ — new offline package mirroring the historical-nutrition-check pattern (Scripts/refresh.sh stages real production sources, including the real migrations and repositories, into Sources/ProgressFlowCheck/Real/). Tests seed the app's actual persistence layer with synthetic data through real migrations and PersonalizationService.recordCooking, then drive the read model through its public API — no re-implemented arithmetic.

M2 — state ownership (commit 6daa89c)

  • apps/ios/Feature/Progress/ProgressRangeCoordinator.swift — owns the trend-range presentation state end to end: selecting a range or refreshing cancels the in-flight read; stop() (view close) cancels all work and tears down observation; journal/profile revisions trigger an explicit refresh through the real loader; failures retain the last good reading and surface the error. The coordinator is injectable: .live(...) drives the real read model on a database queue, .mock(...) serves synthetic readings for tests and previews.
  • The revision observation is owned by Progress (started on a shared DatabaseQueue) rather than piggybacking on the Home dashboard observer, because the revision dimensions (portion/date/profile) are exactly the ones the shared observer omits.

M3 — wired UI + on-demand provenance notes (commit f7ea3b4)

  • ProgressViewModel now loads through the read model (todayReading, weeklyReading, goalTarget are provenance-tagged), owns its revision-token observation (portion edits, date shifts, profile/goal changes → reload + trend refresh) and an Apple Health import observer (.appleHealthDidUpdate → reload + explicit trend refresh). The duplicate ChartRange declaration that lived in this file was removed (single definition in ProgressRangeState.swift).
  • ProgressWeeklyTrendSection renders coordinator-owned state — selection, in-flight load, last good reading on failure, source-labelled subtitle with coverage ("5 of 7 days with data · from your meal log"), a distinct fallback row when the Health read failed, and an error row. Unknown days are never plotted as zero: only days with data enter the chart; the empty chart says "Cook a meal to start tracking!" rather than drawing a zero line. Averages in the insight text cover days with data only.
  • ProgressRecentMealsSection meal cards are tappable and route through ProgressFlowPolicy.mealRoute(for:) to the existing RecipeJournalDetailView — no new journal editor; a portion/date revision lands in the journal and the revision-token observer refreshes the cards instead of leaving them stale.
  • On-demand provenance notes (R4): ProgressSourceNotes builds the wording (pure logic, staged into the offline package and tested) — the tab exposes a "Where do these numbers come from?" disclosure naming the source, the unknown-day averaging policy, coverage, and whether the target is saved or suggested. The calorie hero pill-labels suggested targets ("Suggested target") so a default never presents as user-set.
  • Goal editing still routes through the existing profile callback (onOpenProfileSettings); the meal-logging entry keeps the "Log a Meal" / fork.knife convention with an explicit accessibility label (PR 35's intent), and the correction-handoff rule is encoded in ProgressFlowPolicy (see Assumptions for the UI surface note).

M4 — accessibility evidence (commit 33ec552)

  • apps/ios/Tools/progress-flow-check/Tests/ProgressFlowCheckTests/ProgressContrastTests.swift — WCAG 2.x contrast ratios computed on-host from the actual AppTheme.swift token values, not asserted from memory. The math caught a real violation: the insight row's sage icon on its own 8% tint computed 2.87:1, below the 3:1 graphics threshold — the tint was removed (sage on the page background computes 3.10:1 light / 7.83:1 dark), and row/note body text moved to textPrimary (13.94:1 on the page background, light mode) so small text passes AA.
  • Known design-system finding, flagged not fixed: light-mode textSecondary computes 3.66:1 on the page background — below AA for small text. This is the app-wide subtitle convention (every FLSectionHeader), out of this task's file scope; documented in the ledger.
  • apps/ios/Tools/progress-flow-check/Results/EVIDENCE.md — the evidence ledger: every required check (contrast, keyboard, large type, reduced motion, labels, screenshots, compilation) mapped to verified-on-host / source-asserted / unexecuted-on-host with reasons and how-to-run instructions.
  • apps/ios/Tools/progress-flow-check/Scripts/capture-fixtures.sh — macOS/Xcode script that renders the Progress screen at 390x844 and 1440x900 from synthetic fixtures, exports PNGs, and drives the native XCUITest accessibility audit (performAccessibilityAudit()). Committed marked UNEXECUTED — this host has no Xcode; no PNGs exist and nothing was substituted (no webmocks; Playwright/axe are web tools and were not used).

Commands and results

Host: Debian 13 Linux, Swift 6.4 (x86_64-unknown-linux-gnu), no Xcode/macOS.

$ cd apps/ios/Tools/progress-flow-check
$ ./Scripts/refresh.sh   # stages 22 real production sources into Sources/ProgressFlowCheck/Real/
$ swift test
Test Suite 'ProgressFlowPolicyTests' passed ... Executed 4 tests, with 0 failures
Test Suite 'ProgressRangeCoordinatorTests' passed ... Executed 4 tests, with 0 failures
Test Suite 'ProgressRangeTransitionsTests' passed ... Executed 9 tests, with 0 failures
Test Suite 'ProgressReadModelTests' passed ... Executed 12 tests, with 0 failures
Test Suite 'ProgressSourceNotesTests' passed ... Executed 5 tests, with 0 failures
Test Suite 'ProgressContrastTests' passed ... Executed 5 tests, with 0 failures
Test Suite 'All tests' passed ... Executed 39 tests, with 0 failures (0 unexpected) in 2.087 (2.087) seconds

Computed contrast values printed by the suite (mirrored token values from AppTheme.swift): textPrimary/bg 13.94:1 light / 16.05:1 dark; textPrimary/surface 15.46:1 / 13.76:1; accent/bg 3.65:1 / 6.37:1; sage/bg 3.10:1 / 7.83:1; textSecondary/bg 3.66:1 / 8.78:1.

Raw logs are committed per milestone under apps/ios/Tools/progress-flow-check/Results/ (M3: progress-flow-check-m3.log, M4: progress-flow-check-m4.log plus EVIDENCE.md). The staged "Real" sources are the app's actual read-model/policy/coordinator files plus the real migrations and repositories; the seeded tests drive them through public API against synthetic data.

Actual vs missing native execution

Verified on-host (Linux):

  • All pure/read-model/coordinator/policy/note logic: 39 XCTest cases green against the real persistence layer with synthetic data, covering portion edits, date shifts, profile/goal changes, deletion, source selection + fallback, unknown-day semantics, cancellation/stop, retention-on-failure, note wording, and computed contrast ratios.
  • Contrast math (ProgressContrastTests) computed from the actual DesignSystem token values — including one real violation found and fixed (sage on its own tint, 2.87:1) and one design-system-wide gap flagged (light-mode textSecondary 3.66:1).

UNEXECUTED on this host, with reasons:

  • Full iOS app compilation — no Xcode/macOS. The PR's CI run (GitHub macos-26 runners via ios-ci.yml on pull_request) is the hosted compilation evidence. The app-target files changed here (view model + 5 views + 1 new view) were compiled only as far as Linux allows (the pure logic they call is package-tested); project.yml globs the Feature directory, so the new ProgressSourceNotes.swift is picked up automatically.
  • Fixture-capture script (Scripts/capture-fixtures.sh, committed, marked UNEXECUTED): renders the Progress screen at 390x844 and 1440x900 from synthetic fixtures and exports PNGs; requires Xcode previews/simulators. Not run — no macOS. Never substituted with webmocks; Playwright/axe were not used (web-only tools).
  • Native accessibility audit (XCUITest / Accessibility Inspector), keyboard traversal, Dynamic Type, and Reduce Motion recordings — all require a simulator; unexecuted. What Linux could do is done: contrast math from real token values, note wording tests, and the trait/hint source assertions. Results/EVIDENCE.md maps every check to its status with how-to-run instructions.

Assumptions

  • Correction handoff surface: Progress today has no correction entry point of its own (portion/date edits live in the journal detail screen, which owns that flow). ProgressFlowPolicy.correctionHandoff(isCorrectionFlowActive:) encodes the handoff rule and is tested, but there is no Progress UI that currently needs to call it — adding a correction entry point to Progress would expand the surface this task was scoped away from. The rule is wired for whoever adds that entry point.
  • The read model reuses UserDataRepository's existing daily-total windows as its per-day source and applies the unknown-day/average policy at the read-model layer, rather than changing repository arithmetic (out of scope). This means zero-filled days from the repository are mapped back to "no data" where the journal has no rows for that day — the read model decides emptiness from the journal's day keys, not from the zero value.
  • AppleHealthService.authorizationStatus() failing or returning data-less results is treated as "unknown day" rather than zero, with the fallback reason surfaced.

Expected conflicts

Handoff (final checkpoint, 2026-10-11 — workspace retirement)

The Obvious workspace/account that produced this PR is retired at ~2026-10-11 07:20Z. Its dispatched human-review task and workspace review-summary artifact do NOT survive retirement; the durable review record is apps/ios/Tools/progress-flow-check/REVIEW-SUMMARY.md (committed on this branch by the final checkpoint commit). Review must be re-requested in the successor workspace.

Done

  • M1 d605b8e — typed provenance read model (local-journal vs Health source; unknown day != zero; suggested vs confirmed target; revision token covering portion/date/profile) + apps/ios/Tools/progress-flow-check offline package with synthetic seeded-DB tests.
  • M2 6daa89c — ProgressRangeCoordinator: cancels superseded loads, stops on view close, refreshes on revision-token change, retains last good reading on failure.
  • M3 f7ea3b4 — UI wiring: view model through read model + coordinator, trend source/coverage/fallback states, meal-card tap-through to RecipeJournalDetailView, on-demand provenance notes, suggested-target pill, "Log a Meal"/fork.knife convention preserved (PR 35 intent).
  • M4 33ec552 — accessibility evidence: WCAG contrast computed from real DesignSystem tokens (fixed real 2.87:1 sage-on-tint violation in Progress; flagged app-wide light-mode textSecondary 3.66:1 as a separate out-of-scope decision), Results/EVIDENCE.md ledger, Scripts/capture-fixtures.sh (macOS capture script, committed marked UNEXECUTED).
  • Final checkpoint — REVIEW-SUMMARY.md mirrored into the repo as the durable review record (see Retirement note inside).
  • Hosted CI GREEN at 33ec552: build-and-test (macos-26, Xcode 26.6) and dead-code-advisory — that is the app-compilation evidence; the producing host was Linux (no Xcode).

NOT done

  • Native fixture PNG captures (390x844, 1440x900), XCUITest / Accessibility Inspector audit, keyboard traversal, Dynamic Type, Reduce Motion recordings — no macOS host was available; the script and ledger are committed with how-to-run. Never fabricated; no webmocks; no Playwright/axe.
  • Human review — the review task died with the retiring workspace; this PR has NOT been reviewed.

Exact next steps

  1. In the successor workspace, re-request human review of this PR using apps/ios/Tools/progress-flow-check/REVIEW-SUMMARY.md as the review record (it contains lead verification of authorship, CI, logs, and the acceptance-criteria status).
  2. On a macOS host: bash apps/ios/Tools/progress-flow-check/Scripts/capture-fixtures.sh — produce the two PNG sizes plus the native a11y audit, commit outputs under Results/, and flip the affected EVIDENCE.md rows from UNEXECUTED to executed.
  3. Decide separately on the app-wide light-mode textSecondary 3.66:1 contrast finding (out of this PR's scope by design).
  4. At merge time, reconcile expected conflicts: sibling drafts feat(ios): add storage-performance benchmark harness (storage-perf) #59/feat(ios): add local backup archive codec, validator, and staged restore engine #61/feat(ios): add unified local search — private FTS5 index, Search screen, and Linux eval package #62 (same base, disjoint Tools/* packages) and open PR 35 (ProgressTabView entry points).

Verify commands

  • git rev-parse HEAD — must equal the final checkpoint commit (33ec552 + one docs commit).
  • git log --format='%an <%ae>' obv/fl-next-historical-nutrition-r1..HEAD — every line must be Sam Gu <127461594+SamGu-NRX@users.noreply.github.com>.
  • git log --format='%B' obv/fl-next-historical-nutrition-r1..HEAD | grep -c 'Co-authored-by: obvious-autobuild' — expect 5 (one per commit).
  • swift test --package-path apps/ios/Tools/progress-flow-check — expect 39/39 passing (verified on Debian 13, Swift 6.4.0; raw logs under Results/).
  • CI: PR Progress flow: provenance read model, owned range state, wired UI + a11y evidence #70 checks on the head commit — build-and-test and dead-code-advisory should be green.

Decisions recorded

  • Unknown/missing days render as no-data, never zero; documented aggregate policy (averages exclude unknown days).
  • A default target always presents as "suggested"; only a confirmed personal goal presents as user-set.
  • Correction screen stays its owner: Progress hands off rather than starting its own; no correction entry point exists in Progress today, so the policy is implemented and tested but not wired (no seam exists — recorded as a limitation, not skipped).
  • Revision-token comparison added because the prior aggregated token missed portion and date edits, breaking long-lived sessions.
  • Goal edits route through the existing profile callback; no new journal or goal editor was added.
  • Repository/snapshot arithmetic untouched; all hard exclusions respected.
  • PR remains a draft — agents never merge.

…ckage

Add a UI-free Progress read model that maps today's nutrition, day/
7/30/90-day trends, recent journal entries, and goal targets from the
existing persistence layer, preserving each number's provenance: local
journal entry vs Apple Health total, confirmed personal target vs
suggested default. Unknown days stay distinct from zero intake and
averages cover days with data only.

ProgressRevisionToken tracks portion multipliers, cooked-at epochs, and
a health-profile digest — dimensions the shared dashboard observer
omits — so Progress invalidates on portion edits, date shifts, and
profile/goal changes. ProgressRangeState owns presentation state for
range switches: stale results are dropped, failures retain the last
good reading, and every reading is labelled with its single source.

New offline package Tools/progress-flow-check mirrors the historical
nutrition check pattern: refresh.sh stages the real production sources
into a Linux-runnable package, tests seed the actual migrations and
services with synthetic data, and the suite asserts portion, date, and
profile revisions through the real aggregation APIs.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ProgressRangeCoordinator owns the trend-range presentation state end to
end: selecting a range or refreshing cancels the in-flight read, stop()
(view close) cancels it and tears down the revision observation, and
every revision-token delivery triggers an explicit refresh — so accepted
portion/date revisions and profile/goal edits update Progress without
user action. Apple Health imports bypass the tracked tables, so the
view model refreshes on Health-import events and on appear.

Failures keep the last good reading and surface a readable message:
ProgressReadFailure.from unwraps our own kind instead of re-describing
the enum. The coordinator is lock-guarded rather than MainActor-isolated
so the off-host suite can drive it deterministically; the SwiftUI view
model hops to the main actor when mirroring state into @published.

Tests assert real cancellation (a parked load observes CancellationError
when the selection changes and when the view closes), that no load
starts after close, that journal and profile revisions trigger refresh
through the real loader over the real database, and that a failing
refresh retains the last good reading.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@SamGu-NRX SamGu-NRX changed the title Progress flow: provenance-preserving read model + offline check package (M1) Progress flow: provenance read model + owned range state (M1-M2) Oct 11, 2026
SamGu-NRX and others added 2 commits October 11, 2026 00:12
The Progress tab now renders from the provenance-preserving read model:
the trend chart consumes coordinator-owned range state (selection,
in-flight load, last good reading), meal cards route taps to the existing
journal detail screen, and source/aggregate-policy notes sit behind
on-demand disclosures. A suggested default target is pill-labelled so it
never presents as user-set, and the meal-logging entry follows the
"Log a Meal" / fork.knife convention with an explicit accessibility
label. Provenance note wording is staged into the offline package and
covered by tests (34 passing on Linux).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
…e script

Contrast ratios are now computed on-host from the actual DesignSystem
token values (WCAG 2.x luminance math) rather than asserted from memory.
The math caught a real violation — sage on its own 8% tint computes
2.87:1, below the 3:1 graphics threshold — so the insight row dropped
the tint, and row/note text moved to textPrimary for AA small-text
compliance (13.94:1 on the page background). Light-mode textSecondary
computes 3.66:1, the app-wide subtitle convention: flagged in the
evidence ledger as a design-system finding, not fixed here. The macOS
fixture-capture script (390x844 and 1440x900 PNGs plus the XCUITest
accessibility-audit path) is committed marked UNEXECUTED — this host
has no Xcode, and nothing was substituted for the missing PNGs.
39 tests passing on Linux; raw log committed.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@SamGu-NRX SamGu-NRX changed the title Progress flow: provenance read model + owned range state (M1-M2) Progress flow: provenance read model, owned range state, wired UI + a11y evidence Oct 11, 2026
Workspace retirement checkpoint: preserves the human-review record and acceptance-criteria status beyond the retiring workspace.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant