Cache cue sheets so each route is enriched once (#30) #89

Merged
robert merged 1 commit from area/cue-sheet-cache into main 2026-09-04 14:04:45 +02:00
Owner

Closes #30.

Builds on tonight's merged #26 (TieredCueSheetEnricher/CueSheetOutcome, PR #79) and #25 (RouteLibrary/StoredRoute, PR #88).

What this adds

  • CueSheetCache (companion/core/.../route/enrich/CueSheetCache.kt): getOrEnrich(routeId, rawGpx = null) returns a cached CueSheetOutcome on a hit — no TieredCueSheetEnricher invocation at all, so no network call and no BRouter round-trip — and runs+caches the chain on a miss. forceReEnrich(routeId, rawGpx = null) always re-runs it (the "manual re-enrich, e.g. after installing BRouter" criterion). Both call RouteLibrary.updateEnrichment(), the hook #25 built precisely for this, so "which backend produced it" is recorded and already surfaces through the existing RouteLibraryScreen status line — no new display logic needed.

  • "Enricher version" = one pipeline-wide CURRENT_ENRICHER_VERSION: Int, not one per tier. The event that should invalidate a cached result is either a tier's logic changing or a previously-null tier slot getting wired into TieredCueSheetEnricher (#27/#28/#29 landing) — a single number covers both without inventing a second "pipeline composition" version. Cache rows are keyed by route id in storage, with the enricher version compared at read time (a mismatch is a miss, not a stored-but-invalid row that accumulates).

  • Persistence: a new Room table (cue_sheets), not extra columns on routes. RouteEntity already carries the summary (enrichmentStatus/enrichmentTier) the list UI needs cheaply; this table holds the actual cue payload, encoded via a new hand-rolled CueSheetCodec (mirrors RoutePointsCodec's existing precedent — no kotlinx-serialization dependency, one reader/one writer, JVM-tested round-trip). RouteDatabase bumps to schema version 2 with a real Migration(1, 2) (a single CREATE TABLE) rather than fallbackToDestructiveMigration(), even though nothing has shipped to a device outside this project's own development yet — the correct habit costs nothing here.

  • The rawGpx gap is stated honestly, not papered over. Tier 0 (GpxCueEnricher) needs the original GPX text; nothing in the route library persists that today — StoredRoute/RouteEntity carry only the parsed, simplified geometry, and GpxImporter's raw text is discarded the moment parsing finishes. getOrEnrich/forceReEnrich accept an optional rawGpx; when absent it becomes "" and GpxCueEnricher fails closed to no cues exactly as it already does for malformed source text (see GpxCueEnricherTest), falling through to tier 1+. This is not a regression — no caller anywhere in the codebase can supply rawGpx post-import yet — and the concrete scenario the issue names ("after installing BRouter") is a tier-1 case that needs no rawGpx at all. Persisting rawGpx (a schema change to StoredRoute/RouteEntity) is out of scope here and flagged as a real follow-up if a future issue needs tier 0 retried after import.

  • No automatic "enrich at import" or "ride start" trigger is wired. Neither exists in the app yet (RouteLibrary.importRoute still starts every route at PENDING, per #25's own honesty note; there is no ride-start flow at all). This issue is scoped to exposing the on-demand "get cached, enrich only on miss" API a future trigger will call — plus a small UI hookup for the acceptance criterion that explicitly asks for manual re-enrich today: a "Re-enrich" button on RouteDetailScreen (RouteLibraryScreen.kt), wired via RouteStore.cueSheetCache(context).

Tests

JVM, :companion:core, all passing (96 tests total across the module, 13 new):

  • CueSheetCacheTest — cache hit (enricher never invoked), cache miss, version-bump invalidation, forceReEnrich bypass, unknown-route-id handling (no storage/enricher touch), the optional-rawGpx default, and one end-to-end run against the real GpxCueEnricher + the real bikerouter-style-cues.gpx fixture (not just fakes).
  • CueSheetCodecTest — encode/decode round-trip, including a street name containing both separator characters.

./gradlew :companion:core:test — green.

Verification

Host-verified: everything under :companion:core (./gradlew :companion:core:test, using a JBR JDK as JAVA_HOME since this sandbox's system JRE has no compiler).

Hand-reviewed only, not compiled: everything under :companion:route — CueSheetEntity/CueSheetDao/RoomCueSheetCacheStorage, the RouteDatabase migration, and the RouteStore/RouteLibraryScreen/RouteLibraryActivity wiring. No Android SDK in this sandbox, the same gap every companion PR this session has carried since #25.

No Gradle dependency was added or bumped — Room 2.8.4 was already pinned by #25. The Migration/SupportSQLiteDatabase API used in the new migration was checked against developer.android.com's current Room migration guide (not assumed from memory).

https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt

Closes #30. Builds on tonight's merged #26 (`TieredCueSheetEnricher`/`CueSheetOutcome`, PR #79) and #25 (`RouteLibrary`/`StoredRoute`, PR #88). ### What this adds - **`CueSheetCache`** (`companion/core/.../route/enrich/CueSheetCache.kt`): `getOrEnrich(routeId, rawGpx = null)` returns a cached `CueSheetOutcome` on a hit — no `TieredCueSheetEnricher` invocation at all, so no network call and no BRouter round-trip — and runs+caches the chain on a miss. `forceReEnrich(routeId, rawGpx = null)` always re-runs it (the "manual re-enrich, e.g. after installing BRouter" criterion). Both call `RouteLibrary.updateEnrichment()`, the hook #25 built precisely for this, so "which backend produced it" is recorded and already surfaces through the existing `RouteLibraryScreen` status line — no new display logic needed. - **"Enricher version" = one pipeline-wide `CURRENT_ENRICHER_VERSION: Int`, not one per tier.** The event that should invalidate a cached result is either a tier's logic changing *or* a previously-`null` tier slot getting wired into `TieredCueSheetEnricher` (#27/#28/#29 landing) — a single number covers both without inventing a second "pipeline composition" version. Cache rows are keyed by route id in storage, with the enricher version compared at read time (a mismatch is a miss, not a stored-but-invalid row that accumulates). - **Persistence: a new Room table (`cue_sheets`), not extra columns on `routes`.** `RouteEntity` already carries the *summary* (`enrichmentStatus`/`enrichmentTier`) the list UI needs cheaply; this table holds the actual cue payload, encoded via a new hand-rolled `CueSheetCodec` (mirrors `RoutePointsCodec`'s existing precedent — no `kotlinx-serialization` dependency, one reader/one writer, JVM-tested round-trip). `RouteDatabase` bumps to schema version 2 with a real `Migration(1, 2)` (a single `CREATE TABLE`) rather than `fallbackToDestructiveMigration()`, even though nothing has shipped to a device outside this project's own development yet — the correct habit costs nothing here. - **The `rawGpx` gap is stated honestly, not papered over.** Tier 0 (`GpxCueEnricher`) needs the *original* GPX text; nothing in the route library persists that today — `StoredRoute`/`RouteEntity` carry only the parsed, simplified geometry, and `GpxImporter`'s raw text is discarded the moment parsing finishes. `getOrEnrich`/`forceReEnrich` accept an optional `rawGpx`; when absent it becomes `""` and `GpxCueEnricher` fails closed to no cues exactly as it already does for malformed source text (see `GpxCueEnricherTest`), falling through to tier 1+. This is not a regression — no caller anywhere in the codebase can supply `rawGpx` post-import yet — and the concrete scenario the issue names ("after installing BRouter") is a tier-1 case that needs no `rawGpx` at all. Persisting `rawGpx` (a schema change to `StoredRoute`/`RouteEntity`) is out of scope here and flagged as a real follow-up if a future issue needs tier 0 retried after import. - **No automatic "enrich at import" or "ride start" trigger is wired.** Neither exists in the app yet (`RouteLibrary.importRoute` still starts every route at `PENDING`, per #25's own honesty note; there is no ride-start flow at all). This issue is scoped to exposing the on-demand "get cached, enrich only on miss" API a future trigger will call — plus a small UI hookup for the acceptance criterion that explicitly asks for *manual* re-enrich today: a "Re-enrich" button on `RouteDetailScreen` (`RouteLibraryScreen.kt`), wired via `RouteStore.cueSheetCache(context)`. ### Tests JVM, `:companion:core`, all passing (96 tests total across the module, 13 new): - `CueSheetCacheTest` — cache hit (enricher never invoked), cache miss, version-bump invalidation, `forceReEnrich` bypass, unknown-route-id handling (no storage/enricher touch), the optional-`rawGpx` default, and one end-to-end run against the real `GpxCueEnricher` + the real `bikerouter-style-cues.gpx` fixture (not just fakes). - `CueSheetCodecTest` — encode/decode round-trip, including a street name containing both separator characters. `./gradlew :companion:core:test` — green. ### Verification **Host-verified:** everything under `:companion:core` (`./gradlew :companion:core:test`, using a JBR JDK as `JAVA_HOME` since this sandbox's system JRE has no compiler). **Hand-reviewed only, not compiled:** everything under `:companion:route` — `CueSheetEntity`/`CueSheetDao`/`RoomCueSheetCacheStorage`, the `RouteDatabase` migration, and the `RouteStore`/`RouteLibraryScreen`/`RouteLibraryActivity` wiring. No Android SDK in this sandbox, the same gap every companion PR this session has carried since #25. No Gradle dependency was added or bumped — Room 2.8.4 was already pinned by #25. The `Migration`/`SupportSQLiteDatabase` API used in the new migration was checked against developer.android.com's current Room migration guide (not assumed from memory). https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
Cache cue sheets so each route is enriched once (#30)
Some checks failed
dev-artifact / build-pbw (push) Has been cancelled
dev-artifact / build-apk (push) Has been cancelled
fast-lane / host-c-tests (push) Has been cancelled
fast-lane / jvm-tests (push) Has been cancelled
fast-lane / pebble-build (push) Has been cancelled
fast-lane / lint-and-secrets (push) Has been cancelled
fast-lane / meta-declares-required-jobs (push) Has been cancelled
fast-lane / host-c-tests (pull_request) Has been cancelled
fast-lane / jvm-tests (pull_request) Has been cancelled
fast-lane / pebble-build (pull_request) Has been cancelled
fast-lane / lint-and-secrets (pull_request) Has been cancelled
fast-lane / meta-declares-required-jobs (pull_request) Has been cancelled
dev-artifact / publish (push) Has been cancelled
c42fcab819
Adds the caching/invalidation layer on top of #26's TieredCueSheetEnricher
and #25's RouteLibrary, per issue #30's acceptance criteria:

- CueSheetCache (companion/core, route/enrich/CueSheetCache.kt) orchestrates
  "enrich once, cache, reuse": getOrEnrich() returns a cached CueSheetOutcome
  on a hit (no enricher call at all - no network, no BRouter round-trip) and
  runs+caches the tiered chain on a miss; forceReEnrich() always re-runs it
  (the "manual re-enrich, e.g. after installing BRouter" criterion). Both
  call RouteLibrary.updateEnrichment(), the hook #25 built precisely for this,
  so which backend won is recorded and already displayed by the existing
  RouteLibraryScreen status line - no new display logic needed there.

- CURRENT_ENRICHER_VERSION is one pipeline-wide version, not one per tier:
  the event that should invalidate a cached result is either a tier's logic
  changing *or* a previously-null tier slot getting wired into
  TieredCueSheetEnricher (#27/#28/#29 landing), and a single number covers
  both without inventing a second "pipeline composition version". Cache
  entries are keyed by route id (storage) with the enricher version compared
  at read time (a mismatch is a miss, not a stored-but-invalid row).

- Persistence: a new Room table (cue_sheets, CueSheetEntity/CueSheetDao/
  RoomCueSheetCacheStorage in :companion:route), not extra columns on the
  existing routes table - RouteEntity already carries the *summary*
  (enrichmentStatus/enrichmentTier) the list UI needs cheaply; this table
  holds the actual cue payload, encoded via a new hand-rolled CueSheetCodec
  (companion/core, mirrors RoutePointsCodec's precedent, JVM-tested).
  RouteDatabase bumps to schema version 2 with a real Migration(1, 2) rather
  than fallbackToDestructiveMigration(), even though nothing has shipped to
  a device outside this project's own development yet.

- The rawGpx gap is stated honestly in CueSheetCache's KDoc rather than
  papered over: tier 0 (GpxCueEnricher) needs the original GPX text, which
  nothing in the route library persists today (StoredRoute/RouteEntity carry
  only the parsed, simplified geometry). getOrEnrich/forceReEnrich accept an
  optional rawGpx; when absent it becomes "" and GpxCueEnricher fails closed
  to no cues exactly as it already does for malformed source text, falling
  through to tier 1+. This is not a regression - no caller anywhere in the
  codebase can supply rawGpx post-import yet - and the concrete scenario the
  issue names ("after installing BRouter") is a tier-1 case that needs no
  rawGpx at all.

- No automatic "enrich at import" or "ride start" trigger is wired: neither
  exists in the app yet (RouteLibrary.importRoute still starts every route
  at PENDING, per #25's own honesty note), so this issue is scoped to
  exposing the on-demand "get cached, enrich only on miss" API a future
  trigger will call, plus a small manual UI hookup (a "Re-enrich" button on
  RouteDetailScreen) for the acceptance criterion that explicitly asks for
  manual re-enrich today.

Tests (JVM, :companion:core, all passing - 96 tests across the module,
13 new): CueSheetCacheTest covers cache hit/miss, version-bump invalidation,
forceReEnrich bypass, unknown-route-id handling, the optional-rawGpx default,
and one end-to-end run against the real GpxCueEnricher + a real fixture
(bikerouter-style-cues.gpx). CueSheetCodecTest covers the encode/decode
round-trip including separator-bearing street names.

Host-verified: everything under :companion:core (./gradlew :companion:core:test,
JBR JDK 25 as JAVA_HOME - this sandbox's system JRE lacks a compiler).
Hand-reviewed only, not compiled here: everything under :companion:route
(CueSheetEntity/CueSheetDao/RoomCueSheetCacheStorage, the RouteDatabase
migration, RouteStore/RouteLibraryScreen/RouteLibraryActivity wiring) - no
Android SDK in this sandbox, same gap every companion PR this session has
carried since #25.

No Gradle dependency was added or bumped; Room 2.8.4 was already pinned by
#25, and the classic Migration/SupportSQLiteDatabase API used here was
checked against developer.android.com's current Room migration guide.

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert merged commit 4f839ebb13 into main 2026-09-04 14:04:45 +02:00
Sign in to join this conversation.
No description provided.