Skip to content

Name the talk the audience was about, not the accepted one - #325

Merged
EllAchE merged 2 commits into
mainfrom
comms-audience-merge-20260817t1920
Aug 18, 2026
Merged

Name the talk the audience was about, not the accepted one#325
EllAchE merged 2 commits into
mainfrom
comms-audience-merge-20260817t1920

Conversation

@EllAchE

@EllAchE EllAchE commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Audience-segmented Compose sends resolve {{submission.title}} to the wrong submission,
producing factually incorrect decision emails.

A speaker with an accepted talk and a declined one falls into the "Declined speakers" audience
legitimately. Sending the "Submission declined" template to that audience produced a message naming
his accepted talk and telling him it had not made it into the programme.

What was wrong

resolveRecipients decided who to write to and then, separately, what to say to them:

const preferred = submissions.find((s) => s.status === 'accepted') ?? submissions[0] ?? null;

spec — the audience — is nowhere in that line. matchesAudience knew which proposals had
qualified the recipient, but it answered a boolean and threw the rest away, so the merge fields fell
back to accepted-first across everything the person owns. For anyone with exactly one submission the
two agree, which is why this reads correct until a speaker has two.

The judge verified it by contrast: the automatic per-decision notification for the same decision,
recipient and template names the right talk. It does, because fanOutSubmissionTemplate overrides
submission.* from the row it was called about and never consults this code. Compose has no such
override, and there is no third path — those two are the whole of it.

The fix

qualifyingSubmissions(spec, submissions, isScheduled) returns the submissions that put a recipient
in the audience. mergeSubmission picks from that set, and matchesAudience is now the emptiness
check over the same function — so the filter and the merge fields cannot disagree about which
proposals a segment is about.

Three things worth naming:

  • The audiences that are not about a submission keep the pick they had. Everyone, a hand-picked
    list, an outstanding task: still accepted-first across the lot. Every triggered send resolves its
    recipient through manual, so nothing that was already naming the right talk changes.
  • The tie is broken by ref, not row order. Two declined talks in one declined-speakers send
    used to resolve to whichever row the join returned first. A mailing that is resent should not
    change its subject.
  • The session follows the same submission. {{session.room}} naming one talk while
    {{submission.title}} names another is the same defect wearing a different field. A recipient
    whose qualifying proposal is not on the agenda now gets empty session fields and no calendar
    attachment, which is the truth about it, rather than another talk's room.

Verification

bun run lint, bun run typecheck, bun run test (190 files, 2046 tests) and bun run build all
pass. bun run docs:openapi and bun run docs:mcp regenerate to no diff.

Thirteen new tests over mergeSubmission and qualifyingSubmissions, including the reported case in
one assertion, both segments resolving differently for the same speaker, and the non-submission
audiences keeping their old answer.

Not covered: the send itself. resolveRecipients reads six tables and the unit suite has no
database, so what a test here can observe is the pick, not the mail.

Part of the 2026-08-17T05-46-05 evaluation cycle. Not merged, not deployed.

EllAchE and others added 2 commits August 17, 2026 18:06
An audience-segmented Compose send built `{{submission.title}}` from
`submissions.find(accepted) ?? submissions[0]`, which never looks at the
audience. A speaker with an accepted talk and a declined one is in both
segments, so the "Submission declined" mailing named his accepted talk and
told him it had not made it in.

`qualifyingSubmissions` returns the submissions that put a recipient in the
audience rather than a boolean, and `mergeSubmission` picks from that set.
`matchesAudience` is now the emptiness check over the same function, so the
two can no longer disagree about which proposals the segment is about.

The session follows that submission too, so `{{session.room}}` cannot
describe one talk while the title names another.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@EllAchE
EllAchE merged commit bd379a0 into main Aug 18, 2026
7 checks passed
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