← all branches

feat/discovery

needs attentionviewing older commit
47d25fa · incrementalPR #305reviewed 2026-07-17 00:46 UTC1H · 1M · 2L · 2I
The branch
Purpose
Feature Discovery v1 — flag-gated (OFF by default) contextual discovery layer for existing users: ambient spotlights, Descubre hub, and a first-run guided tour.
Goal
Land a safe, non-disruptive discovery system paced at ≤1/rolling-day per user, retired on dismiss, flag-controlled so nothing reaches prod until deliberately flipped.
Sub-goals
  • SG-1: Ambient spotlights — FCIS core (discovery.decisions, shells, queries, mapper, migration 0061)
  • SG-2: Descubre hub (DescubreMenu.tsx) — header badge with gap list and deep-link pulls
  • SG-3: First-run guided tour (GuidedTour, RpuSubTour, TourOverlay) — 5-step linear tour, once per user
  • SG-4: API surface — discovery contract, handler, mapper (getSpotlight, getGap, recordOutcome)
  • SG-5: Adversarial review + two prod blockers fixed (preview Host bypass; spotlight self-destruct via self-invalidating refetch)
  • SG-6: Medium cluster — atomic upsert, gate-key retirement exemption, ?descubre stripping, org-switch toast, tests
The changes (whole branch)
What
tourPendingRef latch in FeatureSpotlight.tsx fixes pull-card blink where ?descubre stripping triggered a gcTime:0 refetch that voided response, reverting firstRunTourPending to true and causing the tour gate to unmount+remount the just-shown card (double-reporting seen). CTA copy trimmed: 'Got it, show me' → 'Got it' / 'Entendido, muéstrame' → 'Entendido'.
Why
The blink was a visible regression in the ?descubre pull flow with an invisible consequence: double-counted retirement spend. CTA copy is a first-pass placeholder awaiting Diego's wordsmithing.
Areas
apps/platform/src/components/discovery+153010domains/cross-domain/src+15040apps/platform/src/api + app routes+83139domains/core/src/discovery+4980packages/api + analytics + database + secrets+48513apps/platform/src/messages (i18n)+1160e2e/platform+560
Blast
~60 files, +4950/−62 across platform, core, cross-domain, packages; new DB table discovery_state (migration 0061, user-scoped RLS); 3 new API routes; 1 PostHog feature flag dependency. Flag-gated — no user-visible change until feature-discovery-spotlights is enabled.
Feature-gated: feature-discovery-spotlights PostHog flag required before prod rollout Deploy precondition: run scripts/discovery-reclassify-consumer-orgs.sql against target DB before flipping flag Copy is placeholder — Diego to wordsmith the discovery i18n namespace
ci· No CI checks reported for this PRcoderabbit· No .coderabbit.yaml present

Findings · 6

correctness3

medium

Ref mutated during render — Strict Mode double-invoke is idempotent but deviates from pure render expectation

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

tourPendingRef.current is mutated during render. In React 19 Strict Mode, render is invoked twice; the second call re-applies both mutations, which is idempotent given unchanged inputs. Safe in practice and mirrors the existing latchRef convention in the same file — but worth noting if unexpected Strict Mode behaviour is observed.

low

tourPendingRef is a one-way ratchet for a stable mountId (cannot revert to true)

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

Once the server answers firstRunTourPending:false for a given mountId, the latch stays false even if the query is subsequently invalidated and the new answer would be true (e.g., after a tour reset). Intentional per the comment, but latent if tour-pending state can flip false→true server-side — the gate would stay open until a page navigation resets the mountId.

info

No-op intermediate write when mountId change and 200 response coincide in one render

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

If mountId changed AND response?.status===200 in the same render, the code writes {mountId, value:true} then immediately overwrites with the server value. Correct — second write wins, intermediate true state never escapes the render. No action needed.

tests1

high

No regression test for the tourPendingRef latch (the double-`seen` bug)

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

The bug being fixed (gcTime:0 refetch voids response → firstRunTourPending reverts to true → card unmounts+remounts on TOUR_GATED_PAGE → seen reported twice, silently inflating the retirement counter) has no regression test. The spotlight key latch has a symmetric suite in spotlight-latch.test.ts (7 cases). The inline nature of the tour-pending latch makes it untestable in isolation — extracting to a pure computeTourPendingLatch() function in spotlight-catalog.ts and adding 4 cases (initial=true, latch-on-first-200, hold-through-undefined, reset-on-mountId-change) would close the gap and mirror the established key-latch pattern.

improvement2

low

Second instance of the mountId latch pattern — flag for extraction if a third appears

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

The tourPendingRef block is structurally identical to latchRef (useRef + mountId check + response-conditional update). Types differ and update conditions differ, so merging now would entangle unrelated concerns. Flag for a useMountRef<T> helper only if a third instance appears.

info

CTA copy trim removes navigation hint — confirm intent

apps/platform/src/messages/en.json

Got it, show me → Got it removes the affordance hint that the button navigated or revealed something. Product call — consistent with the PR note that CTAs are placeholder for Diego to wordsmith. If the button still navigates, consider whether the stripped copy is too quiet.

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:31
  14. d3f875dneeds attentionincremental1H · 2M · 4L2026-07-17 18:01
  15. 47d25faneeds attentionincremental1H · 1M · 2L2026-07-17 00:46current
  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