# CRM State-Change UI + Lead-Gen Processing Status Migration

## Why this note exists

This release train ships two coordinated changes that both flow through the `@elevasis/core` + `@elevasis/ui` + `@elevasis/sdk` package family.

1. **CRM state-change UI + canonical state source.** CRM `state_key` values are now defined canonically in `@elevasis/core/organization-model` as `CRM_PIPELINE_DEFINITION` (mirrors lead-gen's `ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE` shape). Eight `CRM_*_STATE` constants and a `getValidStatesForStage(definition, stageKey)` helper are exported. The API now exposes `PATCH /api/deals/:dealId/state` with server-side validation against the per-stage allowed-state set, and the deal drawer ships a `StateSelect` plus an Unstaged-deal staging affordance. Manual state changes log `state_changed_manually` (vs. workflow-driven `state_change`).

2. **Lead-gen processing-status migration.** `processing_state` JSONB stage values shift from boolean flags to status strings drawn from `ProcessingStageStatusSchema = 'success' | 'no_result' | 'skipped' | 'error'`. Legacy boolean `true` values continue to read as `success` for the foreseeable future via API normalization. `ListStageProgressSchema` expands from `{ done, total }` to attempted-coverage + per-status counts (`total`, `attempted`, `success`, `noResult`, `skipped`, `error`, `other`, `notAttempted`). Lead-gen list-detail UI renders attempted percentage with stacked status segments. Local `listTool` writers (`updateCompanyStage` / `updateContactStage`) now accept an optional `status` argument; omitted writes default to `success`.

## Applies to

All template-derived projects that:

- consume `@elevasis/core/business/acquisition` types (`ListStageProgressSchema`, `ProcessingStageStatusSchema`, `UpdateCompanyStageParams`, `UpdateContactStageParams`, `TransitionDealStateRequestSchema`)
- import from `@elevasis/core/organization-model` (the new `CRM_PIPELINE_DEFINITION` + `CRM_*_STATE` constants are exposed for the first time in this release)
- render the CRM deal drawer or kanban surfaces from `@elevasis/ui` (the drawer now slots `StateSelect` and an Unstaged staging block; `useTransitionState` is a new exported hook)
- render the lead-gen list-detail progress bars from `@elevasis/ui` (stacked segment rendering replaces single fills)
- author lead-gen workflows that call `acqDb.updateCompanyStage(...)` / `acqDb.updateContactStage(...)`
- author CRM workflows that hardcode `state_key` string literals (`discovery_link_sent`, `discovery_replied`, `reply_sent`, `followup_{1,2,3}_sent`, etc.) — these should now import named constants from `@elevasis/core/organization-model`

Operations-only projects with no CRM surface, no `acq_deals` table, and no lead-gen list workflows can ignore this train.

## Required actions

1. Pull template changes with `/git-sync` so the refreshed package baselines (`core/package.json`, `ui/package.json`, `operations/package.json`) and any propagated scaffold doc updates land.

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

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

3. **Lead-gen workflows** — extend any local `listTool` wrapper signatures to accept the new optional `status` parameter and pass an explicit non-success status where the workflow can distinguish outcomes:

```ts
updateCompanyStage: (params: {
  listId: string
  companyId: string
  stage: 'extracted'
  status?: 'success' | 'no_result' | 'skipped' | 'error'
  executionId?: string
}) => platform.call({ tool: 'list', method: 'updateCompanyStage', params }) as Promise<void>
```

Recommended status mapping per stage (`populated`, `extracted`, `discovered`, `verified`, `qualified`, `personalized`, `uploaded`) is documented in `apps/docs/content/docs/in-progress/active-development/sdk-changes/ship/lead-gen-processing-status-migration.mdx` (Step 4 table). Omitted `status` defaults to `success` so existing call sites keep working unchanged.

4. **CRM workflows** — replace any hardcoded CRM `state_key` string literals with imports from `@elevasis/core/organization-model`:

```ts
import {
  CRM_DISCOVERY_REPLIED_STATE,
  CRM_DISCOVERY_LINK_SENT_STATE,
  CRM_REPLY_SENT_STATE,
  CRM_FOLLOWUP_1_SENT_STATE,
  // ...
} from '@elevasis/core/organization-model'

// before:
await acqDb.transitionItem({ ..., stateKey: 'reply_sent' })

// after:
await acqDb.transitionItem({ ..., stateKey: CRM_REPLY_SENT_STATE.stateKey })
```

The runtime string values are unchanged; this is a mechanical substitution that aligns workflows with the canonical source. The API now validates `stateKey` for the new `PATCH /deals/:dealId/state` route, but `transitionItem` retains existing acceptance behavior for backward compatibility during rollout.

5. **Reading `acq_deal_activity_log`** — pattern-matchers should accept `state_changed_manually` as a new event kind alongside the existing `state_change`. Manual state edits carry a user id and optional reason; workflow-driven state writes continue to use `state_change`.

6. **Lead-gen progress consumers** — if your project reads `GET /acquisition/lists/:listId/progress`, the response shape now exposes attempted-coverage fields. The legacy `{ done, total }` shape no longer ships; consumers should switch to `attempted / total` for completion percentages and use the per-status counts (`success`, `noResult`, `skipped`, `error`, `other`, `notAttempted`) for outcome rendering.

7. **`processing_state` data shape** — running rows now carry string statuses (`"success" | "no_result" | "skipped" | "error"`). Legacy `true` values continue to be normalized server-side. Direct SQL consumers should treat both shapes as readable; new writes should always emit strings.

## Verification

After upgrading and applying the actions above:

- `pnpm check-types` clean across project packages
- `pnpm test` for any operations workflows that exercise `listTool.update*Stage` signatures
- Spot-check a CRM deal drawer in your project's command-center: state dropdown should appear under the Summary block when the deal has a stage; an "Unstaged" staging affordance should appear when `stage_key` is null
- Spot-check a lead-gen list-detail page: progress bars should render stacked segments, with the headline percentage reflecting attempted coverage rather than only successful rows
- `GET /acquisition/lists/:listId/progress` response should carry the new attempted-coverage shape
- Manual deal state changes through the new UI should append `state_changed_manually` rows in `acq_deal_activity_log`

## Not handled by /git-sync

`/git-sync` propagates template-authored files (package baselines, scaffold doc copies, sync-managed shells) but does NOT:

- run `pnpm up @elevasis/core @elevasis/ui @elevasis/sdk --latest` for you — bump deps explicitly after this train publishes
- rewrite hardcoded `state_key` literals in your CRM workflows — Action 4 is a manual substitution
- backfill historical `processing_state: true` JSONB values to `"success"` strings — the platform monorepo ran a one-shot SQL conversion for its prod data on 2026-04-29; your project owns its own backfill if you want clean string-only rows. API normalization makes the backfill optional for correctness.
- regenerate workflow status writes — Action 3 is a code-level change in your operations workflows
- rebuild your `command-center` after the `@elevasis/ui` upgrade — run your project's normal build/dev cycle
