← all branches

feat/discovery

needs attentionviewing older commit
69473b7 · incrementalPR #305reviewed 2026-07-14 01:34 UTC2H · 5M · 3L · 2I
The branch
Purpose
Feature Discovery v3 — contextual in-product nudges (spotlights, guided tour, hub) to help providers discover key platform capabilities and reach their first value moments faster.
Goal
Descubre hub v3.1: persistent for providers even at checklist=0, shows done capabilities as struck achievements, gates on discoveryActive from the server.
Sub-goals
  • SG-1: Add adopted predicates to every DiscoverySignal — done state distinct from eligible
  • SG-2: listDiscoveryChecklist pure function — done+available (locked hidden, retirement ignored)
  • SG-3: Shell emits discoveryActive bool; non-providers get false
  • SG-4: API schema extended with status on items and discoveryActive bool
  • SG-5: DescubreMenu refactored to persistent, renders done/available with check/sparkle icons
  • SG-6: ProductTourMenuItem removed — tour re-entry consolidated inside DescubreMenu
The changes (whole branch)
What
7 adopted predicates added to DISCOVERY_SIGNAL_CATALOG; new listDiscoveryChecklist pure function; shell switched to emit checklist + discoveryActive; API schema extended; DescubreMenu refactored to always-mounted for providers; ProductTourMenuItem removed from DashboardHeader.
Why
The previous hub vanished at gap=0 (all steps done), leaving no stable home for the guided tour. The persistent design gives the tour a stable entry point and turns the hub into an achievement tracker.
Areas
domains/cross-domain/src/feature-discovery.*+1688apps/platform/src/components/discovery/DescubreMenu.tsx+7248packages/api/src/schemas/discovery.schemas.ts+110apps/platform/src/api/handlers/discovery.handler.ts+10apps/platform/src/messages/*.json+42domains/cross-domain/src/index.ts+30apps/platform/src/components/DashboardHeader.tsx+05
Blast
10 files, +245/-63 in this incremental window. listDiscoveryChecklist is side-effect-free; shell change is additive; UI breaking for gap-only API consumers.
incremental review: 1 commit since 92e4235a no CI signal available
ci· CI checks inaccessible via tokencoderabbit· No .coderabbit.yaml present

Findings · 12

correctness1

medium

Toast fires spuriously when query cycles through loading state mid-session

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

When ok flips false (loading/error) then true (data arrived), the useEffect early-returns on `if (!ok) return` without updating prevRef.current. On the next render when data arrives, prev.keys holds the stale snapshot and current holds new data, so a spurious 'completed step' toast fires if items differ. prevRef should be updated even on the !ok early-return path, or depend on a stable isSuccess flag.

security1

info

status: done discloses org setup booleans — intentional, bounded by auth+RLS

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

Each item's status field reveals whether the org completed a setup step. Intentional UX; bounded by withAuth + RLS (no cross-org leakage). Worth re-evaluating if the signal set ever expands to sensitive commercial state (payment status, contract financials).

conventions3

medium

ChecklistItem local type in DescubreMenu is an unsafe cast — no structural alignment

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

A local ChecklistItem type is hand-written then cast with `as ReadonlyArray<ChecklistItem>`. If DiscoveryChecklistItem gains or renames a field, the cast silently succeeds while the component uses stale types. Better: derive from DiscoveryGapResponse['items'][number] (already client-bundle-safe from @batu/api schemas), giving compile-time proof the alias matches the wire schema.

medium

DiscoveryChecklistStatusSchema missing satisfies z.ZodType<DiscoveryChecklistStatus>

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

Per the covariance-gotcha in api-patterns.md: z.enum(['done','available']) needs `satisfies z.ZodType<DiscoveryChecklistStatus>` to catch a future widening of the domain type (e.g. adding 'locked') that leaves the runtime enum stale with no TypeScript error. Add the annotation and import DiscoveryChecklistStatus from @batu/cross-domain.

low

discoveryActive: true hardcoded in shell — derive from orgSegment instead

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

The providers success path emits `discoveryActive: true` as a constant. If a second segment ('enterprise') is ever enabled, this constant stays true silently. Prefer `discoveryActive: input.orgSegment === 'providers'` — stays correct as segments expand and is self-documenting.

tests5

high

export_columns partial-done state untested in listDiscoveryChecklist

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

export_columns uses asymmetric predicates: eligible uses OR (missing either config = eligible), adopted uses AND (both configs needed = done). The partial state — one view configured, not both — should yield 'available'. This is explicitly called out in decisions.ts comments but has no test in the listDiscoveryChecklist suite. The decideSpotlight suite covers it but the checklist function is a separate code path. A future refactor swapping || for && in adopted would regress silently.

high

payment_monitoring available-but-not-done state never asserted in checklist suite

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

The checklist test verifies payment_monitoring is UNDEFINED when hasMonitored=false (locked). But the unlock transition — hasMonitored=true, hasPaymentMonitored=false → 'available' — is never asserted in the listDiscoveryChecklist suite. Also no test for the adopted direction: hasPaymentMonitored=true should yield 'done' regardless of hasMonitored. Both are exercised in decideSpotlight tests but not the dedicated listDiscoveryChecklist describe block.

medium

internal_site_ids checklist status never directly asserted in any test

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

The base-signals test checks cfe_setup, add_contract, monitoring, export_columns, api_keys, and payment_monitoring — but not internal_site_ids. baseSignals has contractCount=10 >= SCALE_SIGNAL_MIN_CONTRACTS and hasInternalSiteIds=false, so expected status is 'available'. The all-adopted test sets hasInternalSiteIds=true but only asserts items.every(done). A bug that dropped internal_site_ids from output would not be caught.

medium

getDiscoveryGapShell integration test absent — discoveryActive unwired at DB/RLS level

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

The existing integration tests exercise getDiscoverySpotlightShell and recordSpotlightOutcomeShell with real DB + RLS, but getDiscoveryGapShell (which calls listDiscoveryChecklist) has no integration test. discoveryActive: true/false and tourSeen from the exposures query are never asserted at the DB layer.

info

Retirement-ignore invariant has no test — architecturally safe but undocumented

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

listDiscoveryChecklist doesn't accept exposures (making retirement impossible to introduce), but no test documents the intent. A test asserting that a user with 2 dismissals on monitoring still sees 'available' in the checklist would document the deliberate divergence from listEligibleSpotlights.

improvement2

low

`ok` is too terse for a variable gating three derived values

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

`ok` guards discoveryActive, items, and a useEffect dependency. The name carries no domain meaning. `checklistReady` or `checklistLoaded` would make intent obvious without adding length.

low

Tour re-entry reachability: verify fully-onboarded providers still see Descubre button

apps/platform/src/components/DashboardHeader.tsx:178

ProductTourMenuItem removed; tour re-entry now lives inside DescubreMenu. DescubreMenu is persistent for providers (discoveryActive=true regardless of checklist fullness), so the button should always be reachable. Verify manually: a provider with all items 'done' still sees the Descubre button and can re-launch the tour — this is the core UX promise of v3.1.

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