1564c087bf
Removals (net -570 lines): - Delete the Instructions and Feedback (feature_requests) pages + backend. - Retire lp_profiles + investor_type across server, ingest, and seeds; migration 0008 drops both empty tables (a sanctioned one-off exception to never-hard-delete). 0001's lp_profiles ALTER is removed so a fresh DB doesn't break the migration chain (live DBs already applied it). Fixes: - Email sync: a transient timeout no longer terminally parks a mailbox; the scheduler retries 'retrying' each cycle and re-includes errored accounts on an hourly backoff, so stuck mailboxes self-heal. - Mobile Contacts: page through the full directory (server caps 500/page) -- one fetch silently truncated at 720, hiding people from the list and from search. - Mobile email review: clock icon to set a reminder inline; approval cards show date/time. New: - Admin-only purge of soft-deleted rows (Settings -> Admin; type-to-confirm, refuses any row still linked to live data). Tests: 45/45 (adds test_sync_ready + test_purge_soft_deleted). Reviewer pass applied (NULL reminders.contact_id on contact purge). Bumped to v0.1.0:104.
121 lines
7.2 KiB
SQL
121 lines
7.2 KiB
SQL
-- Phase 0 — Workstream A2: foundation schema for the agentic system.
|
|
--
|
|
-- ADDITIVE AND REVERSIBLE ONLY (CLAUDE.md guardrail #3): this migration adds
|
|
-- new tables and new nullable columns alongside the existing CRM. It never
|
|
-- drops, renames, or rewrites existing data. Its reversal is 0001_phase0_foundation.down.sql.
|
|
--
|
|
-- Applied once at startup by backend/core_migrations.py, tracked in the
|
|
-- schema_migrations ledger. Safe to leave in place; the canonical layer it
|
|
-- creates starts EMPTY and is populated later by entity resolution (A4/B3).
|
|
|
|
-- ============================================================================
|
|
-- 1. canonical_entities — the single, model-agnostic identity for an LP /
|
|
-- organization / person. Both the classic contacts/lp_profiles model and the
|
|
-- fundraising_* grid map INTO this; neither existing model is demoted.
|
|
-- IDs are full-length (e.g. 'lp_' + uuid4 hex), NOT the 8-char truncated
|
|
-- UUIDs used elsewhere in the CRM, so they are safe as the index/payload key.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS canonical_entities (
|
|
id TEXT PRIMARY KEY,
|
|
entity_kind TEXT NOT NULL, -- 'lp' | 'organization' | 'person'
|
|
display_name TEXT NOT NULL,
|
|
primary_email TEXT,
|
|
-- Phase-0 LP/prospect fields (model-agnostic home):
|
|
thesis_fit TEXT,
|
|
segment TEXT,
|
|
accreditation_status TEXT, -- free-text until counsel defines the vocabulary (guardrail #6)
|
|
qp_status TEXT,
|
|
warmth_score REAL,
|
|
source TEXT,
|
|
owner_id TEXT REFERENCES users(id),
|
|
last_touch_at TEXT,
|
|
notes TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
deleted_at TEXT -- soft-delete (never hard-delete; guardrail #3)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_canonical_kind ON canonical_entities(entity_kind);
|
|
CREATE INDEX IF NOT EXISTS idx_canonical_email ON canonical_entities(primary_email);
|
|
CREATE INDEX IF NOT EXISTS idx_canonical_owner ON canonical_entities(owner_id);
|
|
|
|
-- ============================================================================
|
|
-- 2. entity_links — resolution map. Every source row (a contacts row, a
|
|
-- fundraising_investors row, etc.) and every email/name variant points at the
|
|
-- canonical entity it resolves to. This is how name variants collapse to one id.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS entity_links (
|
|
id TEXT PRIMARY KEY,
|
|
canonical_id TEXT NOT NULL REFERENCES canonical_entities(id) ON DELETE CASCADE,
|
|
source_model TEXT NOT NULL, -- contacts|organizations|lp_profiles|fundraising_investors|fundraising_contacts|email_address|alias
|
|
source_id TEXT, -- the local PK in that model (NULL for a bare email/name alias)
|
|
match_value TEXT, -- normalized email or name variant
|
|
match_kind TEXT NOT NULL, -- exact_email|name_variant|domain|manual
|
|
confidence REAL DEFAULT 1.0,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
UNIQUE(source_model, source_id, match_value)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_entity_links_canonical ON entity_links(canonical_id);
|
|
CREATE INDEX IF NOT EXISTS idx_entity_links_match ON entity_links(match_value);
|
|
CREATE INDEX IF NOT EXISTS idx_entity_links_source ON entity_links(source_model, source_id);
|
|
|
|
-- ============================================================================
|
|
-- 3. interaction_log — APPEND-ONLY record of every agent action and every human
|
|
-- touch (guardrail #5). Distinct from audit_log (which is mutation-diff-only
|
|
-- and has no actor/agent dimension). Nothing in this table is ever updated or
|
|
-- deleted by convention.
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS interaction_log (
|
|
id TEXT PRIMARY KEY,
|
|
ts TEXT NOT NULL DEFAULT (datetime('now')), -- event time
|
|
actor_type TEXT NOT NULL, -- human | agent | system
|
|
actor_id TEXT, -- users.id, or an agent name (Scout/Analyst/...)
|
|
action TEXT NOT NULL, -- e.g. note.created | email.matched | enrichment.written | search.run
|
|
target_type TEXT, -- canonical_entity | contact | communication | opportunity | ...
|
|
target_id TEXT, -- canonical_entities.id where possible
|
|
payload TEXT, -- JSON blob with the action detail
|
|
source TEXT, -- crm_ui | mcp | ingest | scout | ...
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_interaction_target ON interaction_log(target_type, target_id);
|
|
CREATE INDEX IF NOT EXISTS idx_interaction_ts ON interaction_log(ts);
|
|
CREATE INDEX IF NOT EXISTS idx_interaction_actor ON interaction_log(actor_type, actor_id);
|
|
|
|
-- ============================================================================
|
|
-- 4. relationship_edges — derived graph of who-knows-whom between canonical
|
|
-- entities. Starts EMPTY; seeded later from email_investor_links + calendar +
|
|
-- X follower overlap (Analyst, Phase 2).
|
|
-- ============================================================================
|
|
CREATE TABLE IF NOT EXISTS relationship_edges (
|
|
id TEXT PRIMARY KEY,
|
|
src_id TEXT NOT NULL REFERENCES canonical_entities(id) ON DELETE CASCADE,
|
|
dst_id TEXT NOT NULL REFERENCES canonical_entities(id) ON DELETE CASCADE,
|
|
edge_type TEXT NOT NULL, -- email_corr | calendar | x_follow | intro | colleague
|
|
source TEXT NOT NULL, -- provenance of this edge
|
|
strength REAL DEFAULT 0,
|
|
directed INTEGER DEFAULT 0,
|
|
evidence TEXT, -- JSON supporting detail
|
|
first_seen_at TEXT,
|
|
last_seen_at TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
UNIQUE(src_id, dst_id, edge_type, source)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_rel_src ON relationship_edges(src_id);
|
|
CREATE INDEX IF NOT EXISTS idx_rel_dst ON relationship_edges(dst_id);
|
|
|
|
-- ============================================================================
|
|
-- 5. Soft-delete columns on existing tables. Additive nullable columns; the CRM
|
|
-- currently HARD-deletes everywhere (guardrail #3 gap). Adding the column is
|
|
-- safe now; switching the DELETE handlers to set it instead of hard-deleting
|
|
-- is a separate, reviewed code change.
|
|
-- ============================================================================
|
|
ALTER TABLE contacts ADD COLUMN deleted_at TEXT;
|
|
ALTER TABLE organizations ADD COLUMN deleted_at TEXT;
|
|
ALTER TABLE opportunities ADD COLUMN deleted_at TEXT;
|
|
ALTER TABLE communications ADD COLUMN deleted_at TEXT;
|
|
-- lp_profiles ALTER removed (v0.1.0:104): the lp_profiles table is dropped in
|
|
-- 0008_drop_retired_tables.sql and is no longer created by init_db(), so this
|
|
-- ALTER would fail "no such table" on a fresh install. Live DBs already applied
|
|
-- this migration (with the original ALTER) before lp_profiles was dropped, so
|
|
-- removing the line here only affects fresh DBs — same end state either way.
|