2afed210cb
Structural fix for the duplicate-people class of bug: instead of matching a grid contact "pill" to a contacts row heuristically by name/email (which drifted and caused the 1406 double-count), link them by id. Backend: - Migration 0004: fundraising_contacts.contact_id (additive, nullable, logical FK to contacts(id)) + index. Paired down migration. - sync_fundraising_relational now stores the id that _upsert_contact_from_fundraising already returns, so every grid contact carries its contacts-table id. - _backfill_grid_contact_ids: one-time, idempotent backfill on startup (re-runs the grid sync once if any row lacks contact_id), so existing data links immediately. - entity_resolution: grid pass prefers the explicit contact_id link (match_kind 'grid_link') over heuristic email / name+investor, guarded by a PRAGMA check so older DBs without the column still work. Frontend: - Fundraising grid "+ Row" -> "+ Investor" (clear, single investor entry point). - Contacts page: the "+ Add Contact" trigger is replaced by a pointer to the grid; the page is now a read/search/edit view (ContactDetailPanel still edits all fields). New people are added from the grid. No contact data is removed. Tests: backend/ingest/test_entity_resolution.py extended (explicit-link case, 11/11) and a new backend/test_grid_contact_link.py integration test (init_db applies 0004, sync populates contact_id to the right contact, re-sync is idempotent). py_compile + frontend html.parser clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
18 lines
1.0 KiB
SQL
18 lines
1.0 KiB
SQL
-- Grid/contacts unification — explicit link from a fundraising-grid contact
|
|
-- "pill" to its row in the contacts table.
|
|
--
|
|
-- ADDITIVE + REVERSIBLE (CLAUDE.md guardrail #3): adds one nullable column.
|
|
-- Until now a grid contact was tied to its contact only by name/email matching,
|
|
-- which drifted and produced the people double-count. fundraising_contacts.contact_id
|
|
-- records the real link (populated by sync_fundraising_relational, which already
|
|
-- upserts the contact and now stores its id). entity_resolution prefers this link
|
|
-- over heuristic matching.
|
|
--
|
|
-- contact_id is a LOGICAL foreign key to contacts(id). It is intentionally NOT a
|
|
-- declared SQLite FOREIGN KEY: contacts are soft-deleted (never hard-deleted), so
|
|
-- there is nothing to cascade, and SQLite's ALTER TABLE ADD COLUMN cannot add an
|
|
-- enforced FK cleanly. Nullable so existing rows are valid until backfilled.
|
|
ALTER TABLE fundraising_contacts ADD COLUMN contact_id TEXT;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_fundraising_contacts_contact ON fundraising_contacts(contact_id);
|