feat/discovery
needs attentionc1fe7b7 · incrementalPR #305reviewed 2026-07-27 06:02 UTC3H · 4M · 3L · 2I- Purpose
- Contextual feature discovery for existing Batu platform users: ambient spotlights, a Descubre hub, and a first-run linear product tour. Flag-gated (feature-discovery-spotlights, OFF by default).
- Goal
- Ship Feature Discovery v1 — compute discovery gaps on read (no materialized tracker), pace spotlights ≤1/rolling-day with retire-on-dismiss, provide a self-serve Descubre hub, and a fixed 5-step first-run tour.
- Sub-goals
- discovery_state table (migration 0058, user-scoped RLS) — FCIS throughout
- Ambient spotlight engine: DiscoveryFCIS, pure decisions, read-coordinator shell
- Descubre hub component in header with badge + gap listing
- First-run linear tour: 5 steps, dimmed scrim + spotlight cutout, 1/N progress
- JSend API surface + es/en i18n + PostHog analytics
- Adversarial review applied: prod-env bypass fix, spotlight self-destruct fix, atomic upsert, gate-key retirement exemption
- What
- This incremental window is a merge of main into feat/discovery, bringing in One-API groundwork: consolidated PublicIdPrefix registry (shared-kernel/ids.ts), new route-meta.ts export in packages/api, and a comment fix in database/schema/index.ts.
- Why
- Merge main to pick up the id-prefix SSOT (one-api.md) before the feature branch ships — ensures discovery_state uses 'dst' from the canonical registry rather than a local constant.
- Areas
- apps/platform+2542−52domains/cross-domain+1604−0domains/core+498−0packages/secrets+152−12packages/database+138−1packages/api+103−0packages/analytics+98−0e2e/platform+61−0scripts+30−0packages/shared-kernel+1−0
- Blast
- 64 files, +5227/-65 (branch vs main). Core areas: apps/platform (discovery UI + API handlers), domains/cross-domain (coordinator shells), domains/core (discovery entity + decisions). One new migration (0058). No infra changes.
Findings · 13
correctness2
'i' flag in publicIdRegex makes prefix case-insensitive — 'CFJ_xxx' matches
packages/shared-kernel/src/types/ids.ts:78
The 'i' flag is applied to the full regex `^${prefix}_${ULID_BODY_PATTERN}$`. Since the prefix is lowercase (e.g. 'cfj'), 'i' causes 'CFJ_...', 'Cfj_...' etc. to pass wire validation. The docstring says this flag is for accepting lowercase ULID bodies (backward compat), but it silently widens the prefix contract too. Fix: include lowercase chars explicitly in ULID_BODY_PATTERN (`[0-9A-HJKMNPa-hjkmnptv-zTV-Z]{26}`) and drop the 'i' flag, or document that uppercase-prefix tolerance is intentional.
isValidPublicId() accepts non-registry prefixes — diverges from publicIdRegex
packages/shared-kernel/src/types/ids.ts:125
isValidPublicId() uses /^[a-z]{3}_[0-9A-HJKMNP-TV-Z]{26}$/ — accepts any 3-char lowercase prefix, including 'aaa_...'. This is intentional for a format-only check, but it diverges from publicIdRegex which enforces a specific registry prefix. The function's JSDoc should explicitly state it's a format check only (not a registry membership check) to prevent misuse as an authorization guard.
security1
No injection risk — publicIdRegex inputs are compile-time constants
packages/shared-kernel/src/types/ids.ts:78
prefix parameter is typed as PublicIdPrefix (union of literal strings). No user-controlled input flows into the RegExp constructor. Safe.
conventions4
Stale 'cjb_' JSDoc in event-schemas.ts — contradicts canonical registry
services/utility/bills/cfe/src/domain/event-schemas.ts:48
JSDoc on JobCreatedEventDataSchema.publicId still says 'cjb_...' as the CfeJob prefix. The canonical value is now 'cfj' in both the registry and the DB schema. Misleads any reader auditing the id registry and will cause confusion when event-schemas are eventually tightened to use publicIdRegex.
JSDoc claims 'retired copies re-export this constant' — unverified by diff
packages/shared-kernel/src/types/ids.ts:14
The new JSDoc states domains/utility/src/schema/base.ts enum and packages/api/src/schemas/common.schemas.ts map 're-export this constant'. If those files still carry independent values (even if identical), the SSOT claim is aspirational rather than structural. The diff does not show those files being updated to delegate to PublicIdPrefix.
one-api.md reference in JSDoc lacks explicit path
packages/shared-kernel/src/types/ids.ts:10
The JSDoc references 'one-api.md' without a path. The same PR's change to database/schema/index.ts shows the pattern: '.claude/rules/contracts-read-model.md'. Should be '.claude/rules/one-api.md' for consistency.
Comment update in database/schema/index.ts is correct
packages/database/src/schema/index.ts:119
Updating the ADR path to .claude/rules/contracts-read-model.md aligns with the Tier 5 retirement rule in CLAUDE.md. No issue.
tests4
publicIdRegex() has no unit tests
packages/shared-kernel/src/types/ids.ts:72
New exported wire-validation utility with non-trivial behavior (flag handling, ULID body composition, case-sensitivity toggle). Already consumed by job.public-schemas.ts for production wire validation. Missing tests: correct prefix match, wrong prefix rejection, 'i' flag lowercase acceptance, invalid format rejection. A bug here silently breaks schema enforcement across all public-API schemas.
~6 test files and seed data carry stale 'cjb_' prefix strings
domains/utility/src/cfe-job/__tests__/cfe-job.decisions.test.ts:56
Affected files: cfe-job.decisions.test.ts (L56,78,162,171), credit-metering.decisions.test.ts (L97,105), event-schemas.test.ts (multiple), seed-utility-demo.ts (L1352,1363,1396). Tests pass today because decision functions treat IDs as opaque strings (no regex validation). But the test data is inconsistent with the enforced wire schema (cfj_*), and any future validation tightening will cause silent failures.
Seed data 'cjb_*' IDs may break integration tests validating response schemas
packages/database/src/seed-utility-demo.ts:1352
seed-utility-demo.ts seeds cfe_jobs with publicId = 'cjb_001_test_queued' etc. PublicJobIdSchema now enforces cfj_* via publicIdRegex. Integration tests that seed from this file and inspect job IDs in API responses risk failing schema validation or asserting on wrong prefix values.
No regression test pinning PublicIdPrefix.CfeJob === 'cfj'
packages/shared-kernel/src/types/ids.ts:56
The CfeJob prefix was corrected from 'cjb' to 'cfj' with a comment citing production data. No snapshot or assertion test pins this value. An accidental revert would propagate to wire validation (PublicJobIdSchema) without a test-layer catch.
improvement2
'i' flag footgun — callers must know to pass it for wire schemas
packages/shared-kernel/src/types/ids.ts:64
The docstring says 'public-v1 wire schemas pass i' but there's no named constant or wrapper to enforce this. A caller that forgets the flag silently gets a stricter schema. Consider exporting a PUBLIC_V1_REGEX_FLAGS = 'i' constant or two wrappers (publicIdRegexStrict / publicIdRegexWire) to make the pit of success explicit.
MonitoringSubscription prefix 'sub' is ambiguous across domains
packages/shared-kernel/src/types/ids.ts:54
'sub' for MonitoringSubscription could collide conceptually with Stripe subscriptions or other subscription entities. 'mns' or 'mons' would be more domain-specific. No current collision in the registry.
History · 36 commits
- c1fe7b7needs attentionincremental3H · 4M · 3L2026-07-27 06:02current
- 414db56safeincremental0H · 0M · 2L2026-07-23 00:52
- ea68968needs attentionincremental0H · 4M · 7L2026-07-22 23:01
- 4bdb2c9needs attentionincremental1H · 3M · 5L2026-07-21 23:22
- 6e3f255safeincremental0H · 0M · 0L2026-07-20 16:46
- cc41079blockedincremental5H · 5M · 3L2026-07-20 16:25
- 2451ee0needs attentionincremental0H · 5M · 6L2026-07-20 16:09
- ab7c103needs attentionincremental0H · 1M · 6L2026-07-20 15:52
- 7eff611needs attentionincremental0H · 1M · 2L2026-07-18 00:41
- 246c67csafeincremental0H · 0M · 0L2026-07-17 23:58
- 7521b17safeincremental0H · 0M · 0L2026-07-17 23:35
- 1847ec8needs attentionincremental0H · 2M · 4L2026-07-17 23:28
- 5d3175cneeds attentionincremental3H · 6M · 4L2026-07-17 19:31
- d3f875dneeds attentionincremental1H · 2M · 4L2026-07-17 18:01
- 47d25faneeds attentionincremental1H · 1M · 2L2026-07-17 00:46
- 440d838needs attentionincremental4H · 9M · 9L2026-07-17 00:27
- 7466f97needs attentionincremental0H · 1M · 5L2026-07-16 23:20
- b686d58needs attentionincremental0H · 2M · 2L2026-07-16 14:24
- 9ea1446blockedincremental2H · 7M · 10L2026-07-16 13:44
- 6f39bb9needs attentionincremental0H · 2M · 7L2026-07-14 21:53
- 2eadf2aneeds attentionincremental0H · 1M · 2L2026-07-14 20:12
- 97bd08fneeds attentionincremental0H · 2M · 5L2026-07-14 19:40
- a48e3ffneeds attentionincremental5H · 8M · 6L2026-07-14 18:22
- 69473b7needs attentionincremental2H · 5M · 3L2026-07-14 01:34
- 92e4235needs attentionincremental2H · 1M · 3L2026-07-14 01:04
- 6be8018safeincremental0H · 0M · 3L2026-07-14 00:15
- 090b4daneeds attentionincremental1H · 3M · 5L2026-07-13 23:50
- fa7683bneeds attentionincremental1H · 4M · 3L2026-07-13 18:55
- d64cd72needs attentionincremental2H · 3M · 2L2026-07-13 16:27
- c1f337bneeds attentionincremental3H · 7M · 14L2026-07-13 13:30
- 46e32b2safeincremental0H · 0M · 3L2026-07-11 02:54
- 3b30949needs attentionincremental0H · 2M · 3L2026-07-11 02:39
- 9f8dbdfneeds attentionincremental3H · 5M · 5L2026-07-11 02:32
- 23191eeneeds attentionincremental5H · 12M · 7L2026-07-11 02:18
- fa9b88fneeds attentionincremental0H · 1M · 2L2026-07-11 01:30
- 6a8bf42blockedfull8H · 11M · 8L2026-07-11 01:08