Skip to content

feat: let a new account actually do something - #11

Merged
ralyodio merged 3 commits into
mainfrom
feat/add-prospects-and-email-verification
Aug 12, 2026
Merged

ralyodio merged 3 commits into
mainfrom
feat/add-prospects-and-email-verification

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Logging in gave an empty app with no way to fill it. The cause was structural, not cosmetic.

What was actually broken

Registration created a user, organisation and workspace — but no offering and no campaign. A campaign requires an offering_id; a prospect requires a campaign. And there was no route that could add a person at all — the pipeline had only ever been driven from a local script. So the app was empty and had no path out of it.

What this adds

  • Registration provisions a starter offering and campaign. Accounts made before this are backfilled on first use rather than told to create something the UI does not expose.
  • POST /api/v1/prospects runs the full chain for a GitHub handle. Synchronous, because a first-run user needs to see something appear, and it reports identities linked and signals found rather than "queued". @handle and pasted profile URLs are accepted. A handle that resolves to nothing answers 200 with a reason — a typo is information, not a server fault.
  • GET /api/v1/people backs a real prospect list and a detail page showing linked identities with confidence and signals with source links.
  • Today's empty state now distinguishes "no prospects yet" from "no recommendations yet", and offers the action that fixes the first.

Email verification

Format was already validated server-side (isPlausibleEmail, z.string().email()); what was missing was any proof the address exists.

  • Tokens stored as SHA-256 like sessions, superseded rather than accumulated on resend, identical response for unknown/expired/consumed.
  • The gate is on sending, not signing in — research, prospects and drafts all work while the mail is in flight. Approve and execute return 403 email_unverified.
  • Accounts predating this are grandfathered by the migration.
  • A send failure never fails the signup; it is audited instead.
  • Resend over plain HTTP, no SDK. With no key the link is logged, so a fresh checkout still completes a signup.

Structural

apps/worker → packages/pipeline. Both the API and the background loop run the chain, and an app importing another app's source made the dependency direction a lie. apps/worker held nothing else, so it is gone.

Verification

  • bun run check — format, typecheck, 395 tests, all passing
  • apps/web production build passes; /prospects, /prospects/[id] and /verify all present

⚠️ Migration 0006 was applied to production Turso during development (.env points at prod). It is additive — new column, new table, grandfathering update — and the container applies pending migrations at boot anyway.

🤖 Generated with Claude Code

ralyodio and others added 3 commits August 11, 2026 15:10
Adds `packages/ai` — the only package in the tree that talks to a model. The
composer writes the message body for a recommendation, and everything around
it exists to make that output safe to show a reviewer.

Two properties are structural rather than requested:

  1. It can only see grounded inputs. The prompt is assembled from stored
     evidence, stored facts and the customer's own offering. There is no path
     by which the model learns something it may not cite.
  2. Its output is checked, not trusted. Eight deterministic §14.2 gates run
     on every draft — grounding overlap, unsupported claims, identity
     confidence, flattery, spam patterns, cross-prospect duplication,
     sensitive topics, policy. A draft that fails is rewritten once, naming
     the exact invented fragments; still failing, it is withheld.

Withheld is a working state, not an error. The card keeps the prospect, the
evidence and the recommended action, and the reviewer writes the message. A
bad draft next to a caveat is still a bad draft someone might approve.

The undecidable question "is this claim true?" is inverted into a decidable
one: does every specific assertion — quoted phrase, number, mid-sentence
proper noun — appear in stored evidence? That is what `checks.ts` answers,
and it is why no model output reaches a human unexamined.

Also:

- `POST /recommendations/:id/draft` composes or recomposes on demand, returns
  200 with `drafted: false` and a reason when withholding, and audits it.
  A user-edited draft is never discarded by a recompose.
- The approval card gains "Write draft" / "Rewrite", and states plainly why
  no draft exists rather than offering a retry that cannot help.
- Prompt caching splits the stable offering/voice prefix from the
  per-prospect half, so a campaign pays for the prefix once.
- The composer is optional infrastructure: without ANTHROPIC_API_KEY the
  queue still runs end to end and drafting returns 503. Refusing to boot
  would take the system down for a feature designed to produce nothing.

No model decides a merge, a score, or a policy outcome. 367 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A fresh login was an empty app with no way to fill it. The cause was not a
missing screen: registration created a user, organisation and workspace, but
no offering and no campaign — and a campaign requires an offering, a prospect
requires a campaign. There was no route that could add a person at all. The
pipeline had only ever been driven from a local script.

- Registration now provisions a starter offering and campaign. Existing
  accounts are backfilled on first use rather than told to create something
  the UI does not expose.
- POST /api/v1/prospects runs the full chain for a GitHub handle. It is
  synchronous because a first-run user needs to see something appear, and it
  reports identities linked and signals found rather than "queued". A handle
  that resolves to nothing answers 200 with a reason: a typo is information,
  not a server fault.
- GET /api/v1/people backs a real prospect list and detail page, so the
  evidence behind a recommendation is inspectable.
- Today's empty state now distinguishes "no prospects yet" from "no
  recommendations yet" and offers the action that fixes the first.

The pipeline moves from apps/worker to packages/pipeline. Both the API and
the background loop run it, and an app importing another app's source made
the dependency direction a lie. apps/worker held nothing else, so it is gone.

Email verification ships alongside it. Format was already checked; what was
missing was any proof the address exists. Tokens are stored as a SHA-256
digest like sessions, superseded rather than accumulated on resend, and every
failure answers identically so a guessed token learns nothing. The gate is on
sending, not on signing in — research and drafting still work while the mail
is in flight. Accounts predating this are grandfathered, and a send failure
never fails the signup that triggered it.

Resend is reached over plain HTTP; with no key the link is logged instead, so
a fresh checkout still completes a signup.

395 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	.env.example
#	README.md
#	apps/api/src/app.test.ts
#	apps/api/src/app.ts
#	apps/server/src/index.ts
#	bun.lock
#	packages/pipeline/package.json
@ralyodio
ralyodio merged commit 6b1d2a0 into main Aug 12, 2026
4 checks passed
@ralyodio
ralyodio deleted the feat/add-prospects-and-email-verification branch August 12, 2026 01:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant