Soft-delete + source-count diagnostics; thesis v4 (0.1.0:47)

- DELETE handlers soft-delete (set deleted_at) + cascade contact -> opps/comms/lp
  instead of hard-deleting (guardrail #3); list queries filter deleted rows.
- ingest: chunking excludes soft-deleted records; qdrant delete-by-source-id;
  sync prunes soft-deleted records' vectors incrementally.
- /api/system/status returns raw source-record counts for sanity-checking.
- docs/thesis-seed-v4.md (no "bet" language, scarcity-forward, freedom-tech as
  a banner option, tightened pillars, reworked segments + edge).

Soft-delete verified via the running HTTP server (delete -> hidden + row kept).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Keysat
2026-06-05 12:20:38 -05:00
parent bdf9bec4ff
commit 3c31b1e8a5
8 changed files with 144 additions and 17 deletions
+25 -8
View File
@@ -2012,7 +2012,7 @@ class CRMHandler(BaseHTTPRequestHandler):
(SELECT MAX(communication_date) FROM communications WHERE contact_id = c.id) as last_contact_date
FROM contacts c
LEFT JOIN organizations o ON c.organization_id = o.id
WHERE 1=1
WHERE 1=1 AND c.deleted_at IS NULL
"""
args = []
@@ -2197,7 +2197,13 @@ class CRMHandler(BaseHTTPRequestHandler):
return self.send_error_json("Contact not found", 404)
_sync_contact_to_fundraising_state(conn, row_to_dict(existing), actor_user_id=user['user_id'], remove=True)
conn.execute("DELETE FROM contacts WHERE id = ?", (contact_id,))
# Soft-delete (guardrail #3 — never hard-delete): mark deleted_at and
# cascade to the contact's opportunities, communications, and lp_profile.
_ts = now()
conn.execute("UPDATE contacts SET deleted_at = ?, updated_at = ? WHERE id = ?", (_ts, _ts, contact_id))
conn.execute("UPDATE opportunities SET deleted_at = ? WHERE contact_id = ? AND deleted_at IS NULL", (_ts, contact_id))
conn.execute("UPDATE communications SET deleted_at = ? WHERE contact_id = ? AND deleted_at IS NULL", (_ts, contact_id))
conn.execute("UPDATE lp_profiles SET deleted_at = ? WHERE contact_id = ? AND deleted_at IS NULL", (_ts, contact_id))
log_audit(conn, user['user_id'], 'contact', contact_id, 'delete')
conn.commit()
conn.close()
@@ -2213,7 +2219,7 @@ class CRMHandler(BaseHTTPRequestHandler):
SELECT o.*,
(SELECT COUNT(*) FROM contacts WHERE organization_id = o.id) as contact_count,
(SELECT COALESCE(SUM(commitment_amount), 0) FROM opportunities WHERE organization_id = o.id AND stage = 'funded') as total_funded
FROM organizations o WHERE 1=1
FROM organizations o WHERE 1=1 AND o.deleted_at IS NULL
"""
args = []
if params.get('search'):
@@ -2314,7 +2320,7 @@ class CRMHandler(BaseHTTPRequestHandler):
return self.send_error_json("Organization not found", 404)
conn.execute("UPDATE contacts SET organization_id = NULL WHERE organization_id = ?", (org_id,))
conn.execute("DELETE FROM organizations WHERE id = ?", (org_id,))
conn.execute("UPDATE organizations SET deleted_at = ?, updated_at = ? WHERE id = ?", (now(), now(), org_id))
log_audit(conn, user['user_id'], 'organization', org_id, 'delete')
conn.commit()
conn.close()
@@ -2333,7 +2339,7 @@ class CRMHandler(BaseHTTPRequestHandler):
LEFT JOIN contacts c ON op.contact_id = c.id
LEFT JOIN organizations o ON op.organization_id = o.id
LEFT JOIN users u ON op.owner_id = u.id
WHERE 1=1
WHERE 1=1 AND op.deleted_at IS NULL
"""
args = []
if params.get('stage'):
@@ -2524,7 +2530,7 @@ class CRMHandler(BaseHTTPRequestHandler):
conn.close()
return self.send_error_json("Opportunity not found", 404)
conn.execute("DELETE FROM opportunities WHERE id = ?", (opp_id,))
conn.execute("UPDATE opportunities SET deleted_at = ?, updated_at = ? WHERE id = ?", (now(), now(), opp_id))
log_audit(conn, user['user_id'], 'opportunity', opp_id, 'delete')
conn.commit()
conn.close()
@@ -2541,7 +2547,7 @@ class CRMHandler(BaseHTTPRequestHandler):
FROM communications cm
LEFT JOIN contacts c ON cm.contact_id = c.id
LEFT JOIN users u ON cm.created_by = u.id
WHERE 1=1
WHERE 1=1 AND cm.deleted_at IS NULL
"""
args = []
if params.get('contact_id'):
@@ -2810,7 +2816,7 @@ class CRMHandler(BaseHTTPRequestHandler):
conn.close()
return self.send_error_json("Communication not found", 404)
conn.execute("DELETE FROM communications WHERE id = ?", (comm_id,))
conn.execute("UPDATE communications SET deleted_at = ?, updated_at = ? WHERE id = ?", (now(), now(), comm_id))
log_audit(conn, user['user_id'], 'communication', comm_id, 'delete')
conn.commit()
conn.close()
@@ -3492,6 +3498,17 @@ class CRMHandler(BaseHTTPRequestHandler):
except Exception:
out['pending_merge_candidates'] = None
out['index_job'] = entity_jobs.get_status() if entity_jobs else None
# Raw source-record counts, so the resolved canonical numbers can be
# sanity-checked against what's actually in the CRM.
try:
out['source_counts'] = {
'contacts': conn.execute("SELECT COUNT(*) FROM contacts WHERE deleted_at IS NULL").fetchone()[0],
'organizations': conn.execute("SELECT COUNT(*) FROM organizations WHERE deleted_at IS NULL").fetchone()[0],
'fundraising_investors': conn.execute("SELECT COUNT(*) FROM fundraising_investors").fetchone()[0],
'fundraising_contacts': conn.execute("SELECT COUNT(*) FROM fundraising_contacts").fetchone()[0],
}
except Exception:
out['source_counts'] = None
conn.close()
self.send_json({"data": out})