The morning closed out the trial-to-pilot conversion at every layer it was still hiding in — five of them, found one at a time. The afternoon was the first end-to-end operator walkthrough of SDR Hive: forty-two findings across every surface an inbound SDR touches. Nothing in the product was broken by today's work; a great deal that was already broken is now written down.
call_limit had a null-fallback of 10 in the sidebar, the SDR dashboard,
entitlements, and internal BI, while the enforcer and the usage banner defaulted to 60.
All six now read 60. Also removed: "Free trial" plan label, four upgrade CTAs, and two
billing links that led an invited AE to a credit-card form in two clicks.
hqSetTier wrote call_limit = 10 for free tier in the same statement
that set status = 'approved'. Every pilot user approved through HQ would have been
capped at 10 and every reader would have honestly reported it. Zero rows affected — HQ approval
had not yet been used on a free-tier lead.
me.functions.ts gated the entire pilot feature unlock on
trial_state.expiry_date, which create_trial_on_signup writes as
NOW + 14 days. On day 15 every pilot user would lose case studies, battle cards, proof points,
and AE analysis tuning — and clicking any of them opened a modal stating they had used all 60
analyses. Fixed by removing the expiry comparison. trial_state and
create_trial_on_signup untouched.
monthly-credit-reset — scheduled daily at 03:15 — flipped
trial_state.status to expired and enqueued the
trial-expired template, which tells the user their trial ended and links to
/pricing. Both removed; the credit grant was left untouched. The dormant
expire-trials endpoint was also stubbed.
checkTrialExpiry ran in a useEffect on the Qualifier — the SDR's
primary work surface. Opening the page on day 15 performed the expiry, then replaced
the analyze button with a "Trial Expired · View Plans" paywall. Same function carried a
hardcoded 50-analysis cap reading trial_state.analyses_run — a second enforcer
firing ten analyses early. Both removed.
UpgradeModal now reads the user's actual
call_limit rather than a hardcoded 60.
trial_state.expiry_date 30 days in the past, signed in with a live
password grant, and driven in Chromium. Case studies and battle cards both rendered unlocked —
and because the database row said free, an unlocked render was only reachable via
the corrected free_trial promotion. Behaviour, not a re-read of the condition.
Test account and its org, profile, and trial rows were torn down afterward.
trial_state.statusexpire_lapsed_org_trials(), which nothing calls and whose only HTTP caller is
now stubbed, and an explicit admin action in admin-billing.functions.ts. The
client-triggered writer is gone. We had been surprised by this column three times before
asking for the complete list.
call_limit. The remaining three are the
terms-of-service gate and an anti-abuse rate limiter — neither a usage cap. Confirmed:
call_limit is the single enforcer, and no trial_state value can
block an analysis any longer.
refresh_call_activity_daily, running nightly at 02:00 — against
the standing constraint that it should never be invoked or scheduled. The
call_activity_daily table checked clean (16 rows, zero with a stale
org_id), so yesterday's backfill left no damage there. The job is still
scheduled and the decision to unschedule it is open.
| Item | Status |
|---|---|
set_org_id_from_user rewriteResolution order with the owner exclusion in branch (b) — the part that matters. |
Landed correctly. Attached to analysis_history, trial_state, subscriptions, notifications. Not reported by the agent; confirmed by reading the migration. |
profiles_autofill_org_id auditRequested, not delivered. |
Not fixed. Still selects the earliest accepted membership with no owner exclusion — which is always the personal org. Four writers to profiles.org_id exist; only accept_relationship_invite has the correct preference order. Two others resolve to the personal org and are masked only by ordering. |
Notifications with null org_id |
Left in place. The migration's guard excludes them regardless of whether the profile has an org, so the inline justification is asserted rather than verified. Recommendation on record: resolve them — a null in a column every RLS policy filters on makes those rows invisible to everyone permanently. |
Kevin walked SDR Hive as a real inbound user with fresh accounts. Every finding was then traced
to a file and line before being recorded. Full detail lives in
docs/UAT_2026-07-29_FINDINGS.md. What follows is the shape of it.
ae_roster is a private address book with no invite and no linked account.
relationships is the real pairing. Nothing connects them and no invite action is
reachable from the SDR side.team.functions.ts:404transactional_emails; the
drain is an HTTP route and no pg_cron job calls it. If Lovable doesn't invoke
it as a platform job, every email is stuck — including the relationship invite, which would
make the AE on-ramp fail silently even once built. Read-only diagnostic pending.
useState(defaultRole) captures a stale first render so
?role=ae deep links submit SDR; the post-signup redirect uses local form state
while the persistence RPC fails on a bare console.warn, so UI and database can
diverge silently; and routing falls back to guessing the role from an email-suffix regex —
a UAT fixture that shipped.
current_role is derived from roles[0] while the checkbox handler
appends, so the pre-existing first role always wins. ADMIN sits as a peer
checkbox and can become the active role — it has no dashboard and grants org-wide read
across every rep's MBO, coaching notes, and 1:1s.
target="_blank" on both links; the privacy page's Back button guards on a history length that is always 1 in a fresh tab. Form state survives — in the other tab. Fix is a scroll-to-accept modal; the component already exists twice in the codebase._app.sdr.follow-ups.tsx:491transcript_id instead of text,
validates config against an enum allowlist, and caches on a config hash so results are real,
genuinely config-driven, and effectively free once warmed.
IMPACT_COACHING
is the only library that exists — 24 more cards needed for MEDDPICC, BMANTR, MEDDIC, and BANT.
That is authoring doctrine, not a code change.
testhive_leads.products_sold with separate save buttons. Last save wins silently.
The UI carries a "remember to save" note — someone knew and papered it with an instruction.
industry and sub_industry both mapped to industry; one
silently wins. Duplicate detection covers company names only. Otherwise the import handled a
26-column arbitrary file with suggested matches cleanly — that part works well.
The pattern across all five trial-expiry defects: none of them throw, none of them log, and all of them fire on a date rather than an action. They were invisible today because every test account is under fourteen days old.
| Defect | What it would have cost |
|---|---|
| Pilot cap enforced at 10 | Every HQ-approved pilot user capped at 10 analyses instead of 60, with every display honestly reporting 10 — a consistent, wrong number. A pilot designed to prove value on 60 real calls would have died at call eleven. Zero users affected; HQ approval hadn't been used on a free-tier lead yet. |
| Day-15 feature lockout | Case studies, battle cards, proof points, and AE tuning all closing two weeks into a pilot, with the product telling the user they'd exhausted 60 analyses when they may have run three. This would have arrived after the design review and during the first pilot conversations. |
| Silently degraded analyses | The most expensive one. Post-day-15 users stopped receiving battle-card context in their analyses — no error, no visible difference in the output. A worse analysis that looks exactly like a good one. The product would have been quietly disproving its own value proposition, and no one would have known why the pilot underperformed. |
| "Trial expired · Upgrade now" email | A nightly cron telling pilot participants their trial had ended and linking them to a pricing page. To an enterprise prospect mid-evaluation, that reads as either a bait-and-switch or a company that doesn't know what it sold them. |
| Hidden 50-analysis cap | A second enforcer blocking users ten analyses early, contradicting both the enforced cap and every number shown on screen. |
| Card-entry paywall two clicks from the AE dashboard | An invited AE reaching "Enter a card to activate — we won't charge for 14 days" during a pilot that has no billing. Directly contradicts the pilot framing. |
| Seven surfaces of trial and upgrade copy | Against a landing page promising zero AI training and Anthropic zero-retention, inconsistent commercial copy is a trust problem, not a cosmetic one. Enterprise security reviews read the surfaces together. |
Recorded plainly. Three of today's misses were mine, and they share a shape worth naming so the next session doesn't repeat them.
trial_state.status being closed, we asked for every reader and writer of the column and every path that can block an analysis. Both lists immediately surfaced things that scoped questions had missed. Enumerate, then scope.CAP = 10 in the analyze path. But
call_limit is NOT NULL DEFAULT 60, so the null-coalesce is
unreachable. I reasoned about a fallback without checking whether the column could produce a
null. Walked back the same session; it cost a delay.
trial-expired.tsx was reachable through the credit-reset cron. I had traced cron
endpoints but not the enqueue calls inside them. Acting on that advice would have
produced a runtime failure in a nightly job instead of a bad email.
Ordered by what unblocks the most. The first two are cheap and answer questions; the third is the one that makes five walkthrough steps testable and seven findings disappear.
set_org_id_from_user rewrite and the only open security
question on the project. Everything else on the list is a product question.
email_send_log rows for the test accounts, pgmq queue depth, last
successful send, and whether Lovable invokes the drain route as a platform job. Determines
whether the AE invite can work at all once built.
ae_roster / relationships
consolidation rather than forcing it. Unblocks seven findings and five script steps.
defaultRole; remove the email-suffix role
guess; HQ active-seat control separated from capability checkboxes; scroll-to-accept terms
modal; make "Send to AE" an actual action.
CoachingCard entries for MEDDPICC, BMANTR, MEDDIC, and BANT, drafted
from Get to No Quickly for Kevin's correction. That is backlog item 7 — the doctrine
map — arriving as product content rather than a diagram.
ADMIN is one checkbox away in the user editor.enforce_rolescurrent_role derivation and the four writers to profiles.org_id are direct evidence for both.refresh_call_activity_daily is scheduled_shared/lead.ts CAP = 10, generate-briefing's hardcoded cap ignoring call_limit, and the 429 message hardcoding "60". None reachable on the live path. Needs a redeploy, not just a file change.The direct track has moved. Kevin is still working toward a final individual to communicate with, and concerns have been raised about the current state of the partner motion. Next step is outreach to additional Adobe partner connections. Objective: close down the DCN pilot scope within the next one to two weeks.