Migrate storage from Hive to SQLite + add coach SQL query tool - #66
Migrate storage from Hive to SQLite + add coach SQL query tool#66Devasy wants to merge 79 commits into
Conversation
…HR tool Batches several in-flight features that were sitting uncommitted: - Bodyweight/assisted pullup volume: (BW - assist + extra) * reps - MLService reads the past 3 sessions and recovers from a deload week using the pre-deload baseline instead of the deload trough - PRManager scopes records per handle variation (Rope vs Bar) - CoachToolService.get_sleeping_hr_analytics: p5/p25/mean, stdev, variance and linear trend over the last N nights - GenUI parser tolerates numeric StatCard values, loose trend words and Markdown code fences Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Foundation for the genui refactor: a never-throwing view over raw component prop maps that resolves keys by exact match, normalized match (case/underscore/hyphen/space-insensitive), then semantic alias, and coerces values to typed accessors with documented fallbacks instead of throwing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the four-in-one component contract (A2UiSpec) that lets each UI component name itself, parse its own props, build its own widget and document itself for the LLM prompt on one object, plus the A2UiRegistry lookup table that replaces the old allowedA2UiComponents set and two parallel switch statements. Includes an A2UiTheme skeleton (filled in by Task 4) and A2UiNode, the parsed-tree node type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code review found that A2UiRegistry's constructor loop silently resolved canonical-name/alias collisions (last-writer-wins for names, first-writer-wins for aliases), which would produce unreachable specs or dropped aliases with no signal as more components are registered in later tasks. The constructor now throws a StateError identifying both colliding specs for any of: two specs sharing a canonical name, an alias colliding with another spec's canonical name, or two specs sharing an alias. Adds three regression tests using a new configurable _NamedFakeSpec fake. Also documents (doc-comment only, no behavior change) that A2UiNode.children is not defensively copied, per the review's Minor finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the single gate that decides whether an LLM reply is a UI payload or ordinary prose, and turns UI payloads into an A2UiNode tree. Handles markdown fences, prose-wrapped JSON, flat vs props-wrapped shapes, bare-array/envelope auto-wrapping into GridContainer, and recursive children, without ever throwing. Also promotes A2UiProps._asStringKeyed to a public static A2UiProps.stringKeyed so the parser can re-key decoded JSON maps without an awkward part-of coupling between the two libraries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_extractJson previously sliced from the first { to the last }, which
broke on any stray brace in surrounding prose (e.g. "add reps
{optional}"). Replace with a scan that tries jsonDecode on every
balanced {..}/[..] span found via a depth counter that correctly skips
brackets inside string literals, preferring the longest successful
decode as the actual payload.
Also fix _wrap's unconditional single-child collapse: an explicit
envelope key ({"components":[...]}) is a deliberate container request
and must still produce a GridContainer with one child, while a bare
top-level array with one item keeps collapsing since it's ambiguous
between "a list of one" and "just one component."
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds A2UiThemeProvider (InheritedWidget, falls back to A2UiTheme.dark) and the panel/title/empty-state/legend widgets every component spec will share, plus lib/theme/a2ui_app_theme.dart mapping RepForge's real design tokens onto A2UiTheme. This is the only file where the two systems meet - lib/genui/ still imports nothing app-specific. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The injection test compared against repforgeA2UiTheme, which is field-for-field identical to the A2UiThemeProvider.of fallback (A2UiTheme.dark), so it passed even if the InheritedWidget lookup were broken. Inject a fixture with distinct values instead, and assert a sibling context still falls back to the default. Also add direct coverage for A2UiPanel's padding, decoration, and child rendering, previously only exercised indirectly via A2UiEmptyPanel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A2UiSeries.extract() and maxValue() give line/bar/pie and radar chart
components one common {name, values} shape to consume, so a model that
learns {labels, series} once can drive all four components.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ax bug Address code review findings on A2UiSeries: - Add tests pinning down the series->values fallback when every series entry drops to empty/unparseable values, and when series is an empty list — the risky path the brief called out but left untested. - Rename the misleading 'reads the axes alias' test; it only exercised stringified-number coercion inside series values, not alias resolution. - Fix maxValue() to track whether any value has been seen instead of seeding with 0.0, so all-negative series report their true max instead of silently clamping to 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Establishes the pattern for Tasks 7-13: a typed props record, an A2UiSpec bundling name/aliases/doc/parseProps/buildWidget, and never-throwing parsing that degrades to documented fallbacks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes the validator/renderer contradiction where a String value was accepted but cast to num, and the min == max NaN sweep angle bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the most-used and most complex A2UI component so far, covering
line/bar/pie rendering over the shared {labels, series} shape with
never-throwing prop parsing and label padding to prevent out-of-range
axis lookups.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds paired x/y observation plotting with an optional correlation badge, following the Task 6-8 A2UiSpec pattern. Malformed points are dropped rather than throwing, and bounds widen degenerate axes so fl_chart never sees a zero-span range. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… in SqliteStorageService
…t going through main()
…ial exposure) SELECT * FROM settings or sqlite_master passed all existing run_sql_query validation and would leak the migrated Gemini API key into model context and persisted chat history. Add a second denylist of restricted table/ schema identifiers, checked the same way as the existing forbidden-keyword list, plus a substring guard against SQLite's pragma_* table-valued functions.
A model-submitted query ending in a `--` line comment swallowed the wrapper's closing paren when concatenated onto one line, producing an avoidable syntax error. Put the closing `) LIMIT ?` on its own line. Also finishes staging test/sql_query_service_test.dart, which now covers both this fix (trailing-comment query succeeds) and the settings/ sqlite_master restricted-table rejections from the previous commit.
sqflite's row maps are keyed by column name, so a natural join query like "SELECT * FROM sessions s JOIN exercise_logs l ON ..." silently drops duplicate columns (e.g. id, notes) from one side with no error. Steer the model's generated SQL toward explicit aliased columns instead.
…liteStorage.init() - lib/main.dart: sqliteStorage.init() was outside the try/catch on the path every existing user hits on first launch after this update — disk-space/sandbox/SQLite-build failures propagated out of main() before runApp(), so the app never booted even though the working Hive storage right above it was fine. Now guarded with its own fallback to Hive. Also documents why Hive.initFlutter() stays unconditional post- cutover: ApiService reads/writes an installation id directly against this settings box, independent of IStorageService. - lib/services/storage_backend_resolver.dart (new): extracts the Hive-vs-SQLite decision (migrate-or-fallback, flag write) out of main.dart's untestable _resolveStorageBackend into a pure, directly testable top-level function. - test/storage_backend_resolver_test.dart (new): covers the two real-world paths every user takes — already-migrated relaunch, and fresh-install migration success. The forced-migration-failure case is intentionally omitted; there's no way to make StorageMigrationService.migrate() throw with SqliteStorageService's current public API without adding production surface purely for testability, and that path is exercised indirectly by storage_migration_service_test.dart.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## r2.1.0 #66 +/- ##
==========================================
+ Coverage 74.90% 76.43% +1.52%
==========================================
Files 88 113 +25
Lines 14491 16450 +1959
==========================================
+ Hits 10855 12573 +1718
- Misses 3636 3877 +241 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… SQL joins Lets run_sql_query join workout data against sleep/HR history instead of requiring separate live Health Connect tool calls per question.
Five-task TDD plan: schema + upsert methods, HealthDataSyncService, launch-time wiring, manual sync button, and the coach's schema description.
…liteStorageService - Add schema v2 with three new tables: health_samples, sleep_sessions, sleep_stage_intervals - Add upsertHealthSamples() and upsertSleepSessions() methods for health data sync - Add onUpgrade callback for v1->v2 schema migration - Use temporary files for in-memory test databases to support read-only connections - All tests passing (35/35) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nection
openReadOnlyDatabase(path) with the default singleInstance:true returns the
app's existing shared connection when called against the same path as
SqliteStorageService's live database, so the coach's per-query
finally { db.close() } was tearing down the app's only connection after
the first query. Pass singleInstance:false to force a genuinely separate
connection.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Remove _isTestDatabase path-substring flag; production init() no longer branches on test-fixture path content - Remove the unconditional health-schema fallback loop that made onUpgrade untested/redundant; onCreate and onUpgrade are now the only paths that create the health tables - Revert IF NOT EXISTS back to plain CREATE TABLE/CREATE INDEX, matching the existing schema statement convention - Use a const list spread (..._healthSchemaStatements) instead of a duplicated inline copy in _schemaStatements - :memory: overrides still resolve to temp files (needed for read-only secondary connections in tests), but now via an explicit Finalizer-based cleanup keyed on the constructor's _databasePathOverride parameter rather than sniffing the resulting path string Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Remove Finalizer mechanism and unused imports (dart:async) - Remove _tempDatabasePath and _generatedTempPath fields - Simplify init() to convert :memory: to temp files without tracking - Add deterministic tearDown() in test to close database and delete temp files - Verified: no temp file leaks, all 35 tests passing Closes: finding #5 from previous review Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires HealthDataSyncService into the composition root, guarded to only exist post-SQLite-cutover (mirrors the CoachToolService sqlQuery guard). Fired fire-and-forget from AppInitializer._initializeApp() alongside readiness.refresh() so it never blocks app startup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets the user force a Health Connect -> coach SQLite sync on demand from the Health Connect section, instead of waiting for the next app launch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… tables Extends the schema description in CoachToolService's run_sql_query declaration with health_samples, sleep_sessions, and sleep_stage_intervals so the coach LLM knows these tables exist and can join against them. Adds a test asserting the description text mentions the new tables (nothing else would catch a typo/omission there), plus a regression test for the join shape the coach will run.
…upgrade test - Remove auto-close block from init() that was closing database for any explicit file path, breaking coach_tool_service_test and other callers - Add explicit await upgraded.close() in upgrade test before file deletion - Regression: coach_tool_service_test now passes again - All related tests verified: sqlite_storage_service (35), coach_tool_service (11), health_data_sync_service (6), sql_query_service (10) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Skip syncing a health stream entirely when its HealthReadType isn't granted, and leave its watermark untouched — prevents watermarks from silently advancing to `now` on first launch before the user has opted into Health Connect, which was breaking the 90-day backfill for essentially every user. - Store health_samples/sleep_sessions timestamps as local time (.toLocal() before .toIso8601String()) to match the local-naive convention used by `sessions.date`, fixing day-bucketing joins for non-UTC users. - Wrap the already-migrated SQLite init() branch in main.dart with a Hive fallback, mirroring the fresh-migration branch, so a partial upgrade failure can't crash app startup. - Add IF NOT EXISTS to the health-schema DDL so a retried onUpgrade after a partial failure doesn't blow up on already-created tables. - Add missing tearDown to health_data_sync_service_test.dart to stop leaking temp db files, guard a profile_screen snackbar with mounted for consistency, and reset _initialized on close() so a close()+init() cycle actually reopens the connection. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
sqflite) as RepForge's persistence backend via a newSqliteStorageService implements IStorageService, with a one-time, flag-gated, reversibleStorageMigrationServicethat copies every entity from Hive on first launch post-update. Hive data is never deleted; the app automatically falls back to Hive on any migration failure.run_sql_querytool to the AI Coach's function-calling tool set, letting the model run arbitrary read-only SQL against the live database via a dedicated read-only connection, alongside the existing curated coach tools (kept, not replaced).run_sql_querycould read secrets (e.g. the user's Gemini API key) out of thesettingstable — now blocked by an identifier denylist.Design spec:
docs/superpowers/specs/2026-08-08-sqlite-migration-and-coach-sql-tool-design.mdImplementation plan:
docs/superpowers/plans/2026-08-08-sqlite-migration-and-coach-sql-tool.mdBuilt via subagent-driven development: 12 tasks, each with an isolated implementer + independent reviewer, plus a final whole-branch review that caught and fixed a credential-exposure issue and two reliability/correctness gaps before merge.
Test plan
flutter analyze— cleanflutter test— 924/924 passing (full suite, including all new tests for the migration and SQL tool)🤖 Generated with Claude Code