← all branches

feat/discovery

needs attentionviewing older commit
1847ec8 · incrementalPR #305reviewed 2026-07-17 23:28 UTC0H · 2M · 4L · 2I
The branch
Purpose
Feature Discovery v1 — contextual spotlight coachmarks, Descubre hub checklist, and first-run linear tour for Batu for Providers users, flag-gated (feature-discovery-spotlights OFF by default).
Goal
Fix a cluster of dogfooding bugs discovered after the initial PR review: ambient coachmark popping over the tour close/RPU drawer, double 'Siguiente paso' toast mid-tour, pulled hub cards dying instantly post-param-strip on the tour-gated page, dead recibos tour anchor, and CFE credentials handoff toast auto-dismissing before the user could read it.
Sub-goals
  • SG-1: Replace seen-based firstRunTourPending with cooldown-based holdAmbientForTour (keeps ambient out of the whole tour session, not just pre-first-engagement)
  • SG-2: Add origin:'pull'|'ambient' to SpotlightPick so the tour-cooldown gate exempts user-consent pulls (bills-contratos pull cards were dying post-param-strip)
  • SG-3: Extract shouldConsumePullParam as a named function (testable, fixes the keepPreviousData blink regression)
  • SG-4: Double-toast guard in DescubreMenu (suppress 'Siguiente paso' when in tour flow)
  • SG-5: Rebind recibos tour step anchor from monitoring toggle to recibos_col column header (dead-button-under-scrim dogfooding bug)
  • SG-6: duration:Infinity on CFE credentials handoff toast (auto-dismiss was too fast)
  • SG-7: Extract tutorialHref + ChecklistItem to discovery-links.ts for testability; add tutorial-href and shouldConsumePullParam unit tests
The changes (whole branch)
What
Cooldown-based ambient hold (shouldHoldAmbientForTour, 20h post-tour-engagement); SpotlightPick.origin field; shouldConsumePullParam extraction; inTourFlow guard; recibos_col anchor; Infinity toast; discovery-links.ts module; API field rename firstRunTourPending→holdAmbientForTour; listEligibleKeysOnPage drops exposures arg.
Why
Dogfooding the first-run tour revealed several visual-layer bugs: the ambient coachmark appeared while the tour was still closing, the Descubre hub fired a second toast mid-tour, user-pulled cards on the main tour page were unmounted instantly after param strip, the recibos tour step pointed at a toggle the scrim blocked, and the CFE handoff toast vanished too fast.
Areas
domains/cross-domain/src+9690domains/core/src/discovery+3990apps/platform/src/components/discovery+11970apps/platform/src/api+5250apps/platform/src/app+5370packages/api/src/schemas+990e2e/platform+610packages/database+1364512
Blast
60 files +5006/-63 (excluding drizzle meta snapshot); all changes flag-gated — no user impact until feature-discovery-spotlights is flipped.
flag-gated: feature-discovery-spotlights OFF by default no auto-merge (CODEOWNERS → tech-leads)
typecheck· not available in this runCI checks· GitHub API restricted for checks on this tokenCodeRabbit· no .coderabbit.yaml in repo

Findings · 8

correctness2

medium

shouldHoldAmbientForTour resets on every tour re-engagement — rolling, not one-way

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

Every time the user relaunches the tour via the hub, lastSeenAt updates and the 20h cooldown resets. A user who relaunches the tour daily would never see the ambient coachmark on the contratos page. The old isFirstRunTourPending was one-way (false forever after first engagement); the new function is a rolling window. This may be intentional (don't surface ambient during active tour sessions), but it is undocumented — the comment says 'surfaces on a later visit' without mentioning that repeated relaunch extends the hold indefinitely.

low

inTourFlow guard may suppress the 'Siguiente paso' toast on bookmarked TOUR_RETURN_PARAM URLs

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

The double-toast guard reads searchParams.has(TOUR_RETURN_PARAM). A user who navigates to the CFE credentials page mid-tour and bookmarks that URL would have TOUR_RETURN_PARAM in the URL on a fresh load with no active tour — the toast would be silently suppressed. Very narrow edge case (TOUR_RETURN_PARAM is meant to be consumed by the tour flow), and prevRef logic further limits the scope, but the guard is slightly wider than 'is a tour actually running'.

conventions2

medium

shouldHoldAmbientForTour not named decide* per canonical naming

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

Pure decision functions in the .decisions.ts layer should follow the decide{Operation} naming convention (canonical-form.md). The new function is named shouldHoldAmbientForTour; per convention it should be decideHoldAmbientForTour. Sibling isFirstRunTourPending has the same deviation and predates this change — but that does not justify extending the pattern. Low-risk (purely naming), but the canonical form should be respected for new additions.

info

Wire rename firstRunTourPending → holdAmbientForTour — all refs updated but no grep record in PR

packages/api/src/schemas/discovery.schemas.ts:75

canonical-form.md requires a workspace grep before renaming wire-level fields. All references are correctly updated (handler, component, shell, schema) with no orphans. The concern is process: the PR description does not document that a grep was performed. This is a process note, not a live bug — flagging so the convention is explicitly followed next time.

tests2

low

Shell integration test does not assert holdAmbientForTour output

domains/cross-domain/src/__tests__/feature-discovery.integration.test.ts

The five getDiscoverySpotlightShell integration tests assert spotlightKey but none assert holdAmbientForTour. The shared-now fix (time-consistency between decideSpotlight and shouldHoldAmbientForTour) is validated only by unit tests. A targeted test (profile with product_tour exposure within/outside cooldown) would pin the shell wiring and catch future regressions at the injection boundary.

low

shouldHoldAmbientForTour exact boundary (== cooldownHours) not covered

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

The implementation uses strict less-than (< cooldownHours * 3600 * 1000), so at exactly AMBIENT_AFTER_TOUR_COOLDOWN_HOURS the result is false (released). Tests prove at +1h past but not at the exact boundary. Minor for a 20h window but the strict vs non-strict edge is undocumented by tests.

improvement2

low

shouldHoldAmbientForTour and isFirstRunTourPending independently re-implement the 'never engaged' check

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

Both functions search for the product_tour exposure and return true when none is found, but using different logic paths. If the 'never engaged' definition changes (e.g., a new field), only isFirstRunTourPending would be updated. shouldHoldAmbientForTour could call isFirstRunTourPending(exposures) in the no-tour-row branch to make the dependency explicit.

info

inTourFlow searchParams checks could use a shared TOUR_PARAMS array from tour-catalog

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

Three separate searchParams.has() calls on TOUR_PARAM / TOUR_RETURN_PARAM / SUBTOUR_PARAM. These are co-defined in tour-catalog.ts. Extracting TOUR_PARAMS = [TOUR_PARAM, TOUR_RETURN_PARAM, SUBTOUR_PARAM] as const there and using .some(p => searchParams.has(p)) would make exhaustiveness structural — if a fourth tour param is added to the catalog, this guard would be an easy miss otherwise.

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:28current
  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: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