← all branches

feat/discovery

needs attentionviewing older commit
9f8dbdf · incrementalpre-PRreviewed 2026-07-11 02:32 UTC3H · 5M · 5L · 3I
The branch
Purpose
Feature Discovery v3 — contextual spotlights (coachmarks) that guide providers through onboarding setup: CFE credential → first contract → bill monitoring → payment monitoring → internal site IDs → export columns → API keys
Goal
Implement persistent gap badge (Descubre hub) showing how many features remain unactivated, plus self-initiated spotlight pulls — so discovery is both push (daily spotlight) and pull (user opens Descubre menu)
Sub-goals
  • SG-1: Core decision engine — pure eligibility + pacing + retirement logic
  • SG-2: Descubre hub — persistent badge showing gap count, user can click any eligible feature to get that spotlight on demand
  • SG-3: Bug fixes — cfe_setup anchor, add_contract signal, pacing relax on previews, coachmark layout, idempotent secrets, clicking never retires (this commit)
The changes (whole branch)
What
This commit removes `|| exposure.clickedAt !== null` from the isRetired predicate and wraps the seenCount check in `clickedAt === null`. Updates two tests to match (rewrites the 'retires a clicked key' test to assert the opposite, adds a shows-based guard exemption test). Updates the schema comment.
Why
Production incident: a user clicked the cfe_setup spotlight (which suppresses all other spotlights when absent) and the AWS credential creation failed. The old code retired the key on click, leaving the user with gap=[], no spotlight, no badge — permanently. The fix makes live-state adoption (hasCfeCredential, hasContracts, etc.) the sole natural terminator; dismissal is the explicit user exit.
Areas
domains/cross-domain+5740domains/core/src/discovery+4490apps/platform/src/components/discovery+4400apps/platform/src/api+2780packages/database/src/schema+950e2e/platform+480packages/analytics+420packages/secrets/src/client+4812apps/platform/src/app+262scripts+300
Blast
44 files, +2893/-14 lines. Scoped to new Feature Discovery domain (new tables, new contracts, new UI). Minimal risk to existing features — touches sidebar/header for badge count, bills/contratos layout for spotlight anchors, and credential page. No schema migrations to existing tables.
cfe_setup is a gate signal — its retirement suppresses all other spotlights. The fix directly addresses a user-bricking scenario. No PR open yet — pre-merge branch.
CI· No open PR — CI status unavailable for pre-PR branchCodeRabbit· No .coderabbit.yaml in repo

Findings · 15

correctness4

high

Clicked cards have no upper bound — permanently-failing actions cause infinite reappearance

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

Once clickedAt is set, the seenCount >= RETIRE_AFTER_UNCLICKED_SHOWS guard is permanently bypassed. If the downstream action keeps failing (e.g., a persistently broken AWS endpoint, plan limitation), the card reappears every day with no escape short of explicit dismissal. Consider a RETIRE_AFTER_TOTAL_SHOWS cap (e.g., 10) applied unconditionally to bound the zombie-card scenario, or at minimum document that dismissal is the intended escape hatch in this scenario.

medium

Test change from 'internal_site_ids' to 'monitoring' could mask a priority-ordering regression

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

The old test verified priority fallthrough (dismissal retires 'monitoring' → 'internal_site_ids' is returned). The new test only verifies that clicking doesn't retire. No test explicitly covers that dismissal-triggered fallthrough still works correctly after this change. The existing dismissal retirement test covers it implicitly, but a targeted assertion would be clearer.

low

isBudgetSpent relies on 'seen' event preceding 'clicked' — no enforcement of ordering

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

The 20h cooldown for a clicked-but-failed card depends on a 'seen' outcome having been recorded before 'clicked'. If a client sends 'clicked' without a preceding 'seen' (race condition or replay), lastSeenAt remains stale and the pacing guard may not block re-display in the same session. Pre-existing gap, but amplified now that clicks no longer retire cards.

info

Schema comment updated correctly to match new semantics

packages/database/src/schema/discovery-state.ts:14

Comment now accurately reflects the new retirement rules. No action needed.

security2

low

Non-admin user could set clickedAt on adminOnly key if shown via stale client state

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

adminOnly filtering is enforced in decideSpotlight (pure decision). If a non-admin user somehow sends a 'clicked' outcome for an adminOnly key (e.g., via direct API call or stale client render), their discovery state would record clickedAt. The card now never retires until dismissal. Impact is cosmetically scoped to their own UX state — no privilege escalation to the underlying feature — but worth verifying that the click outcome endpoint validates admin status before recording clickedAt.

info

No new PII or sensitive data introduced by this change

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

The isRetired change and new tests operate entirely on DiscoveryState fields (clickedAt, seenCount, dismissCount). No user PII, credentials, or sensitive business data is introduced.

conventions1

low

Schema comment continuation line missing JSDoc asterisk prefix

packages/database/src/schema/discovery-state.ts:15

The continuation of the added comment uses a leading space instead of ' * ' prefix, breaking the JSDoc block style used everywhere else. Should be: ` * (adoption ends a card naturally; a failed action must be retryable)`.

tests5

high

Regression test uses wrong key — cfe_setup is the actual prod bug, monitoring doesn't reproduce it

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

The production bug was 'clicked cfe_setup + AWS failure bricked the whole surface'. cfe_setup is the adminOnly gate signal — its retirement suppresses everything else. The test uses 'monitoring', which is not a gate signal and doesn't reproduce the full failure path. A cfe_setup-keyed test case with clickedAt set should be added to prevent regressions of the specific live incident.

high

Missing test: dismissed AND clicked key — dismissCount threshold must still retire regardless of clickedAt

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

isRetired returns true when dismissCount >= RETIRE_AFTER_DISMISSALS regardless of clickedAt. This edge case — a card that was clicked AND then dismissed N times — is the load-bearing invariant separating 'permanent exit via dismissal' from 'temporary exit via click'. Without a test, a future refactor adding clickedAt short-circuit logic could inadvertently exempt clicked keys from dismissal-based retirement.

medium

No test: clicked card + fresh lastSeenAt still blocked by daily budget

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

There is no test verifying that a clicked card with a fresh lastSeenAt (within the 20h pacing window) is correctly suppressed by the budget guard. If the budget check is accidentally skipped for clicked keys, the spotlight could show every page load after a failed action. Stale vs fresh lastSeenAt cases for clicked cards should be explicitly covered.

medium

listEligibleSpotlights not tested with new clicked-card-still-eligible semantics

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

The diff only changes decideSpotlight tests. listEligibleSpotlights (which powers the gap badge count in the Descubre hub) uses the same isRetired predicate. If a stale call-site copy exists or the predicate is re-evaluated differently, the old retire-on-click behavior could persist in the badge while the spotlight itself is fixed. A test confirming a clicked card still appears in the eligible list should be added.

low

Shows-based guard exemption test lacks a counterpart asserting that the same seenCount without clickedAt would retire

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

The 'clicked keys are exempt from shows-based guard' test proves the exemption holds but not that it's actually doing anything. Without a companion case (same seenCount, clickedAt=null, key is retired), the test would pass even if the seenCount guard was inadvertently removed.

improvement3

medium

Missing isAdopted abstraction makes the retirement invariant comment-enforced, not structural

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

The contract 'adoption is detected via live state, not clickedAt' is enforced only by the absence of a clickedAt check and a comment. A future contributor adding a new key could re-introduce clickedAt-based retirement without violating any type contract. Extracting an isAdopted(key, signals) predicate and having isRetired not depend on clickedAt would make this invariant structural.

low

Comment should explicitly call out that shows-based guard is suspended once clicked

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

The updated comment says 'Clicking NEVER retires a key' but doesn't state the corollary: once clicked, the seenCount cap is inert, and the only retirement path is dismissal. Adds a sentence like: 'A clicked card ignores the unclicked-shows cap and only retires via RETIRE_AFTER_DISMISSALS dismissals.'

info

RETIRE_AFTER_UNCLICKED_SHOWS constant name remains precise after the change

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

The constant name accurately describes its new semantics (only applies when the card was never clicked). No rename needed.

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