Splitting tickets pair + undo on both merge and split (#914–#916)
New: Splitting-Tickets + Splitting-Tickets-Developer-Guide, same format as the
other pairs — colour-keyed file table, the reasoning, and the traps.
The dev guide leads on why split is NOT the mirror of merge: a merge protects a
reference the customer already holds (pointer + inbound redirect), a split mints
one nobody has ever seen (no pointer, no redirect, both tickets live, linked
`related` not `duplicate_of`). That table is the thing worth reading.
Also written up where they will be read:
- the empty-ticket guard, and why the preview and the split share one function
- the thread renders NEWEST-FIRST, so every string says "newer" not "after"
- the control was hover-only and nobody could find it; a control nobody can
find is a missing feature, not a subtle one
- splitPlural() exists because "1 message(s)" reads as a bug
Merge pages updated for undo, including the contrast that matters: undo-split
REFUSES on any activity (the new ticket was brand new), unmerge deliberately
does NOT (the survivor is the live ticket and collects replies within the hour).
Plus what unmerge records and why the source's previous status must be stored
rather than inferred.
Every file, function and column named in the tables verified to exist; every
internal link resolves. Linked from the sidebar under Tickets and from
Tickets.md.
Merging tickets: analyst page + developer guide (#912)
Merging-Tickets.md — doing a merge, what happens to the reference and why that
matters, the AI briefing, the two settings and their trade-offs (including the
search trade-off on HTML-only), whether the HTML attachments are safe to open,
and the honest edges (can't be undone, 20 max, same company, no split yet).
Merging-Tickets-Developer-Guide.md — colour-keyed file table, then the four
decisions:
- the SDREF in every past notification is why a merged ticket is never
deleted, and why the pointer is a column and not a status name
- what moves vs what stays, with the test that decides it ("does this row
describe the conversation, or what happened to THIS ticket?")
- access checked for every ticket before a single row moves; loadTicket made
public rather than copying the scope rule; one transaction
- the snapshot is safe because of THREE things, and the one that matters most
is Content-Disposition: attachment — it never renders on the app's origin
Includes both bugs found on live data after shipping, written up where they
will be read: tickets.tenant_id NULL means the DEFAULT company (comparing raw
values refused ordinary merges), and only Anthropic streams tokens so an AI
progress indicator must never depend on tokens arriving.
Every file, function, constant and CSS class named in the tables verified to
exist; every internal link resolves. Linked from the sidebar under Tickets and
from Tickets.md.