-
Notifications
You must be signed in to change notification settings - Fork 0
Migrate storage from Hive to SQLite + add coach SQL query tool #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Devasy
wants to merge
86
commits into
r2.1.0
Choose a base branch
from
migrate/sqflite-db
base: r2.1.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
86 commits
Select commit
Hold shift + click to select a range
7fda8d5
Adds tests for screens
Devasy a3d61b9
Adds tests
Devasy 71659af
Adds comprehensive tests
Devasy 9732b9a
Adds new tests
Devasy 87ce588
Updates test.yml to run on release branches
Devasy bb98883
Adds test and resolved the warnings and issues
Devasy 8e745cc
Updates tests and minor bug fixes
Devasy f0fc3c2
Adds fixes for failing testsm and adds connection timeout safety for …
Devasy 43e4dd3
Adds missing lines patch
Devasy 3abd451
Updates the tests with analyse failures
Devasy 9a55007
Updates tests and routine creator to use the common component
Devasy 3b787a0
Updates flutter version and adds tests
Devasy 4b85588
Adds major genui Feature and renderer
Devasy 2a17656
chore: remove patch_so script
Devasy a790d6e
build: add --build-id=none for jni package in F-Droid metadata
Devasy e89e2ca
ci: add jni build-id sed step for future reproducible releases
Devasy 85a48fd
feat: assisted pullups, deload-aware ML, handle-scoped PRs, sleeping …
Devasy 62beb92
feat(genui): add A2UiProps alias-aware coercing property reader
Devasy 36253fb
feat(genui): add A2UiSpec contract, A2UiNode and A2UiRegistry
Devasy 85b472d
fix(genui): make A2UiRegistry throw on name/alias collisions
Devasy 6a67623
feat(genui): add A2UiParser with fence, envelope and alias repair
Devasy 116ef77
fix(genui): balanced-bracket JSON extraction and envelope singleton fix
Devasy c846369
feat(genui): inject A2UiTheme and extract shared panel chrome
Devasy ffa451e
test(genui): strengthen theme-injection and add A2UiPanel coverage
Devasy cb60d5f
feat(genui): add A2UiSeries as the shared categorical data shape
Devasy a426fa2
fix(genui): cover fallback path in A2UiSeries.extract, fix negative-m…
Devasy a0c3ffc
feat(genui): add StatCardSpec with typed props and trend synonyms
Devasy 1ce4ae1
feat(genui): add MetricGaugeSpec with safe progress and null value
Devasy 58a77f2
feat(genui): add DynamicChartSpec for line, bar and pie
Devasy 243d907
feat(genui): add ScatterPlotSpec with point repair and safe bounds
Devasy 43cae7f
feat(genui): add RadarChartSpec sharing the labels/series shape
Devasy d3c18b3
feat(genui): add DataListGroupSpec with row repair and optional title
Devasy 5e2e2f1
feat(genui): add FilterChipsSpec with nullable active option
Devasy 359ffd3
feat(genui): add GridContainerSpec, default registry and renderer
Devasy 8d32d0d
feat(genui): generate the A2UI prompt section from the registry
Devasy fc7e843
refactor(genui): wire coach screen to the A2UI package, drop legacy r…
Devasy 1b0b8cc
fix(genui): bracket negative-value ranges in DynamicChart line/bar axes
Devasy 2287ff0
test(genui): cover all-negative bounds and malformed point entries
Devasy 939349a
fix(genui): widen per-node children lookup back to components/element…
Devasy 298126e
fix(genui): widen looksLikeUi to catch prose-prefixed fences, fix vac…
Devasy 6561e95
refactor(genui): drop presentation payload from tools, add purity and…
Devasy 5735d98
fix(genui): depth-agnostic purity regex, pin two silent-visual regres…
Devasy c0913bb
fix(genui): propagate registry through recursion, pin prompt drift, c…
Devasy 8024efa
Merge branch 'r2.1.0' of https://github.com/Devasy/RepForge into feat…
Devasy 1f327a5
docs: add design spec for Hive->SQLite migration + coach SQL query tool
Devasy 7a0c18c
fix: persist assisted-load volume correctly, tighten exercise-handle …
Devasy f363a21
fix: bound sleep-analytics window, drop fabricated data, resolve musc…
Devasy 8862e9d
fix(genui): pie negative-value filtering, overflow guard, stat-card u…
Devasy 0ff6c2d
test: close vacuous-test gaps and pin already-fixed regressions
Devasy b35df0a
docs: add implementation plan for Hive->SQLite migration + coach SQL …
Devasy f22e1eb
chore: add sqflite dependencies for SQLite storage migration
Devasy 18bfb86
feat: add SqliteStorageService with schema and workout session CRUD
Devasy 8105d9d
fix: persist bodyWeightAtLog in SqliteStorageService sets table
Devasy 6908914
feat: implement routine and target CRUD in SqliteStorageService
Devasy bd95d23
feat: implement muscle group and custom exercise CRUD in SqliteStorag…
Devasy 34c485c
feat: implement settings, PR, training program, and conversation CRUD…
Devasy bf771d0
feat: implement export/import in SqliteStorageService, completing ISt…
Devasy 06281e0
feat: add settings enumeration helper to StorageService for migration
Devasy 93f0713
feat: add StorageMigrationService for one-time Hive-to-SQLite migration
Devasy 8c9efcf
feat: resolve Hive-vs-SQLite storage backend in main() before runApp
Devasy 6c82b4f
feat: add SqlQueryService for read-only SQL execution
Devasy 6265901
feat: wire run_sql_query tool into CoachToolService
Devasy e1854d4
fix: fall back to fresh StorageService when app is constructed withou…
Devasy 616ee4e
fix: block run_sql_query from reading settings/sqlite_master (credent…
Devasy 07ae559
fix: prevent trailing SQL comment from breaking LIMIT wrapper
Devasy 2b94d45
docs: warn model against SELECT * across joins in run_sql_query
Devasy 14d8ec8
refactor: extract testable storage backend resolution logic; guard sq…
Devasy e4cface
docs: add design spec for syncing sleep/HR data into SQLite for coach…
Devasy d009807
docs: add implementation plan for syncing sleep/HR data into SQLite
Devasy 4b607be
feat: add health_samples/sleep_sessions tables + upsert methods to Sq…
Devasy 35952cc
fix: prevent run_sql_query from closing the app's shared database con…
Devasy ca12d74
fix: address Task 1 review findings
Devasy 5569113
fix: replace Finalizer with deterministic tearDown cleanup
Devasy fdb7d88
feat: add HealthDataSyncService to pull sleep/HR data into SQLite
Devasy a35eed7
feat: sync health data into SQLite once per app launch
Devasy 7f7504e
feat: add manual 'Sync coach data now' action to Profile screen
Devasy f59f749
feat: teach run_sql_query about the new health_samples/sleep_sessions…
Devasy 71de154
fix: remove overly broad auto-close from init, add explicit close to …
Devasy 6e9014d
fix: address final review findings for health sync + coach SQL tool
Devasy cc45fcb
fix: address migration/SQL-tool review findings from PR #66
Devasy 3019728
Merge branch 'r2.1.0' into migrate/sqflite-db
Devasy a04517f
fix: address second round of CodeRabbit findings on PR #66
Devasy 727b1b1
feat: make coach tool-call round limit configurable, surface invoked …
Devasy b664f20
Merge branch 'main' of https://github.com/Devasy/RepForge into migrat…
Devasy 715ef8c
Merge branch 'r2.1.0' into migrate/sqflite-db
Devasy 1719801
Merge branch 'r2.1.0' of https://github.com/Devasy/RepForge into migr…
Devasy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
2,413 changes: 2,413 additions & 0 deletions
2,413
docs/superpowers/plans/2026-08-08-sqlite-migration-and-coach-sql-tool.md
Large diffs are not rendered by default.
Oops, something went wrong.
1,032 changes: 1,032 additions & 0 deletions
1,032
docs/superpowers/plans/2026-08-11-health-data-sync-and-coach-sql.md
Large diffs are not rendered by default.
Oops, something went wrong.
135 changes: 135 additions & 0 deletions
135
docs/superpowers/specs/2026-08-11-health-data-sync-and-coach-sql-design.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| # Health Data Sync (Sleep + HR) into SQLite — Design Spec | ||
|
|
||
| **Date:** 2026-08-11 | ||
| **Status:** Approved | ||
| **Feature area:** Storage layer (`lib/services/`) + AI Coach SQL tool (`lib/services/ai/`) | ||
|
|
||
| --- | ||
|
|
||
| ## 1. Problem | ||
|
|
||
| The AI Coach's `run_sql_query` tool (added in `docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.md`) can query workouts, sets, targets, and PRs directly — but health data (sleep stages, heart rate, resting HR, HRV) is fetched live from Health Connect on every request via `HealthConnectService`/`HealthHistoryManager` and is never persisted. This means the coach cannot join health data against workout data in a single SQL query (e.g. "average sleep the night before a PR attempt" or "HR trend across the last 8 weeks of leg day sessions") — each half of the question requires a separate tool call and the model has to reconcile the join itself, unreliably. | ||
|
|
||
| This spec adds three SQLite tables that mirror Health Connect data, plus a sync service that keeps them populated, so `run_sql_query` can join across workout and health data directly. | ||
|
|
||
| --- | ||
|
|
||
| ## 2. Goal | ||
|
|
||
| 1. Persist sleep sessions (with stage breakdown) and HR-related samples (raw heart rate, resting heart rate, HRV RMSSD) into the same SQLite database `SqliteStorageService` already owns. | ||
| 2. Keep this data reasonably fresh via sync-on-app-launch (throttled) plus a manual "Sync now" action — no background service. | ||
| 3. Extend `run_sql_query`'s schema description so the coach can query and join the new tables. | ||
| 4. Keep the existing live `get_health_metrics` coach tool as-is, for "right now" freshness the synced tables won't have until the next sync. | ||
|
|
||
| Non-goals: no background/periodic sync (WorkManager or equivalent), no downsampling/compaction of old raw samples, no changes to `IStorageService`'s method signatures (this feature is additive on `SqliteStorageService` directly, matching how `SqlQueryService` already bypasses that interface), no UI beyond one manual sync button. | ||
|
|
||
| --- | ||
|
|
||
| ## 3. Schema | ||
|
|
||
| Added to the same database `SqliteStorageService` manages, created in `onCreate` (and via a migration step for existing installs already past `onCreate` — see §6). | ||
|
|
||
| ```sql | ||
| -- Raw heart rate, resting heart rate, and HRV RMSSD samples all share the | ||
| -- same {time, value} shape from Health Connect; one EAV-style table avoids | ||
| -- three near-identical tables and keeps the coach's query surface simple | ||
| -- ("WHERE type = 'heart_rate'") instead of three tables to remember. | ||
| CREATE TABLE health_samples ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| type TEXT NOT NULL, -- 'heart_rate' | 'resting_heart_rate' | 'hrv_rmssd' | ||
| timestamp TEXT NOT NULL, -- ISO8601 | ||
| value REAL NOT NULL | ||
| ); | ||
| CREATE UNIQUE INDEX idx_health_samples_unique ON health_samples(type, timestamp); | ||
| CREATE INDEX idx_health_samples_type_ts ON health_samples(type, timestamp); | ||
|
|
||
| CREATE TABLE sleep_sessions ( | ||
| id TEXT PRIMARY KEY, -- synthetic: the start_ts ISO string | ||
| start_ts TEXT NOT NULL, | ||
| end_ts TEXT NOT NULL, | ||
| light_min INTEGER, | ||
| deep_min INTEGER, | ||
| rem_min INTEGER, | ||
| awake_min INTEGER | ||
| ); | ||
| CREATE INDEX idx_sleep_sessions_start ON sleep_sessions(start_ts); | ||
|
|
||
| CREATE TABLE sleep_stage_intervals ( | ||
| sleep_session_id TEXT NOT NULL REFERENCES sleep_sessions(id), | ||
| start_ts TEXT NOT NULL, | ||
| end_ts TEXT NOT NULL, | ||
| stage TEXT NOT NULL -- 'deep' | 'rem' | 'light' | 'awake' | ||
| ); | ||
| CREATE INDEX idx_sleep_stage_session ON sleep_stage_intervals(sleep_session_id); | ||
| ``` | ||
|
|
||
| Sync watermarks (one ISO8601 timestamp per data stream, e.g. key `health_sync.heart_rate`) are stored as ordinary rows in the existing `settings` table — no new table needed for that. | ||
|
|
||
| `sleep_sessions.id` is derived from `start_ts` so re-syncing the same session (e.g. after a Health Connect correction) is a natural upsert target, not a duplicate. | ||
|
|
||
| --- | ||
|
|
||
| ## 4. `HealthSyncService` | ||
|
|
||
| New file: `lib/services/health_sync_service.dart`. | ||
|
|
||
| ```dart | ||
| class HealthSyncService { | ||
| HealthSyncService(this._hc, this._db); | ||
|
|
||
| final IHealthConnectService _hc; | ||
| final SqliteStorageService _db; | ||
|
|
||
| Future<void> sync({bool force = false}) async { ... } | ||
| } | ||
| ``` | ||
|
|
||
| - **Throttle:** skip if the most recent sync (tracked via a `health_sync.last_run` watermark) was less than 30 minutes ago, unless `force: true`. | ||
| - **Per-stream incremental fetch with look-back:** for each of `sleep`, `heart_rate`, `resting_heart_rate`, `hrv_rmssd`: read that stream's watermark from `settings` (default `now - 90 days` if absent — the agreed backfill window). Fetch from `watermark - 3 days` through `now` — the 3-day look-back re-pulls recent data even though it was already synced, to catch late corrections Health Connect or the watch itself makes to recent records (e.g. a sleep session Health Connect revises the next morning). Anything before the look-back window is assumed final and is never re-fetched. | ||
| - **Upsert:** | ||
| - `health_samples`: `INSERT OR REPLACE` keyed by the `(type, timestamp)` unique index — naturally idempotent and self-correcting. | ||
| - `sleep_sessions` / `sleep_stage_intervals`: for each `SleepPeriod` in the fetch window, delete-then-reinsert `sleep_stage_intervals` for that session id and upsert the `sleep_sessions` row — same delete/reinsert-child-rows pattern the original migration spec already uses for `sets`/`exercise_logs`. | ||
| - After all four streams succeed, advance each stream's watermark to `now` and the `last_run` throttle marker to `now`. | ||
| - **Failure handling:** any exception (permission not granted, Health Connect unavailable, one stream fails) is caught per-stream — a failed stream's watermark is left untouched so the next sync retries it, and does not block the other streams or crash the caller. Matches the existing best-effort caching posture in `HealthHistoryManager._readCachedHrDay`. | ||
|
|
||
| ### Wiring | ||
|
|
||
| Only constructed when the active backend is `SqliteStorageService` — mirrors the existing guard in `main.dart:191-192` (`_storageService is SqliteStorageService ? SqlQueryService(...) : null`). Health data has no meaning under the pre-migration Hive fallback path. | ||
|
|
||
| - `AppInitializer` calls `sync()` once after both `HealthConnectService` and `SqliteStorageService` are ready, fire-and-forget (does not block first frame). | ||
| - A "Sync now" button is added to the existing health-permissions area of the Profile screen, calling `sync(force: true)`. | ||
|
|
||
| --- | ||
|
|
||
| ## 5. Coach SQL Tool Update | ||
|
|
||
| `CoachToolService`'s embedded schema description (used by `run_sql_query`, §7 of the original migration spec) gets the three new tables appended in the same one-line-per-table/column format as the existing schema text, so the model can join them against `sessions`, `exercise_logs`, and `sets` without a separate discovery call. | ||
|
|
||
| `get_health_metrics` (the existing live Health Connect tool) is unchanged — it remains the source for "right now" data that the synced tables won't have until the next app-open or manual sync. | ||
|
|
||
| --- | ||
|
|
||
| ## 6. Migration for Existing Installs | ||
|
|
||
| Existing SQLite installs (already past `onCreate`) need the three new tables added without a fresh install. `SqliteStorageService.init()` bumps `_dbVersion` and adds an `onUpgrade` step that runs the `CREATE TABLE`/`CREATE INDEX` statements from §3 if the new tables don't already exist (`CREATE TABLE IF NOT EXISTS`, safe to run unconditionally on upgrade). No data migration needed — these are brand-new tables with no prior data to carry forward; the first post-upgrade sync populates them via the normal 90-day backfill path. | ||
|
|
||
| --- | ||
|
|
||
| ## 7. Testing | ||
|
|
||
| - **`HealthSyncService`** (new test file, in-memory DB via `sqflite_common_ffi` + a fake `IHealthConnectService`): | ||
| - First sync with no prior watermark backfills the full 90-day window. | ||
| - Second sync only re-fetches from `watermark - 3 days` onward (verify the fake service receives the narrower range). | ||
| - Re-running sync is idempotent: no duplicate rows in `health_samples` or `sleep_sessions`, and changed values from a "corrected" fake response overwrite the prior row. | ||
| - A sync attempted less than 30 minutes after the last one is skipped unless `force: true`. | ||
| - An exception thrown by the fake health service for one stream doesn't propagate, doesn't advance that stream's watermark, and doesn't block the other streams from syncing. | ||
| - **`SqliteStorageService`**: extend the existing test file to cover the new upsert methods and the `onUpgrade` path (open a v-1 schema DB, run `init()`, assert the new tables exist). | ||
| - **`run_sql_query`**: extend `sql_query_service_test.dart` with a join query across `sessions`, `sets`, `sleep_sessions`, and `health_samples`, confirming the schema and join work end-to-end. | ||
|
|
||
| --- | ||
|
|
||
| ## 8. Rollout Notes | ||
|
|
||
| - No new dependencies — reuses `sqflite`, `sqflite_common_ffi` (test), and the existing `IHealthConnectService`. | ||
| - No changes to `IStorageService`, `MockStorageService`, or any manager/provider — additive on `SqliteStorageService` only, same boundary `SqlQueryService` already uses. | ||
| - `CLAUDE.md`'s "6 boxes" / schema references would benefit from a follow-up doc note once this ships, but that's out of scope here (same deferral pattern as the original migration spec, §9). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.