You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parent epic:#1446 (Amicus — AI Study Partner v1) Phase: 3 · Size: S · Depends on:#1461 (chip pool — closed), #1462 (FAB + peek — closed)
Client-side logic that decides which prompt chips to surface in the FAB peek based on the user's current screen context. Pure function — no LLM call, no network. Reads pre-computed chips from scripture.db and filters by profile variant.
Status — partial existing work
This card was originally spec'd as a three-file pure-function decomposition. During implementation the work collapsed into a single hook (app/src/hooks/useAmicusChips.ts) with the route-detection logic inlined in AmicusPeekSheet.tsx. The hook works and is in production, but the spec's decomposition goal was not achieved and two pieces of behavior from the spec were never built.
What's in the codebase now (Apr 21 2026)
Spec called for
Actual state
app/src/services/amicus/chipSelection.ts
❌ Not created. The selection logic is inside useAmicusChips.ts as the internal loadChips function (exported via _internal for testing).
❌ Not created. Logic lives inline in AmicusPeekSheet.tsx as useNavigationContext + findDeepestRoute, tightly coupled to useNavigationState.
app/src/hooks/useAmicusChips.ts
✅ Exists (4KB, imported from AmicusPeekSheet.tsx).
__tests__/chipSelection.test.ts
⚠️ Replaced by app/src/hooks/__tests__/useAmicusChips.test.tsx which covers variantLookupOrder and entityKeyFromContext via the _internal export. DB-hitting loadChips is not unit-tested.
__tests__/contextDetection.test.ts
❌ Not created. The inline route→context logic has zero test coverage.
Feature gaps (not just refactoring)
These are the reasons this card stays open, beyond the decomposition concern:
place context is a dead branch.ChipContext in the hook declares { kind: 'place'; placeId }, but useNavigationContext in PeekSheet only handles Chapter, PersonDetail, and DebateDetail routes. PlaceDetail and Map routes fall through to { kind: 'none' }. Place-scoped chips from the chip pool are currently unreachable.
Profile-variant selection was never implemented. The hook accepts a preferredVariant?: string parameter, but the one call site in AmicusPeekSheet.tsx (line 55: useAmicusChips(ctx)) passes nothing. The hook therefore always falls through to generic_balanced for every user regardless of their profile lean. The selectProfileVariant(profile) → variant function specified in the original card does not exist anywhere in the codebase.
Route-detection coupling makes testing harder. Because useNavigationContext is a hook that calls useNavigationState, unit tests of the route-to-context mapping have to mock React Navigation. A pure detectContextFromRoute(route) function would be trivially testable with plain route objects. (This also came up during review of PR Fix iOS map and Amicus crash paths #1562, which tried to refactor it via prop-drilling — that PR was closed; the right fix is the original spec's decomposition.)
Remaining work
Files to create
app/src/services/amicus/contextDetection.ts — export a pure function detectContextFromRoute(route) that maps a route object to ChipContext. Include PlaceDetail and Map route handling (currently missing from the inline PeekSheet version). The ChipContext type moves here too (re-export from useAmicusChips.ts for back-compat).
app/src/services/amicus/profileVariant.ts — export selectProfileVariant(profile: CompressedProfile): ProfileVariant. Deterministic selection over the 6 variants based on dominant tradition + dominant genre. Examples:
app/src/services/amicus/__tests__/contextDetection.test.ts — cover every route branch (Chapter, PersonDetail, PlaceDetail, Map, DebateDetail, unknown, malformed params, nested navigator state). Use plain route objects; no navigation mocks.
app/src/services/amicus/__tests__/profileVariant.test.ts — assert determinism (same profile → same variant) and the documented mapping examples. Cover edge cases (empty profile, tied leans, missing fields).
Files to modify
app/src/hooks/useAmicusChips.ts — re-export ChipContext from contextDetection.ts (single source of truth). No behavioral change to loadChips.
app/src/components/amicus/AmicusPeekSheet.tsx — replace inline useNavigationContext with const ctx = detectContextFromRoute(findDeepestRoute(state)). Read the user's compressed profile (via whatever hook surfaces it — check ai-partner: compressed profile generator #1452's integration), call selectProfileVariant(profile), and pass the result as the second arg to useAmicusChips(ctx, variant). Delete the local findDeepestRoute helper only if contextDetection.ts exposes its own (to avoid duplication).
Acceptance criteria
detectContextFromRoute lives in its own module as a pure function, imported by AmicusPeekSheet.tsx
PlaceDetail and Map routes produce { kind: 'place', placeId } — verified end-to-end (place screen → peek shows place-scoped chips)
selectProfileVariant exists, is deterministic, and is called from AmicusPeekSheet.tsx so non-default-leaning users get their variant's chips
contextDetection.test.ts exercises every switch branch + malformed inputs with plain objects
useAmicusChips.test.tsx continues to pass unchanged
No any types; lint clean; npx tsc --noEmit clean
Out of scope (deliberate)
Extracting loadChips into a separate pure selectChips module. It already hits the DB; splitting the pure filter/slice from the DB call is low-value given _internal.entityKeyFromContext + _internal.variantLookupOrder are already testable.
Changing the precached_prompts DB schema or the _tools/build_prompts.py generator.
Any changes to navigation architecture — this card is the decomposition + missing features, not a refactor of how navigation state flows.
References
Original spec (this card, pre-update): see revision history
Parent epic: #1446 (Amicus — AI Study Partner v1)
Phase: 3 · Size: S · Depends on: #1461 (chip pool — closed), #1462 (FAB + peek — closed)
Client-side logic that decides which prompt chips to surface in the FAB peek based on the user's current screen context. Pure function — no LLM call, no network. Reads pre-computed chips from
scripture.dband filters by profile variant.Status — partial existing work
This card was originally spec'd as a three-file pure-function decomposition. During implementation the work collapsed into a single hook (
app/src/hooks/useAmicusChips.ts) with the route-detection logic inlined inAmicusPeekSheet.tsx. The hook works and is in production, but the spec's decomposition goal was not achieved and two pieces of behavior from the spec were never built.What's in the codebase now (Apr 21 2026)
app/src/services/amicus/chipSelection.tsuseAmicusChips.tsas the internalloadChipsfunction (exported via_internalfor testing).app/src/services/amicus/contextDetection.ts(puredetectContextFromRoute)AmicusPeekSheet.tsxasuseNavigationContext+findDeepestRoute, tightly coupled touseNavigationState.app/src/hooks/useAmicusChips.tsAmicusPeekSheet.tsx).__tests__/chipSelection.test.tsapp/src/hooks/__tests__/useAmicusChips.test.tsxwhich coversvariantLookupOrderandentityKeyFromContextvia the_internalexport. DB-hittingloadChipsis not unit-tested.__tests__/contextDetection.test.tsFeature gaps (not just refactoring)
These are the reasons this card stays open, beyond the decomposition concern:
placecontext is a dead branch.ChipContextin the hook declares{ kind: 'place'; placeId }, butuseNavigationContextin PeekSheet only handlesChapter,PersonDetail, andDebateDetailroutes.PlaceDetailandMaproutes fall through to{ kind: 'none' }. Place-scoped chips from the chip pool are currently unreachable.Profile-variant selection was never implemented. The hook accepts a
preferredVariant?: stringparameter, but the one call site inAmicusPeekSheet.tsx(line 55:useAmicusChips(ctx)) passes nothing. The hook therefore always falls through togeneric_balancedfor every user regardless of their profile lean. TheselectProfileVariant(profile) → variantfunction specified in the original card does not exist anywhere in the codebase.Route-detection coupling makes testing harder. Because
useNavigationContextis a hook that callsuseNavigationState, unit tests of the route-to-context mapping have to mock React Navigation. A puredetectContextFromRoute(route)function would be trivially testable with plain route objects. (This also came up during review of PR Fix iOS map and Amicus crash paths #1562, which tried to refactor it via prop-drilling — that PR was closed; the right fix is the original spec's decomposition.)Remaining work
Files to create
app/src/services/amicus/contextDetection.ts— export a pure functiondetectContextFromRoute(route)that maps a route object toChipContext. IncludePlaceDetailandMaproute handling (currently missing from the inline PeekSheet version). TheChipContexttype moves here too (re-export fromuseAmicusChips.tsfor back-compat).app/src/services/amicus/profileVariant.ts— exportselectProfileVariant(profile: CompressedProfile): ProfileVariant. Deterministic selection over the 6 variants based on dominant tradition + dominant genre. Examples:tradition: "Reformed"+genre: "narrative"→"reformed_narrative"tradition: "Jewish"+focus: "Torah"→"jewish_pentateuch""generic_balanced"app/src/services/amicus/__tests__/contextDetection.test.ts— cover every route branch (Chapter, PersonDetail, PlaceDetail, Map, DebateDetail, unknown, malformed params, nested navigator state). Use plain route objects; no navigation mocks.app/src/services/amicus/__tests__/profileVariant.test.ts— assert determinism (same profile → same variant) and the documented mapping examples. Cover edge cases (empty profile, tied leans, missing fields).Files to modify
app/src/hooks/useAmicusChips.ts— re-exportChipContextfromcontextDetection.ts(single source of truth). No behavioral change toloadChips.app/src/components/amicus/AmicusPeekSheet.tsx— replace inlineuseNavigationContextwithconst ctx = detectContextFromRoute(findDeepestRoute(state)). Read the user's compressed profile (via whatever hook surfaces it — check ai-partner: compressed profile generator #1452's integration), callselectProfileVariant(profile), and pass the result as the second arg touseAmicusChips(ctx, variant). Delete the localfindDeepestRoutehelper only ifcontextDetection.tsexposes its own (to avoid duplication).Acceptance criteria
detectContextFromRoutelives in its own module as a pure function, imported byAmicusPeekSheet.tsxPlaceDetailandMaproutes produce{ kind: 'place', placeId }— verified end-to-end (place screen → peek shows place-scoped chips)selectProfileVariantexists, is deterministic, and is called fromAmicusPeekSheet.tsxso non-default-leaning users get their variant's chipscontextDetection.test.tsexercises every switch branch + malformed inputs with plain objectsprofileVariant.test.tsasserts determinism + documented mappinguseAmicusChips.test.tsxcontinues to pass unchangedanytypes; lint clean;npx tsc --noEmitcleanOut of scope (deliberate)
loadChipsinto a separate pureselectChipsmodule. It already hits the DB; splitting the pure filter/slice from the DB call is low-value given_internal.entityKeyFromContext+_internal.variantLookupOrderare already testable.precached_promptsDB schema or the_tools/build_prompts.pygenerator.References
app/src/hooks/useAmicusChips.tsapp/src/components/amicus/AmicusPeekSheet.tsx:237(useNavigationContext,findDeepestRoute)