Pipeline funnel v2: 4-stage enum + migration 0007 + derived grid signals

Collapse the inherited 6-stage opportunity funnel to the locked 4-stage
per-investor funnel (lead -> engaged -> diligence -> commitment), terminal at
commitment. Migration 0007 remaps existing stage values (outreach/meeting ->
engaged, due_diligence -> diligence, committed/funded -> commitment) and
archives the stray 'lost' value (the grid row is left intact). Inject read-only
existing_investor (total_invested>0), last_activity_at, and staleness
(''/'aging'>=30d/'stale'>=60d) into the grid GET, stripped on write. Frontend:
4-stage chip tints + Pipeline board / opp-form / mock on the new enum.

The visible desktop existing-investor star + staleness recency column + the
Stale saved view are deferred to mobile Phase 3 (data is injected + test-locked
now, so that phase stays pure-frontend). Longshot was already retired by prior
cleanup -- no-op.

Tests: test_pipeline_stages_v2.py (migration remap + derivation boundaries) +
updated grid-pipeline-link / soft-delete / nl_query; 36/36 green, render-smoke
green, fresh-DB migrate clean.
This commit is contained in:
Keysat
2026-06-19 12:54:12 -05:00
parent fe62df1a14
commit e46dd36517
12 changed files with 420 additions and 95 deletions
@@ -0,0 +1,14 @@
-- Reversal of 0007_pipeline_stages_v2.sql (manual; .down files are never auto-applied).
--
-- BEST-EFFORT: the 6->4 stage collapse is lossy and cannot be perfectly inverted (the
-- pattern other .down files here share -- e.g. 0005 cannot DROP COLUMN on old SQLite). It
-- restores VALID legacy 6-stage values, choosing a representative for each collapsed pair:
-- engaged was outreach OR meeting -> 'meeting' (representative)
-- diligence -> 'due_diligence' (exact)
-- commitment was committed OR funded -> 'committed' (representative)
-- Opportunities archived from the stray 'lost' value still carry stage = 'lost' but cannot be
-- re-identified as "archived by this migration" vs archived for other reasons, so they are
-- left archived; un-archive (clear deleted_at) manually if a rollback truly needs them back.
UPDATE opportunities SET stage = 'meeting' WHERE stage = 'engaged';
UPDATE opportunities SET stage = 'due_diligence' WHERE stage = 'diligence';
UPDATE opportunities SET stage = 'committed' WHERE stage = 'commitment';
@@ -0,0 +1,25 @@
-- Pipeline funnel v2 — collapse the inherited 6-stage opportunity funnel into the locked
-- 4-stage per-investor funnel: lead -> engaged -> diligence -> commitment, terminal at
-- commitment. See ROADMAP "Pipeline stages + investor flags/labels -- LOCKED SPEC" (2026-06-19)
-- and server.PIPELINE_STAGES.
--
-- DATA-ONLY + DEPLOYMENT-STATE-INVARIANT (migrations guide): targets stage values
-- structurally, so it is a no-op on a fresh DB (no opportunities) and remaps deterministically
-- on a populated one.
-- outreach, meeting -> engaged (a two-way conversation has begun; "meeting" was an
-- activity, not a position, so it folds in here)
-- due_diligence -> diligence
-- committed, funded -> commitment (terminal; post-commit $ lives in the grid fund cell,
-- and fund admin owns post-commitment -- no "funded" stage)
UPDATE opportunities SET stage = 'engaged' WHERE stage IN ('outreach', 'meeting');
UPDATE opportunities SET stage = 'diligence' WHERE stage = 'due_diligence';
UPDATE opportunities SET stage = 'commitment' WHERE stage IN ('committed', 'funded');
-- The stray legacy 'lost' value is not in the new settable enum, and a lost deal is a dead
-- deal: ARCHIVE (soft-delete) the opportunity rather than leave an un-settable stage on a live
-- row. The grid investor row is left fully intact (the grid is canonical); graveyarding the
-- investor stays a human action, never an auto-mutation (human-in-the-loop guardrail). The
-- stage text is left as 'lost' on the archived row for provenance -- it is filtered out
-- everywhere by deleted_at IS NULL.
UPDATE opportunities SET deleted_at = datetime('now'), updated_at = datetime('now')
WHERE stage = 'lost' AND deleted_at IS NULL;