Skip to content

feat(explore): on-device natural-language search, phase 1 (core plus dives) - #2196

Open
ericgriffin wants to merge 33 commits into
mainfrom
ericgriffin/on-device-nlang-search-2cdf94
Open

ericgriffin wants to merge 33 commits into
mainfrom
ericgriffin/on-device-nlang-search-2cdf94

Conversation

@ericgriffin

Copy link
Copy Markdown
Member

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: a MethodChannel package 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 to DiveFilterState.
  • DiveFilterState gains water temperature, visibility, water type, species and site-set axes, evaluated in Statistics SQL, the paginated list SQL and apply() with a three-path parity test; a sightings change tick keeps species-filtered lists fresh.
  • Gate providers (sync platform gate plus async availability probe), a name index builder over the entity repositories, the Explore query notifier, result, count and chart data providers, and recent queries in the local cache database (schema 18).
  • The Explore page at /dives/explore with understood and needs-attention chip rows, chart cards reusing DiveTrendChart and HorizontalCategoryBarChart, results via CompactDiveListTile, and handoff buttons; a gated app bar action on every dive list app bar and a keyboard shortcut.
  • The filter sheet gains sections for the new axes, and the dive list's active-filter bar now shows a chip for every axis, so a handoff never lands on a list whose filter is invisible.
  • 90 new strings in all 11 locales.

Test Plan

  • flutter test passes (30,541 tests, 21 pre-existing skips)
  • flutter analyze passes
  • Manual testing on: macOS 26 with Apple Intelligence (owed; the app builds with Xcode 27 but the model was not exercised on hardware), Android with AICore (owed; the debug APK builds)

Screenshots

Owed with the manual smoke.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 High severity · 3 Medium severity · 1 Low severity

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.

Comment thread lib/features/explore/domain/query_compiler.dart
Comment thread lib/features/explore/presentation/providers/explore_gate_providers.dart Outdated
Comment thread lib/features/explore/presentation/providers/explore_providers.dart Outdated
Comment thread lib/features/explore/presentation/widgets/explore_chip_rows.dart Outdated
Comment thread lib/features/dive_log/presentation/widgets/dive_list_content.dart
@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build artifacts for this PR · commit 5de7439

Platform Download
Android (APK) android-apk
macOS macos-build
Windows windows-build
Linux linux-build

Artifacts expire in 7 days. Downloading requires being signed in to GitHub. macOS needs two extractions: unzip the downloaded artifact, then unzip the submersion-macos.zip inside it to get a runnable submersion.app. The build is ad-hoc signed — right-click → Open on first launch.

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.
… 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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

2 participants