← all branches

feat/discovery

needs attentionviewing older commit
ea68968 · incrementalPR #305reviewed 2026-07-22 23:01 UTC0H · 4M · 7L · 4I
The branch
Purpose
Feature Discovery v1 — add ambient spotlights, a Descubre hub, and a first-run product tour to guide existing users toward unused capabilities in the Batu platform.
Goal
Ship contextual coachmarks (computed on-read, paced ≤1/rolling-day), a header hub badge, and a 5-step linear tour — all behind a flag (feature-discovery-spotlights, off by default) — without touching live users until the flag is flipped.
Sub-goals
  • Ambient spotlights: one contextual coachmark per session gap, retire-on-dismiss, 7 signal types
  • Descubre hub: header badge listing gaps, self-serve pull, 'Volver a tomar el tour'
  • First-run linear tour: fixed 5 steps, dimmed scrim + spotlight cutout, 1/N progress, 'Hazlo ahora' shortcut
  • discovery_state table: user-scoped RLS, migration 0064
  • FCIS throughout: pure decisions + read-coordinator shell, DiscoveryFCIS namespace
  • JSend API, es/en i18n, PostHog analytics
  • Adversarial review remediation: preview bypass (was keyed on Host header, now VERCEL_ENV), spotlight self-destruct fix (render latch), atomic upsert, gate-key retirement exemption
  • RPU history drawer: row click opens regime-timeline drawer with deep-link URL state (?detail=<id>)
  • RPU validation fix: 12-digit exact match (was 11-12), too-long state, debounced hint
  • Site name collision: ID match auto-switches, name match shows advisory (not auto-attach) — 379-sites lesson
The changes (whole branch)
What
This incremental commit merges main into feat/discovery to pick up migration 0063→0064 renumber. The diff adds: RPU validation precision (12 digits exact), site-name collision advisory in InlineContractForm, row-click wiring for ContractDetailDrawer, i18n keys for the chapters/drawer namespace, and the discovery_state migration SQL.
Why
Renumber migration after main advanced past 0063; fix edge cases (11-digit RPU, name-only auto-attach) found during adversarial review; wire the chapters endpoint that the ContractDetailDrawer depends on.
Areas
apps/platform — discovery components+120020domains/cross-domain — feature discovery FCIS+11000domains/core — discovery shells/decisions+2500packages/database — schema + migration + seed+137500packages/api — discovery schemas + analytics events+2000apps/platform — contracts table + drawer + page+20030e2e + tests+7000
Blast
67 files, +19 067/−65 lines across discovery components, cross-domain FCIS, core domain, DB schema, API contracts, and UI. Flag-gated: no user impact until feature-discovery-spotlights is flipped. Migration 0064 is additive-only (new table).
flag-gated: feature-discovery-spotlights (off by default) deploy precondition: PostHog flag creation + 2-row reclassify script before flag flip
typecheck· No CI data available in this runtests· No CI data available in this runcoderabbit· No .coderabbit.yaml present

Findings · 14

correctness5

medium

sameNameSite advisory can show stale site name after input is cleared

apps/platform/src/app/[locale]/(dashboard)/bills/_components/InlineContractForm.tsx:196

The sameNameSite effect depends only on [siteOptions] but reads serviceName, customSiteName, useServiceAsSite, siteMode, and submitting from closure. If the user clears the service name field while siteOptions has not re-fetched, the effect does not re-run and sameNameSite stays non-null — the amber advisory remains visible referencing a site that no longer matches the current input. Fix: add the missing closure variables to the dependency array, or capture a ref snapshot of the inputs inside the effect.

medium

SiteModeToggle does not clear sameNameSite — advisory can resurface unexpectedly

apps/platform/src/app/[locale]/(dashboard)/bills/_components/InlineContractForm.tsx:586

When the user clicks the SiteModeToggle directly (not the advisory's 'Usar el sitio existente' button), the toggle sets userChoseModeRef.current = true and setSiteMode(m) but does NOT call setSameNameSite(null). The advisory is hidden while in 'existing' mode. However if the user later switches back to 'new' mode, the effect bails early (userChoseModeRef.current === true) and sameNameSite retains its old value, causing the advisory to reappear with a stale site reference. Fix: add setSameNameSite(null) to the SiteModeToggle onChange handler.

low

useLookupContract enabled guard uses '>= 11' — dead condition after 12-digit fix

apps/platform/src/app/[locale]/(dashboard)/bills/_components/AddContractDrawer.tsx:182

useLookupContract is called with enabled: debounced.length >= 11. Since shouldLookup only fires at exactly 12 digits, debounced is always '' when digits < 12, so the >= 11 check is never independently reached. The guard should be debounced.length === 12 to match the stated invariant and prevent any future path from triggering a lookup on an 11-digit value.

low

[role='dialog'] in row click guard is a dead selector

apps/platform/src/app/[locale]/(dashboard)/bills/contratos/_components/ContractsTable.tsx:669

The guard target.closest('button, a, input, [role="checkbox"], [role="dialog"]') includes [role="dialog"]. Radix/shadcn dialogs render in portals outside the table DOM — a click inside a dialog cannot bubble through a <tr>. The selector is harmless today but misleading to maintainers and could cause unintended behavior if dialogs ever render inside table rows.

low

detailId useState reads window.location.search — SSR/client hydration mismatch

apps/platform/src/app/[locale]/(dashboard)/bills/contratos/page.tsx:130

The lazy initializer guards against SSR with typeof window === 'undefined' but still produces a null (server) vs non-null (client with ?detail=id in URL) mismatch on hydration. The established App Router pattern is useSearchParams() — already used on this page (discoverySearchParams) and SSR-safe by design. Using it for the initial detailId read would eliminate the guard and the mismatch.

security2

info

No client-side publicId format validation before issuing chapters query

apps/platform/src/app/[locale]/(dashboard)/bills/contratos/page.tsx:131

The ?detail param is passed directly to the API as publicId. The server handles arbitrary strings safely (parameterized queries, returns 404 for unknown IDs). A lightweight format guard (e.g. /^ctr_[0-9A-Z]{26}$/) would fail fast on malformed deep-links and avoid unnecessary API round-trips. Not a security risk given server-side safety.

info

chapters handler service-role bypass is load-bearing — document the invariant

apps/platform/src/api/handlers/utility-contracts.handler.ts

After confirming org access via RLS-wrapped findOverviewByContractPublicId, subsequent reads use the service-role connection (no RLS). This is correct because utility_contracts/bills carry no org_id. The org gate is the overview lookup. The invariant — subsequent reads must only use IDs returned by the authenticated overview, never client-supplied IDs — is currently enforced by convention only. Adding a typed scope object would make the constraint structurally enforced.

conventions2

info

Removed i18n keys monitoredOnly/fullHistory — verified clean

apps/platform/src/messages/en.json

Both keys removed from en.json and es.json. Grep confirms no remaining references in .tsx/.ts files. Clean removal.

info

PR description references stale migration number '0058' (actual: 0064)

packages/database/drizzle/0064_discovery_state.sql

The migration was renumbered from 0063 to 0064 when main was merged in (per commit message). The PR description still says 'migration 0058'. Cosmetic only — no runtime impact. Update the PR body for audit trail accuracy.

tests5

medium

12-digit RPU validation boundary (was 11) has no regression test

apps/platform/src/app/[locale]/(dashboard)/bills/_components/AddContractDrawer.tsx:156

The 11→12 digit boundary fix was a confirmed production bug (11-digit incomplete numbers verified as valid). The LookupState state machine — shouldLookup, debounced, too-short, too-long, showLengthHint — is entirely untested. A unit test covering 11 digits → too-short, 12 digits → triggers lookup, 13 digits → too-long would guard against regression.

medium

ID-vs-name match split in InlineContractForm advisory has no unit test

apps/platform/src/app/[locale]/(dashboard)/bills/_components/InlineContractForm.tsx:195

The three-way branch (ID match → auto-switch; name match → advisory only; no match → clear) guards against the 379-sites mass-merge bug at the UI layer. The backend decision function is well-tested, but the UI advisory path is not. A mismatch here could silently re-introduce the over-merge bug even though the backend is correct.

low

Drawer deep-link URL state not covered by tests

apps/platform/src/app/[locale]/(dashboard)/bills/contratos/page.tsx:130

openDetail, closeDetail, and the detailId initializer (deep-link from ?detail= on load) are untested. The deep-link init path is the kind of edge case easy to break silently, especially after router changes.

low

ContractsTable onOpenDetail prop not exercised in existing memo test

apps/platform/src/app/[locale]/(dashboard)/bills/contratos/_components/ContractsTable.tsx

The existing ContractsTable.memo.test.tsx renders without onOpenDetail. No test verifies the row click calls the handler or that rows gracefully no-op without the prop.

low

chapters handler wiring has no integration test

apps/platform/src/app/api/[...ts-rest]/route.ts:499

The new chapters: listContractChaptersHandler wiring is not covered by an integration smoke test. An auth regression or contract-shape mismatch would go undetected until runtime.

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