# Lead-Gen Substrate Train (Tracks A + B + C)

> **PARTIAL SUPERSEDE — 2026-04-29.** Track A's `acq_touchpoints` table and all `recordTouchpoint` / `listDealTouchpoints` helpers were removed end-to-end on 2026-04-28/29. Replacement substrate for outreach event audit is `acq_deals.activity_log` (JSONB) appended via `acqDb.recordDealActivity`. Replacement substrate for thread context (Instantly thread UUIDs) is the same `activity_log` — `reply_received` entries now carry thread fields in their payload. Ignore every touchpoint-flavored instruction in this note (table creation, hooks, UI tabs, writers, verification). The `acq_artifacts` table and ICP rubric columns from Track A remain valid. Track B + Track C non-touchpoint content remains valid.

## Why this note exists

This release train ships the Lead-Gen Substrate Generalization across three coordinated tracks:

- **Track A — Additive Primitives.** New `acq_artifacts` (versioned content: audit PDFs, email drafts, etc.) and `acq_touchpoints` (append-only outreach event log) tables, plus ICP rubric columns (`qualification_score numeric`, `qualification_signals jsonb`, `qualification_rubric_key text`) on `acq_companies` and `acq_contacts`. Purely additive schema — no column drops on these entities.
- **Track B — Stateful Trait Generalization.** The `(pipeline_key, stage_key, state_key, activity_log)` quartet from the CRM HITL deal cutover is now applied to `acq_lists`, `acq_list_members`, and `acq_list_companies`. Legacy `acq_lists.status`, `acq_list_*.stage`, and `acq_list_*.stage_updated_at` columns are dropped. `acq_companies` and `acq_contacts` are explicitly **excluded** (batch-ETL pattern, not state-machine).
- **Track C — Campaign Creator UI.** `@elevasis/ui` ships a hooks layer (`useArtifacts`, `useCreateArtifact`, `useTouchpoints`, `useListMembers`, `useListMember`, `useTransitionList`, `useTransitionListMember`, `useTransitionListCompany`, `useDeriveActions`) plus upgraded `LeadGenListsPage` and `LeadGenListDetailPage` (5-tab layout: Overview / Members / Activity / Artifacts / Touchpoints) and a `ListMemberDrawer` deep-linkable via `?member=<id>&memberKind=<contact|company>`.

Concretely, this train introduces:

- a `Stateful` interface + `StatefulSchema` Zod object + generic `TransitionItem<T,TEvent>` / `DeriveActions<T,TAction>` type aliases at `@elevasis/core/business/acquisition/stateful`
- three `StatefulPipelineDefinition` constants on `@elevasis/core/organization-model` — `ACQ_LISTS_LEAD_GEN_PIPELINE` (single stage `lifecycle` with five states: draft / enriching / launched / closing / archived), `ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE`, `ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE` — plus `findPipeline()` helper and `LEAD_GEN_PIPELINE_DEFINITIONS` map
- API routes `GET/POST /acquisition/artifacts`, `GET /acquisition/touchpoints`, `GET /acquisition/lists/:listId/members`, `GET /acquisition/list-members/:memberId`, `PATCH /acquisition/lists/:listId/transition`, `PATCH /acquisition/list-members/:memberId/transition`, `PATCH /acquisition/list-companies/:listCompanyId/transition`
- new flat columns on `acq_artifacts` / `acq_touchpoints` / `acq_lists` / `acq_list_members` / `acq_list_companies` and ICP rubric columns on `acq_companies` / `acq_contacts`

## Applies to

All template-derived projects that:

- consume `@elevasis/core/business/acquisition` types or import the `Stateful` trait / `LEAD_GEN_PIPELINE_DEFINITIONS`
- render the lead-gen lists / list-detail / Campaign Creator surfaces from `@elevasis/ui/features/lead-gen`
- have a project-local `acq_*` schema that mirrors the platform tables (artifacts, touchpoints, lists, list-members, list-companies)
- author workflows under `operations/src/sales/{outreach,prospecting,qualification}/**` that read or write the `acq_lists` / `acq_list_members` / `acq_list_companies` lifecycle columns, or that produce email drafts / audit content / outreach events
- read `acq_companies.qualification_score` / `qualification_signals` / `qualification_rubric_key`, or the parallel columns on `acq_contacts`

Operations-only projects with no lead-gen surface and no `acq_*` tables can ignore this train.

## Required actions

1. Pull template changes with `/git-sync` so the refreshed lead-gen hook surface, `LeadGenListsPage` upgrades, and `ListMemberDrawer` deep-link wiring are available.

2. After the release train is published, update package versions in the project:

```bash
pnpm up @elevasis/core @elevasis/ui @elevasis/sdk --latest
```

3. If your project has project-local `acq_*` tables, run additive + cutover migrations:
   - **Additive (Track A — safe to apply standalone):** `acq_artifacts` and `acq_touchpoints` table creation (canonical 3-policy RLS — `Platform admins have full access to <table>` / `Org members with acquisition.manage can manage <table>` / `Org members can view <table>`). ICP columns added to `acq_companies` and `acq_contacts` (`qualification_score numeric`, `qualification_signals jsonb`, `qualification_rubric_key text`, all nullable).
   - **Stateful trait (Track B — atomic cutover, not dual-write):** add `pipeline_key text NOT NULL`, `stage_key text NOT NULL`, `state_key text NOT NULL`, `activity_log jsonb NOT NULL DEFAULT '[]'` to `acq_lists` / `acq_list_members` / `acq_list_companies`; drop `acq_lists.status`, `acq_list_members.{stage,stage_updated_at}`, `acq_list_companies.{stage,stage_updated_at}` in the same transaction.
   - **Backfill values used in the platform cutover:** `acq_lists` -> `pipeline_key='lead-gen'`, `stage_key='lifecycle'`, `state_key=coalesce(status,'draft')`. `acq_list_members` and `acq_list_companies` -> `pipeline_key='lead-gen'`, `stage_key='outreach'`, `state_key=coalesce(stage,'pending')` (preserves `personalized` / `verified` / `discovered` / `qualified` / `populated` / `extracted` vocabularies losslessly).
   - The cutover is single-shot. Confirm prod row counts on these tables are small enough to accept fix-forward semantics before applying.

4. Replace any direct lifecycle writes on lists / list-members / list-companies with the new transition surface:
   - workflow code that previously wrote `acq_lists.status`, `acq_list_members.stage`, or `acq_list_companies.stage` directly must now route through `acqDb.transitionList(...)` / `transitionListMember(...)` / `transitionListCompany(...)`
   - the API server injects `organizationId` from execution context — never pass `organizationId` from worker payload
   - the platform-side methods append `ActivityEventSchema`-shaped events to `activity_log`; do NOT also write activity rows from the workflow side

5. Replace `enrichmentData.pipeline.{auditDoc, touchpointCount, lastReply}` writers with `acq_artifacts` / `acq_touchpoints` writes:
   - audit PDFs / email drafts / proposals -> `acq_artifacts(kind='audit'|'email_draft'|'proposal', owner_kind='company'|'contact'|'list'|'list_member', version=N, content=<jsonb>, source_execution_id=<execution>)`
   - outreach events -> `acq_touchpoints(direction='outbound'|'inbound', channel='email'|'linkedin'|'sms', kind='initial'|'followup'|'reply'|'nudge', occurred_at=<ts>, payload=<jsonb>, artifact_id=<linked draft>, source_execution_id=<execution>)`
   - `pipeline_status.pipelineStatus` text enum on `acq_companies` is **untouched** (out of trait scope per Decision B1) — that JSONB key remains canonical for company-level state

6. Adopt the qualification rubric columns:
   - LLM qualifiers should write `acq_companies.qualification_score` / `qualification_signals` (jsonb shape: `{ status: 'complete' | 'pending' | ..., result?: ..., reason?: ..., completedAt?: ... }`) and stop writing `pipeline_status.qualification`
   - readers (e.g. `fetch-companies` filters in email-discovery / company-qualification workflows) should use `qualificationSignals?.status === 'complete'` instead of `pipeline_status.qualification?.status`
   - `qualification_rubric_key` is a free-form text key today; an Org OS rubric registry may land in a later train without a schema change

7. If you read `activity_log` rows on lists / list-members / list-companies and pattern-match by `type`, mirror the flat schema already used for `acq_deals`:

```ts
{ type: 'state_change', timestamp, stateBefore, stateAfter, reason? }
{ type: 'stage_change', timestamp, stageBefore, stageAfter, reason? }
{ type: 'task_created', timestamp, taskId }
```

8. If you ship lead-gen UI from `@elevasis/ui/features/lead-gen`, the `lists.tsx` and `list-detail.tsx` pages now expect:
   - `state_key` badge column resolved via `findPipeline('acq.list')` from `LEAD_GEN_PIPELINE_DEFINITIONS`
   - ICP rubric column reading `list.config.qualification.qualificationRubricKey` (em-dash when absent)
   - `stateKeyFilter` Select chip in the FilterBar (options sourced from `ACQ_LISTS_LEAD_GEN_PIPELINE.stages[0].states`)
   - 5-tab detail layout (Overview / Members / Activity / Artifacts / Touchpoints) plus action toolbar fed by `useDeriveActions(list)`
   - Row-click navigation on the Members tab via `window.history.pushState`, with `useSearch({ strict: false })` reading `?member=<id>&memberKind=<contact|company>` to drive `ListMemberDrawer`
   - For the `?member=` deep-link to be typed, add `validateSearch` to the project's `routes/lead-gen/lists.$listId.tsx` route file (optional; the drawer reads search params untyped via `strict: false`)

9. Redeploy `operations` after the package upgrades so workers pick up the new typed adapter surface, the artifact/touchpoint writers, and the qualification column writes:

```bash
pnpm -C operations exec elevasis-sdk deploy --prod
```

## Verification

Run from the project root after package updates and any DB migration:

```bash
pnpm -C ui check-types
pnpm -C ui build
pnpm -C operations check-types
pnpm -C operations exec elevasis-sdk check
```

Then exercise the lead-gen surface:

- open the lists page — confirm the new state-key badge column renders, the ICP rubric column shows either the rubric key or an em-dash, and the FilterBar exposes a state-key Select chip
- open a list detail — confirm the 5-tab layout (Overview / Members / Activity / Artifacts / Touchpoints), the state-machine progression badge in the Overview tab, and that the Action toolbar in `PageTitleCaption` renders the actions returned by `useDeriveActions(list)`
- click a member row — confirm the drawer opens, the URL gains `?member=<id>&memberKind=<contact|company>`, the activity / touchpoints / artifacts tabs render, and `onClose` clears both params
- after the operations redeploy, run a campaign through the qualification + email-discovery workflows; confirm new rows in `acq_artifacts`, new rows in `acq_touchpoints`, and non-null `qualification_score` / `qualification_signals` on the affected `acq_companies` rows

If a workflow that historically wrote `acq_lists.status` / `acq_list_*.stage` throws Zod errors against `StatefulSchema` or fails on missing `state_key`, route it through `acqDb.transitionList(...)` / `transitionListMember(...)` / `transitionListCompany(...)`.

## Not handled by /git-sync

- `/git-sync` does not publish or upgrade `@elevasis/core`, `@elevasis/ui`, or `@elevasis/sdk`.
- `/git-sync` does not run the project-local `acq_artifacts` / `acq_touchpoints` table creation, ICP-column additions, or the Stateful-trait cutover on `acq_lists` / `acq_list_members` / `acq_list_companies`. Each of those is owner-authorized and atomic.
- `/git-sync` does not redeploy the project's `operations` bundle to dev or prod.
- `/git-sync` does not rewrite project-owned workflow code that bypasses `transitionList` / `transitionListMember` / `transitionListCompany` to write list / member / company stage directly.
- `/git-sync` does not retire project-local `enrichmentData.pipeline.{auditDoc, touchpointCount, lastReply}` writers — those must be migrated to `acq_artifacts` + `acq_touchpoints` by the project owner.
- `/git-sync` does not migrate workflow qualifiers off `pipeline_status.qualification` onto `qualification_score` / `qualification_signals` / `qualification_rubric_key`.
