feat(explore): on-device natural-language search, phase 1 (core plus dives) - #2196
ericgriffin wants to merge 33 commits into
Conversation
Program spec for the Explore feature: an onboard platform model turns a sentence into a small clause list, a pure-Dart compiler grounds and lowers it to the existing filter models, and a dedicated page shows the compiled filter as chips, charts and linked results. Phase 1 (core plus dives) is fully specified; phase 2 (profile-derived predicates) and phase 3 (other subjects) are scoped.
Seventeen TDD tasks covering the query model, field catalog and unit grounding, time grammar, name index and resolver, the four new filter axes through all three filter paths with a parity test, the compiler, the submersion_nl channel package with Apple and Android adapters, gate and query providers, recent queries in the local cache database, localized strings, filter sheet and chip bar coverage, the Explore page, the route and entry points, and whole-project verification with a manual smoke checklist.
…ated list with parity
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved filtering, gating, concurrency, persistence, and native-model issues block approval.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (5)
What changed in this PR
Adds Phase 1 of Explore: on-device natural-language dive search with deterministic filtering, charts, results, and handoffs.
Changes:
- Adds Apple and Android native model adapters with availability gating.
- Adds query parsing, compilation, entity resolution, charts, and recent queries.
- Extends dive filtering across SQL, in-memory evaluation, statistics, and UI.
- Adds localized Explore screens, shortcuts, routes, and comprehensive tests.
| File | Summary |
|---|---|
test/l10n/explore_strings_test.dart |
Localization coverage. |
test/features/explore/presentation/providers/explore_providers_test.dart |
Explore provider behavior. |
test/features/explore/presentation/providers/explore_gate_providers_test.dart |
Availability gate behavior. |
test/features/explore/presentation/chip_labeler_test.dart |
Chip formatting. |
test/features/explore/domain/unit_grounding_test.dart |
Unit conversion. |
test/features/explore/domain/time_grammar_test.dart |
Relative time parsing. |
test/features/explore/domain/query_model_test.dart |
Query schema validation. |
test/features/explore/domain/nl_prompt_test.dart |
Prompt generation. |
test/features/explore/domain/entity_resolver_test.dart |
Entity resolution. |
test/features/explore/domain/dive_field_catalog_test.dart |
Field catalog behavior. |
test/features/explore/domain/chart_selection_test.dart |
Chart selection rules. |
test/features/explore/data/recent_query_repository_test.dart |
Recent-query persistence. |
test/features/explore/data/name_index_builder_test.dart |
Name index construction. |
test/features/explore/data/explore_repository_test.dart |
Explore repository queries. |
test/features/explore/data/channel_nl_engine_test.dart |
Native channel integration. |
test/features/dive_log/presentation/widgets/dive_list_active_filter_chips_test.dart |
Active filter chips. |
test/features/dive_log/presentation/widgets/dive_filter_sheet_interactions_test.dart |
Filter-sheet interactions. |
test/features/dive_log/presentation/widgets/dive_filter_sheet_explore_axes_test.dart |
Explore filter axes. |
test/features/dive_log/presentation/pages/dive_list_explore_entry_test.dart |
Explore entry points. |
test/features/dive_log/domain/models/dive_filter_state_test.dart |
In-memory filter evaluation. |
test/features/dive_log/data/repositories/dive_repository_explore_axes_filter_test.dart |
Repository filter parity. |
test/core/router/app_router_test.dart |
Explore route coverage. |
test/architecture/repository_tick_stream_test.dart |
Repository tick behavior. |
pubspec.yaml |
Dependency configuration. |
pubspec.lock |
Dependency lock updates. |
packages/submersion_nl/pubspec.yaml |
Native package configuration. |
packages/submersion_nl/lib/submersion_nl.dart |
Method and event channel wrapper. |
packages/submersion_nl/darwin/submersion_nl.podspec |
Apple plugin configuration. |
packages/submersion_nl/darwin/Classes/SubmersionNlPlugin.swift |
Apple model adapter. |
packages/submersion_nl/android/src/main/kotlin/app/submersion/nl/SubmersionNlPlugin.kt |
Android model adapter. |
packages/submersion_nl/android/src/main/AndroidManifest.xml |
Android plugin manifest. |
packages/submersion_nl/android/build.gradle |
Android plugin build configuration. |
macos/Podfile.lock |
macOS dependency lock updates. |
macos/Flutter/GeneratedPluginRegistrant.swift |
macOS plugin registration. |
lib/l10n/arb/app_zh.arb |
Chinese Explore strings. |
lib/l10n/arb/app_pt.arb |
Portuguese Explore strings. |
lib/l10n/arb/app_nl.arb |
Dutch Explore strings. |
lib/l10n/arb/app_it.arb |
Italian Explore strings. |
lib/l10n/arb/app_hu.arb |
Hungarian Explore strings. |
lib/l10n/arb/app_he.arb |
Hebrew Explore strings. |
lib/l10n/arb/app_fr.arb |
French Explore strings. |
lib/l10n/arb/app_es.arb |
Spanish Explore strings. |
lib/l10n/arb/app_en.arb |
English Explore strings. |
lib/l10n/arb/app_de.arb |
German Explore strings. |
lib/l10n/arb/app_ar.arb |
Arabic Explore strings. |
lib/features/statistics/data/dive_filter_sql.dart |
Statistics filter SQL. |
lib/features/explore/presentation/widgets/explore_results_list.dart |
Explore result rendering. |
lib/features/explore/presentation/widgets/explore_chip_rows.dart |
Query chip rows and editing. |
lib/features/explore/presentation/widgets/explore_charts.dart |
Explore chart cards. |
lib/features/explore/presentation/providers/explore_gate_providers.dart |
Model availability providers. |
lib/features/explore/presentation/pages/explore_page.dart |
Explore page and model flow. |
lib/features/explore/presentation/chip_labeler.dart |
Query chip labels. |
lib/features/explore/domain/unit_grounding.dart |
Unit grounding. |
lib/features/explore/domain/time_grammar.dart |
Time grammar. |
lib/features/explore/domain/nl_engine.dart |
Natural-language engine abstraction. |
lib/features/explore/domain/name_index.dart |
Entity name index. |
lib/features/explore/domain/entity_resolver.dart |
Entity matching. |
lib/features/explore/domain/dive_field_catalog.dart |
Dive field definitions. |
lib/features/explore/domain/compiled_query.dart |
Compiled query models. |
lib/features/explore/domain/chart_selection.dart |
Chart selection rules. |
lib/features/explore/data/recent_query_repository.dart |
Recent-query persistence. |
lib/features/explore/data/explore_repository.dart |
Explore aggregate queries. |
lib/features/explore/data/channel_nl_engine.dart |
Channel-backed engine. |
lib/features/dive_log/presentation/widgets/dive_list_content.dart |
Dive-list actions and filters. |
lib/features/dive_log/presentation/widgets/active_filter_chip_labels.dart |
Active filter labels. |
lib/features/dive_log/presentation/providers/dive_providers.dart |
Dive provider invalidation. |
lib/features/dive_log/presentation/pages/dive_list_page.dart |
Dive-list Explore entry. |
lib/features/dive_log/domain/models/dive_filter_state.dart |
Extended filter state. |
lib/features/dive_log/data/repositories/dive_repository_impl.dart |
Dive filtering and change ticks. |
lib/core/router/app_router.dart |
Explore route registration. |
lib/core/database/local_cache_database.dart |
Recent-query schema. |
lib/core/accessibility/app_shortcuts.dart |
Explore keyboard shortcut. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
📦 Build artifacts for this PR · commit
Artifacts expire in 7 days. Downloading requires being signed in to GitHub. macOS needs two extractions: unzip the downloaded artifact, then unzip the Updated automatically on each push. |
…and decoding - A strict operator now lowers to the inclusive bound the filter actually applies, and the chip says so, so a chip can never claim a query was strict while a dive on the threshold matches it. - The availability gate stays closed while a probe is in flight, so a locale change cannot leave the entry point open on the previous locale's answer. - A decoded JSON root that is not an object raises a schema mismatch instead of a TypeError that left the notifier running forever. - Explore chips are removable, not editable in place: the sentence's parse is the single source of truth, and a filter-sheet edit could not be reflected back into the chips. The handoffs read the published filter. Adds regression tests for each, plus database-backed tests for the result, count and chart providers and branch tests for the name index builder, chip labels and chart cards.
…-nlang-search-2cdf94
… singular
gen-l10n compiles an ARB `=1` branch into the CLDR `one` category, and
French and Portuguese put zero in that category, so `=1{1 plongee}` would
report one dive when the count is zero. Both branches now interpolate
their argument, matching the convention the plural guards on main enforce.
The hero test offset today by an elapsed-time Duration, which is not the same thing as a calendar day. US daylight saving ends on 2026-11-01, so from 2026-09-20 the "+43 days" trip end landed on 2026-11-01 23:00 rather than 2026-11-02: today = 2026-09-20 00:00 start = 2026-10-30 00:00 end = 2026-11-01 23:00 The trip then spanned three calendar days, the hero generated three itinerary days, and "tapping Generate itinerary saves generated days" expected four. The failure was purely a function of the date the suite ran on, and it reddened main and every open PR for the duration of the window. Build the dates with DateTime(y, m, d + n) instead, letting the constructor normalise the overflowing day field. Ten date expressions in the file shared the pattern, so four other trips were latent. Closes #2207
ItineraryDay.generateForTrip already counts calendar days rather than elapsed hours, but nothing pinned a window that crosses a transition: the existing cases only cross one by accident of the dates they chose, and a window built from DateTime.now() plus an offset crosses one for only part of the year. Add a fall-back window (2026-10-30 to 2026-11-02, a 25-hour local day) and a spring-forward window (2027-03-12 to 2027-03-15, a 23-hour one), both at explicit calendar dates. Each asserts the generated dates, not just the count, so the per-day DateTime(y, m, d + i) arithmetic is held in place alongside the total. Refs #2207
Two trip screens counted days with Duration.inDays on local DateTime values instead of the DST-safe helpers Trip already exposes. The edit page's duration label read the trip range in elapsed hours, so a liveaboard from 2027-03-12 to 2027-03-15 (71 hours, because DST starts on 2027-03-14) displayed "3 days" for a 4-day trip, contradicting the 4-day itinerary generated from the same range. The upcoming-trip countdown measured from the current instant rather than from today's date, so it floored away the rest of the current day: a trip four calendar days out read "In 3 days" at any hour past midnight, and a spring-forward inside the window took a further day. Route both through the existing UTC date-only arithmetic, promoting trip.dart's private calendarDaysBetween so the edit page shares one implementation with Trip.durationDays, and replacing the countdown expression with Trip.daysUntilStart. Both new tests were confirmed to fail against the previous code. Closes #2216
…-nlang-search-2cdf94 # Conflicts: # test/features/trips/presentation/widgets/story/trip_story_hero_test.dart
…-nlang-search-2cdf94 # Conflicts: # test/features/trips/presentation/widgets/trip_summary_widget_test.dart
Ten TDD tasks: the derived metric value types and the pure engine that reads final-stop stability and bucketed SAC off a profile, two device-local engine-versioned tables with their migration rung, the isolate worker and its compute-through-cache repository, a single-flight scheduler copied from the sensor summary one, the predicate types with the single SQL builder all three filter paths share, the filter axis with a four-way parity test, the schema v2 compiler fields and prompt, and the dive detail read path.
…-nlang-search-2cdf94 # Conflicts: # lib/features/trips/domain/entities/trip.dart # test/features/trips/domain/entities/itinerary_day_test.dart # test/features/trips/presentation/pages/trip_edit_page_test.dart # test/features/trips/presentation/widgets/trip_summary_widget_test.dart



Related Issue
Refs #2195 (phase 1 of 3; phases 2 and 3 follow in their own PRs)
Summary
Phase 1 of Explore: an onboard platform model turns a sentence such as "Turtles below 20m in Bonaire with viz over 20m" into a small clause list, and pure Dart compiles that into the existing
DiveFilterState, rendered as editable chips, the words it could not place, a count, up to three charts and the matching dives, with handoffs into the dive list and Statistics. Only the sentence reaches the model; every filter, count and chart is computed from the local database. The entry point is hidden wherever no model exists for the device or the active locale.Spec:
docs/superpowers/specs/2026-09-19-explore-natural-language-search-design.md. Plan:docs/superpowers/plans/2026-09-19-explore-phase1-core-dives.md.Changes
packages/submersion_nl: aMethodChannelpackage with an Apple Foundation Models adapter (guided generation from a schema shipped by Dart, iOS 26 / macOS 26, weak-linked so older targets report not eligible) and an Android adapter over the ML Kit GenAI Prompt API (prompt-only JSON, English and Korean).lib/features/explore/domain: the versioned query model with JSON validation, the dive field catalog, unit grounding against the diver's settings, a deterministic time grammar, a name index and Dice-similarity resolver, rule-based chart selection, and the compiler that lowers a parsed query toDiveFilterState.DiveFilterStategains water temperature, visibility, water type, species and site-set axes, evaluated in Statistics SQL, the paginated list SQL andapply()with a three-path parity test; a sightings change tick keeps species-filtered lists fresh./dives/explorewith understood and needs-attention chip rows, chart cards reusingDiveTrendChartandHorizontalCategoryBarChart, results viaCompactDiveListTile, and handoff buttons; a gated app bar action on every dive list app bar and a keyboard shortcut.Test Plan
flutter testpasses (30,541 tests, 21 pre-existing skips)flutter analyzepassesScreenshots
Owed with the manual smoke.