Skip to content

Let the directory learn the speakers your events already have - #329

Closed
EllAchE wants to merge 3 commits into
event-person-names-20260817t2030from
crm-adopt-event-speakers-20260817t2100
Closed

Let the directory learn the speakers your events already have#329
EllAchE wants to merge 3 commits into
event-person-names-20260817t2030from
crm-adopt-event-speakers-20260817t2100

Conversation

@EllAchE

@EllAchE EllAchE commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Stacked on #328 (event-person-names-20260817t2030) — it uses eventPersonName from that branch. GitHub retargets this to main when #328 merges.

sbek CRM-S1, run 2026-08-17T05-46-05 (major):

The org-level speaker directory and the per-event speaker roster are two separate stores with only a manual, one-directional bridge, while the directory's own subtitle claims otherwise. /crm reads 'Every speaker and contact your organization has worked with, across all events', yet it opened at 'Showing 0 of 0 contacts / Your speaker database is empty' even though the same speakers.csv had already been imported into an event roster earlier in the run — the CRM never learns about people who exist as event speakers. The only link is the contact record's 'Add to an event' button, which pushes org → event; there is no reverse sync, so edits made on the event speaker record do not flow back and the same person can exist as two unlinked records. An organizer arriving with existing event rosters finds an empty CRM and must re-import.

The fix

The directory adopts the speakers on every event the organizer runs, on read. Keyed by email, idempotent:

  • a speaker the directory has never heard of becomes a contact, linked to the event they were found on
  • a speaker it already had is linked, not duplicated
  • a speaker already linked to that event is left alone
  • a person on two events is one record with two links

planRosterAdoption is the pure half and holds all of that; adoptEventSpeakers does the reads and writes around it.

Adoption only ever adds. A contact the organizer has since renamed, re-companied or written a bio for is theirs, and a roster row is not entitled to overwrite it. Adopted records carry source: 'event roster' so they are distinguishable from ones typed in by hand.

Both entrances run it — the directory and the dashboard — because landing on either first should not show a database of nobody.

The contact lookup includes merged-away tombstones on purpose: contact_owner_email is unique across them, so a merged row still holds its address and inserting over it would fail rather than adopt.

What this does not do

The enrolment half of the finding is closed. The edit half is not: renaming someone on an event speaker record still does not update their directory contact. Which store wins on a conflicting edit is a product decision, and picking one silently inside a bug fix would be the wrong place to make it.

Cost

One extra read on the two CRM entry points, and writes only on the first visit after a roster changes — after that the plan comes back empty and nothing is written.

Tests

lib/services/crm.test.ts gains 7 cases on planRosterAdoption: adopts an unknown speaker and records the event; does nothing the second time; links rather than duplicates someone already in the directory; one record and two links for a speaker on two events; adds only the missing event for a partially-linked speaker; keys the address case-insensitively (the column is stored lowercased, so a case-sensitive key would adopt twice); skips a participant with no address rather than guessing.

lint, typecheck, test (2051 in 190 files), build, docs:openapi, docs:mcp all clean.

Not merged and not deployed.

…' into crm-adopt-event-speakers-20260817t2100
/crm says it holds "every speaker and contact your organization has worked
with, across all events". It held none of them. The only bridge between the
two stores pushed org -> event, through the contact record's "Add to an
event" button, so an organizer who imported a roster and then opened the CRM
found "Your speaker database is empty" and was asked to import the same CSV
a second time.

The directory now adopts the speakers on every event the organizer runs, on
read. Adoption is keyed by email and idempotent: a speaker already in the
directory is linked to the event rather than duplicated, one already linked
is left alone, and a person on two events is one record with two links.

It only ever adds. A contact the organizer has since renamed, re-companied or
written a bio for is theirs, and a roster row is not entitled to overwrite
it. Adopted records carry `source: 'event roster'` so they are
distinguishable from ones typed in by hand.

Both entrances do it — the directory and the dashboard — because landing on
either first should not show a database of nobody.

The lookup includes merged-away tombstones. `contact_owner_email` is unique
across them, so a merged row still holds its address and inserting over it
would fail rather than adopt.

This closes the enrolment half of the finding. Edits still do not flow back:
renaming someone on an event speaker record does not update their directory
contact, and which store wins on a conflict is a product decision this change
does not make.

Found by `sbek` CRM-S1 against run 2026-08-17T05-46-05.
@EllAchE
EllAchE deleted the branch event-person-names-20260817t2030 August 18, 2026 00:05
@EllAchE EllAchE closed this Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant