Skip to content

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.)

  • /connecting route with an action queue default view — only cooling/cold/new contacts, sorted most-overdue-first.
  • Bi-directional interaction model. Every interaction tagged outbound or inbound. 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_days so 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

  1. MVP scope: include Linear mirror or defer? Recommend defer (see #1 above).
  2. 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.
  3. 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.
  4. 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)

  • /connecting renders 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 /connecting mid-week
  • Letting a contact go dormant is one click; no confirmation dialog, no "are you sure"