← all branches

feat/discovery

needs attentionviewing older commit
5d3175c · incrementalPR #305reviewed 2026-07-17 19:31 UTC3H · 6M · 4L · 1I
The branch
Purpose
Feature Discovery v1 — contextual onboarding for existing Batu platform users. Flag-gated (feature-discovery-spotlights, OFF by default).
Goal
Ambient spotlight coachmarks, Descubre hub, and first-run guided tour. Pure decision/read-coordinator FCIS throughout.
Sub-goals
  • Ambient spotlights: one contextual coachmark per page, paced ≤1/rolling-day, retire-on-dismiss
  • Descubre hub: header badge + self-serve pull + 'Volver a tomar el tour'
  • First-run linear product tour: 5 steps, dimmed scrim + spotlight cutout, skippable
  • discovery_state table (user-scoped RLS, migration 0062)
  • FCIS throughout: DiscoveryFCIS namespace, JSend API, es/en i18n, PostHog analytics
The changes (whole branch)
What
Bug-fix sweep (last 2 commits): pull-card blink fix (keepPreviousData + isPlaceholderData guard), add_contract routing through tutorialHref instead of ?descubre, timer leak fix (timers[] array in TourOverlay), parked-state reset on RpuSubTour re-open, latch→pick migration (eligibility-based lifetime vs budget-based), isFirstRunTourPending extracted as public exported decision function, migration renumbered 0061→0062.
Why
Previous review found prod bugs invisible to preview dogfooding (budget relaxed on previews). This diff addresses the remaining items from the loop-review comment on d3f875d.
Areas
apps/platform+242350domains/cross-domain+15430domains/core+4980packages/secrets+15212packages/database+1381packages/api+1030packages/analytics+920e2e/platform+570scripts+300
Blast
60 files, +5037/-63 lines across the branch. Scoped to discovery feature (flag-gated, no prod user impact until flag flip). Migration 0062 is additive (new table only).
feature-discovery-spotlights (flag-gated, OFF by default) deploy precondition: PostHog flag + organizations.segment reclassify script
typecheck· not checked in this runci· CI status not accessible via tokencoderabbit· no .coderabbit.yaml

Findings · 15

correctness1

medium

?descubre strip fires on non-200 response, silently losing the pull

apps/platform/src/components/discovery/FeatureSpotlight.tsx:170

The useEffect that removes ?descubre fires when `requested && response && !isPlaceholderData` — it does NOT require response.status === 200. On a 500 error, ok is false so no card shows, but the param is deleted. The pull is lost silently with no retry path. Fix: add `&& ok` (or `&& response.status === 200`) to the guard.

security1

low

No DELETE RLS policy on discovery_state for authenticated users

packages/database/drizzle/0062_discovery_state.sql:26

SELECT/INSERT/UPDATE policies exist for 'authenticated'; DELETE is missing. RLS default-deny means users cannot self-delete rows through the API. service_role ALL covers server-side deletes. FK cascade on profile_id handles lifecycle cleanup. Worth documenting the intentional omission if a self-service reset path is planned.

conventions6

high

Test passes `exposures` to `listEligibleKeysOnPage` which doesn't accept it

domains/cross-domain/src/__tests__/feature-discovery.decisions.test.ts:413

The test helper passes an `exposures` property to listEligibleKeysOnPage, but the function signature has no such parameter. TypeScript's excess property check on object literals flags this as a compile error. Either the signature was trimmed without updating tests, or the param was supposed to be added. Will fail tsc in strict mode.

medium

`isFirstRunTourPending` placed in cross-domain instead of core discovery decisions

domains/cross-domain/src/feature-discovery.decisions.ts:222

The predicate operates solely on DiscoveryState rows (core-domain entity) with no cross-domain dependency. Per canonical form, single-domain predicates belong in the owning domain's decisions file (domains/core/src/discovery/discovery.decisions.ts). The shortcut avoids a circular import concern but inverts the dependency direction: core shells now implicitly depend on cross-domain.

medium

setState during render comment overstates the no-repaint guarantee

apps/platform/src/components/discovery/FeatureSpotlight.tsx:144

Comment says setPick during render avoids 'a paint flash'. React's guarantee is synchronous re-render in the same pass only before commit; the convergence guard is necessary. The comment could mislead maintainers into dropping the guard. The code is correct; the comment needs tightening.

medium

Double blank line left after isFirstRunTourPending removal

domains/cross-domain/src/feature-discovery.shells.ts:87

Two consecutive blank lines between FeatureDiscoveryErrors closing brace and the shell section comment — stray artifact from the extraction. Project convention is one blank line between top-level declarations.

medium

Double blank line left after isFirstRunTourPending removal

domains/cross-domain/src/feature-discovery.shells.ts:87

Duplicate of above — de-deduplicated in counts.

low

7-line JSDoc on isFirstRunTourPending exceeds project altitude

domains/cross-domain/src/feature-discovery.decisions.ts:215

Per CLAUDE.md: no multi-paragraph docstrings. The architectural placement rationale belongs in the cross-domain CLAUDE.md caveats file, not in the function JSDoc. Trim to the function contract.

tests5

high

No regression test for pull-card blink fix (keepPreviousData + isPlaceholderData)

apps/platform/src/components/discovery/FeatureSpotlight.tsx:129

The pull-card blink was a production bug: response going undefined during refetch caused firstRunTourPending to flip true and blink the card; ?descubre stripped before the pulled key was picked. Fixed with keepPreviousData + isPlaceholderData guard. No test pins this interaction. A mock simulating isPlaceholderData=true should assert the strip does NOT fire and firstRunTourPending stays stable.

high

No regression test for add_contract routing through tutorialHref

apps/platform/src/components/discovery/DescubreMenu.tsx:61

The bug was that add_contract routed to ?descubre=add_contract (ambient coachmark) instead of the RPU sub-tour. Fixed by routing through tutorialHref(next). No test asserts tutorialHref({ key: 'add_contract', ... }) returns a subtour URL. High consequence: wrong surface = confusing UX for the highest-intent moment in the discovery flow.

medium

No regression test for timer-leak fix in TourOverlay

apps/platform/src/components/discovery/TourOverlay.tsx:93

The timers[] fix prevents in-flight retries from firing after cleanup. A vi.useFakeTimers() test that triggers cleanup mid-retry and asserts no further state mutations would pin this regression. Without it, the bug is invisible to TypeScript.

medium

No regression test for parked-state reset in RpuSubTour on re-open

apps/platform/src/components/discovery/RpuSubTour.tsx:86

Bug: parked=true from a prior session persisted into a fresh re-open. Fixed with setParked(false) in the open effect. No test covers the re-open scenario: open tour → park → close → reopen → assert parked=false and step=0.

info

SpotlightCoachmark may have the same single-timer leak pattern as TourOverlay

apps/platform/src/components/discovery/FeatureSpotlight.tsx:282

SpotlightCoachmark has a retry loop with `let timer = window.setTimeout(...)`. If it reassigns `timer` across retries (same pattern as the TourOverlay bug), only the last timer is cleared on cleanup. This diff fixed TourOverlay but not SpotlightCoachmark. Worth confirming the structure is safe and, if not, applying the same timers[] fix.

improvement2

low

Three scattered ternaries for the same `ok` guard could be one block

apps/platform/src/components/discovery/FeatureSpotlight.tsx:133

resolvedKey, eligibleKeys, and firstRunTourPending are all `ok ? <real> : <default>`. A single destructure block would make loading/error defaults visible as a unit. Minor readability improvement.

low

keepPreviousData prose duplicated across two comment blocks

apps/platform/src/components/discovery/FeatureSpotlight.tsx:124

Lines 124-130 explain keepPreviousData interaction; lines 157-169 re-narrate for firstRunTourPending. Per project convention (no multi-paragraph docstrings), collapse to one definitive note.

History · 36 commits

  1. c1fe7b7needs attentionincremental3H · 4M · 3L2026-07-27 06:02
  2. 414db56safeincremental0H · 0M · 2L2026-07-23 00:52
  3. ea68968needs attentionincremental0H · 4M · 7L2026-07-22 23:01
  4. 4bdb2c9needs attentionincremental1H · 3M · 5L2026-07-21 23:22
  5. 6e3f255safeincremental0H · 0M · 0L2026-07-20 16:46
  6. cc41079blockedincremental5H · 5M · 3L2026-07-20 16:25
  7. 2451ee0needs attentionincremental0H · 5M · 6L2026-07-20 16:09
  8. ab7c103needs attentionincremental0H · 1M · 6L2026-07-20 15:52
  9. 7eff611needs attentionincremental0H · 1M · 2L2026-07-18 00:41
  10. 246c67csafeincremental0H · 0M · 0L2026-07-17 23:58
  11. 7521b17safeincremental0H · 0M · 0L2026-07-17 23:35
  12. 1847ec8needs attentionincremental0H · 2M · 4L2026-07-17 23:28
  13. 5d3175cneeds attentionincremental3H · 6M · 4L2026-07-17 19:31current
  14. d3f875dneeds attentionincremental1H · 2M · 4L2026-07-17 18:01
  15. 47d25faneeds attentionincremental1H · 1M · 2L2026-07-17 00:46
  16. 440d838needs attentionincremental4H · 9M · 9L2026-07-17 00:27
  17. 7466f97needs attentionincremental0H · 1M · 5L2026-07-16 23:20
  18. b686d58needs attentionincremental0H · 2M · 2L2026-07-16 14:24
  19. 9ea1446blockedincremental2H · 7M · 10L2026-07-16 13:44
  20. 6f39bb9needs attentionincremental0H · 2M · 7L2026-07-14 21:53
  21. 2eadf2aneeds attentionincremental0H · 1M · 2L2026-07-14 20:12
  22. 97bd08fneeds attentionincremental0H · 2M · 5L2026-07-14 19:40
  23. a48e3ffneeds attentionincremental5H · 8M · 6L2026-07-14 18:22
  24. 69473b7needs attentionincremental2H · 5M · 3L2026-07-14 01:34
  25. 92e4235needs attentionincremental2H · 1M · 3L2026-07-14 01:04
  26. 6be8018safeincremental0H · 0M · 3L2026-07-14 00:15
  27. 090b4daneeds attentionincremental1H · 3M · 5L2026-07-13 23:50
  28. fa7683bneeds attentionincremental1H · 4M · 3L2026-07-13 18:55
  29. d64cd72needs attentionincremental2H · 3M · 2L2026-07-13 16:27
  30. c1f337bneeds attentionincremental3H · 7M · 14L2026-07-13 13:30
  31. 46e32b2safeincremental0H · 0M · 3L2026-07-11 02:54
  32. 3b30949needs attentionincremental0H · 2M · 3L2026-07-11 02:39
  33. 9f8dbdfneeds attentionincremental3H · 5M · 5L2026-07-11 02:32
  34. 23191eeneeds attentionincremental5H · 12M · 7L2026-07-11 02:18
  35. fa9b88fneeds attentionincremental0H · 1M · 2L2026-07-11 01:30
  36. 6a8bf42blockedfull8H · 11M · 8L2026-07-11 01:08