Proposal: /connecting — networking tracker with warmth monitor¶
Status: draft (logged at Low priority per operator; spare-cycles work, post-July-15 arXiv deadline LIS-186)
GitHub issue: #256
Author: operator (issue spec) + Claude (this writeup)
Date: 2026-05-25
Problem¶
The operator maintains a growing networking surface — researchers, peers, mentors, fellowship contacts — and relationships go cold without obvious signal. Existing surfaces don't help:
- Linear: rich for tracked tasks; wrong shape for "is this relationship cooling."
- Email/Slack: search-only; no decay model.
- A spreadsheet: requires the operator to monitor it, which is the problem.
The bench's role: let go of monitoring. A first-class surface that surfaces only what's overdue, treats dormancy as a frictionless choice, and never auto-pings.
Proposed design¶
(From #256. Highlights here; the GH issue body is canonical.)
/connectingroute with an action queue default view — only cooling/cold/new contacts, sorted most-overdue-first.- Bi-directional interaction model. Every interaction tagged
outboundorinbound. Warmth reflects two-way flow; one-sided outreach gets a quiet flag, not an alarm. - Warmth = decay against expected per-type cadence, not absolute days:
- peer: 10d
- mentor: 30d
- initiative: 45d
- field: 90d
- Tiers: warm / cooling / cold / new. Tier thresholds tied to
cadence_daysso changing the type updates the math. - Anti-obsession is a hard requirement. No real-time pings. Any digest folds into the existing Sunday review (LIS-213). Letting a relationship go dormant is first-class and frictionless.
- Optional Sonnet assist — "draft a re-warm note" button. Suggest-only, never auto-send, refuses to invent a hook from empty notes.
Data model¶
contact (
id UUID PK
user_id UUID FK → auth.users
name TEXT
type TEXT -- 'peer' | 'mentor' | 'initiative' | 'field'
source TEXT -- how I met them
links_json JSONB -- {twitter, linkedin, github, email, ...}
context TEXT -- short prose: "met at NeurIPS poster on..."
cadence_days INTEGER -- override per-type default
status TEXT -- 'active' | 'dormant' | 'archived'
linear_issue_id TEXT -- for one-way mirror
created_at TIMESTAMPTZ
)
interaction (
id UUID PK
contact_id UUID FK → contact
direction TEXT -- 'outbound' | 'inbound'
channel TEXT -- 'email' | 'twitter' | 'in-person' | ...
occurred_on DATE
summary TEXT
next_step TEXT
next_step_due DATE
)
What this proposal adds on top of the GH issue¶
Three things to lock down at design-review time (not in the GH issue):
1. Ship MVP without the Linear mirror¶
The GH issue itself notes: "Adds meaningful build scope — reinforces the Low priority below." My recommendation: scope v1 to just the local tracker. No linear_issue_id, no GraphQL key, no background sync worker. Ship /connecting with bench-only data. Add the Linear mirror as a follow-up if and only if:
- Operator finds themselves wanting cross-system reads
- The Sunday-review integration (LIS-213) doesn't surface enough state on its own
Trade-off: operator maintains contact data in one place (bench) without the cross-link convenience. The convenience isn't worth dual-write complexity until usage proves it.
2. RLS posture (mandatory, not optional)¶
Both new tables get the migration-010 owner-only treatment:
ALTER TABLE contact ENABLE ROW LEVEL SECURITY;
CREATE POLICY contact_owner_only ON contact
FOR ALL USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
ALTER TABLE interaction ENABLE ROW LEVEL SECURITY;
CREATE POLICY interaction_owner_only ON interaction
FOR ALL
USING (EXISTS (SELECT 1 FROM contact c WHERE c.id = interaction.contact_id AND c.user_id = auth.uid()))
WITH CHECK (EXISTS (SELECT 1 FROM contact c WHERE c.id = interaction.contact_id AND c.user_id = auth.uid()));
interaction uses the indirect-user pattern (subquery against contact.user_id) — same shape as assessment_responses / credential_files / attempt_reviews in migration 010.
3. Tests pin the warmth math¶
The decay calculation has subtle boundaries (just-warm-to-cooling, just-cooling-to-cold, never-replied edge case, bi-directional asymmetry). One focused test class — tests/unit/test_warmth.py — covering:
- Each tier boundary for each contact type
- Bi-directional flow: contact with
outbound-only interactions for 2× cadence shows "cooling (one-sided)" rather than "cold" - New contacts (no interactions) show "new" not "cold" until past their first cadence
- Type-change recomputes tier without requiring a new interaction
Without explicit tests, this kind of calculation drifts over time and the operator stops trusting the signal.
Open decisions¶
- MVP scope: include Linear mirror or defer? Recommend defer (see #1 above).
- Sonnet assist: ship in v1 or v1.1? Lean v1.1 — the "draft a re-warm note" feature is high-touch and would benefit from real usage data to write a good prompt. Ship the tracker first; add the assist after the operator has actual context to refer to.
- Manual seed of existing contacts. The issue mentions backfilling existing hand-made Linear issues (Dalia, LIS-330). With the Linear mirror deferred (per #1), this becomes: a one-shot script the operator runs once to import contacts from a CSV or JSON they paste in. Or a v0 "Add contact" form that just accepts text — operator types each in by hand. Whatever's least friction.
- Where does the nav link live? Adding "Connecting" to the primary nav makes 9 items, which is getting crowded. Alternative: under
/aim(it's adjacent to JD work) or under the profile dropdown. Recommend profile dropdown initially — once the operator uses it daily, promote to top nav.
Scope estimate¶
| Scope variant | Files | Lines | PRs |
|---|---|---|---|
v1 MVP (recommended): tables + repo + /connecting route + warmth math + tests. No Linear mirror, no Sonnet assist. |
~10-12 | ~700-1000 | 1 |
| v1 + Sonnet assist | +2 | +200 | +1 |
| v1 + Linear mirror | +4 | +400-600 | +1 |
Migration 011 (contacts.sql) is CODEOWNERS — operator-authored or following the past pattern (#220, #239, #220, #239).
Why later, not now¶
Operator explicitly logged this as Low priority with a clear trigger: capstone arXiv submission (LIS-186) on July 15. Cycling /connecting before that deadline trades the capstone for the tool. The right move is to leave this proposal in draft with the trigger documented, and pick it up post-capstone.
Acceptance criteria (when picked up)¶
/connectingrenders the action queue with only cooling/cold/new contacts, sorted most-overdue-first- Adding an interaction updates the warmth tier in <1s without page reload (HTMX swap)
- Type changes recompute warmth without requiring a new interaction
- Tests cover the warmth math at each tier boundary + bi-directional asymmetry
- New tables have RLS enabled with owner-only policies (matching migration 010)
- The Sunday review (LIS-213) surfaces "N contacts cooling, M cold" without the operator opening
/connectingmid-week - Letting a contact go dormant is one click; no confirmation dialog, no "are you sure"