← all branches

feat/discovery

needs attentionviewing older commit
fa7683b · incrementalPR #305reviewed 2026-07-13 18:55 UTC1H · 4M · 3L · 1I
The branch
Purpose
Ambient feature discovery for existing users — coachmarks that surface unused capabilities contextually, without a manual tour
Goal
Feature Discovery v1: ambient spotlights + Descubre hub + first-run tour, flag-gated (feature-discovery-spotlights OFF by default)
Sub-goals
  • SG-1: Ambient spotlights — one contextual coachmark per rolling day, computed on read from real rows
  • SG-2: Descubre hub — header badge listing user gap + 'Volver a tomar el tour'
  • SG-3: First-run linear product tour — fixed 5 steps, dimmed scrim, Hazlo ahora shortcut
  • SG-4: discovery_state table (RLS, migration 0058), FCIS throughout, JSend API, es/en i18n, PostHog analytics
  • SG-5 (this commit): per-view column-config signal — export_columns fires until BOTH recibos + descargas configs personalized
The changes (whole branch)
What
Adds hasRecibosConfig to DiscoverySignalState; changes export_columns from a descargas-only signal (priority 1) to a per-view signal (priority 5) with primary page bills-contratos and alsoOn: ['bills-descargas']; fires until both column configs are personalized; refactors listEligibleSpotlights to iterate catalog once (no per-page duplication, fixes potential hub duplicate); updates getDiscoverySignalState query to also fetch activeRecibosConfigId; updates i18n copy to reflect the multi-view nature; updates tests
Why
Column personalization is per-user, per-view (recibos + descargas). The old signal retired too early — a user who configured descargas columns but not recibos was incorrectly marked 'done'. Contratos is the primary download surface (per-RPU Copy/CSV/ZIP icons), making it a better primary firing page than descargas. The hub dedup fix prevents export_columns appearing twice when users have both surfaces in their gap.
Areas
apps/platform/src/messages+44domains/cross-domain/src+7321
Blast
5 files changed in this increment (+77/−25); full branch: 50 files, +3738/−31 across platform UI, cross-domain decisions, core domain, API contracts, database schema, analytics, secrets, e2e
flag-gated (feature-discovery-spotlights OFF by default) deploy preconditions: create PostHog flag + run reclassify script before flipping
typecheck· PR description states all touched packages typecheck greenci· GitHub Actions check data not accessible via PATcoderabbit· No .coderabbit.yaml in repo

Findings · 9

correctness2

high

export_columns on alsoOn surface (descargas) may CTA-redirect to wrong page

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

When decideSpotlight returns 'export_columns' because the user is on bills-descargas (via alsoOn), the returned key resolves to signal.page = 'bills-contratos'. If the UI uses signal.page as the CTA navigation destination, users on descargas who click 'Configurar columnas' will be taken to contratos instead of /settings/columns. The DiscoverySignal.page JSDoc says 'The hub still deep-links to page' but does not clarify behavior for the in-page spotlight CTA on an alsoOn surface. Confirm spotlight-catalog.ts or the FeatureSpotlight CTA routes to /settings/columns directly, not through the catalog page.

low

Deleted requestedKey test was testing an impossible case (export_columns on contratos was cross-page before)

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

Minor: the old test ('never returns a requested key that belongs to another page') requested export_columns from the default page (contratos). Since export_columns previously lived on bills-descargas, this correctly returned null. Now export_columns lives on contratos so the old assertion would return the key — deletion is correct. No action needed here beyond the missing cross-page test noted above.

conventions3

medium

listEligibleSpotlights duplicates eligibility guards from eligibleCandidatesOnPage

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

The refactored listEligibleSpotlights re-implements the adminOnly + hasCfeCredential + eligible() + isRetired() guards that already live in eligibleCandidatesOnPage. If eligibleCandidatesOnPage is still used by decideSpotlight, the two eligibility paths must stay in sync manually. Extracting a shared predicate (isEligibleForHub vs isEligibleOnPage) or having the hub iterate via a page-independent helper would be safer.

low

Priority 1→5 change lacks a catalog-level ordering comment

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

The inline comment explains the rationale ('Priority after the core setup/activate signals so it doesn't pre-empt them on Contratos') but there is no summary comment on the catalog that documents the intended priority order across all signals. As the catalog grows, a brief ordering legend (e.g. '// priority: 1=cfe 2=monitoring 3=payment 4=ids 5=columns 6=api') would help future editors avoid accidental re-ranking.

info

activeRecibosConfigId column assumed present on userPreferences — confirm migration covers it

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

The query now selects userPreferences.activeRecibosConfigId. If this column was added in the current branch, confirm the Drizzle migration (0058 or a prior one) covers the userPreferences table (not just discovery_state). Typecheck green implies the Drizzle schema already has the column; this is an informational flag for the reviewer's awareness.

tests4

medium

Both-configs-set suppression not tested from contratos (the new primary surface)

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

The 'both set → done' assertion is only exercised from bills-descargas. There is no case asserting that decideSpotlight(page='bills-contratos', hasRecibosConfig=true, hasDescargasConfig=true) returns null. A regression where the contratos path ignores the combined-completion gate would go undetected.

medium

Cross-page suppression for export_columns no longer tested

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

The deleted test verified that requesting export_columns on a page that is neither its primary nor alsoOn returns null. That cross-page blocking case is now entirely untested for export_columns. The replacement test uses api_keys (an adminOnly signal) which is a different gate. A minimal additional case — e.g. requestedKey='export_columns' on 'credentials-api' → null — would restore coverage.

medium

No symmetric test: export_columns fires on descargas after core signals are settled

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

The new contratos test verifies that export_columns surfaces after core signals (hasMonitored, hasPaymentMonitored, hasInternalSiteIds) are satisfied. There is no symmetric descargas test confirming the same settled-state fires export_columns as a secondary surface. The per-view test only uses baseSignals, leaving an asymmetry between the two surfaces.

low

listEligibleSpotlights comment updated but no new structural assertion added

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

The comment on expect(keys).toContain('export_columns') was updated to mention 'bills-contratos (primary) + descargas' but the assertion unchanged. A test that explicitly calls listEligibleSpotlights and checks the returned page field is 'bills-contratos' (the catalog's primary) would give structural confidence the hub deep-link promotion is preserved.

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