Gera read the messages, found no cancellation, and doubted himself. He was right: a person cancelled that tour in PropFlow. The rail knew and threw the fact away at render. Now it says so — and links to the thread when there really is one.
30 Aug 2026 · branch fix/rail-provenance-anchor · captured against propflow-stage · at 868e21a2f0+local
Data note. Every name and balance below comes from propflow-stage, which is anonymized by construction (ADR-0097/0110 — phones in the impossible +1000 NPA, emails @example.test). The identical capture against prod would carry real tenants' names and balances and is not safe to attach anywhere, which is why the generator refuses to run against one.
Captured from a second dev server pinned to ae2d1f575c — a fixed sha, not the main ref. That sha sits before #6630, which is exactly what makes this a BEFORE and what keeps it one: today's main carries the change, so a frame pinned to the main ref would have quietly become the AFTER the moment #6630 landed. The provenance line under the frame is derived from that server's own worktree rather than typed here, so the sha under the picture is the code that drew it.
The rail opens on Tour, which is where this lead stopped, and the panel states the cancellation: the label, the tour's date and time, the reason the writer recorded, the stamp. Everything except the one thing Gera was actually asking about — who or what did this.
That is not an oversight in the panel. applyCancel writes Tour cancelled via <trigger> (<channel>) — <reason> into the tour's own history, and both renderers on this page delete the middle two tokens before a reader sees them: stripTourTrigger strips the clause from the description, and the cancellation read keeps only the tail after the em dash. Both are right about the spelling — nobody should read via ui (manual) off a detail page. The bug is that the FACT went with it.
Asserted in the captured DOM — Leasing progress · Tour Cancelled · Cancelled. Rail-local strings only, and deliberately NOT the new sentence — this tool can assert that a string is PRESENT, never that one is absent, so "no source line here" is carried by the picture and by the positive controls in the PR, not dressed up as a check. "Application Submitted" is avoided for the reason #6619 documented: it renders twice on this page.
The same lead, same panel, on this branch. The third line is new: From the text conversation, rendered as a link.
It is derived, not stored. The tour's history row here reads Tour cancelled via voice_handler (sms) — Hello, I need to cancel this appointment; the new tour-event-provenance module reads the channel out of that clause and says what it means in English. The channel and not the trigger, deliberately: there are seven trigger names and more arrive every time a decider is split, whereas the reader's question is only ever "is there a message I can go read?" — which is exactly what four stable channel values answer.
Note what did not come back: via voice_handler (sms) appears nowhere on the page. The clause is parsed, not restored, and a test asserts the raw spelling stays gone.
Asserted in the captured DOM — From the text conversation · Leasing progress · Tour Cancelled. "From the text conversation" is minted in tour-event-provenance and rendered by nothing else on this page — so this assertion cannot pass off the stage badge, the Activity Log or the tour history card, each of which also says "Tour Cancelled".
The script clicked the source line. It is a real anchor, so it navigates — and it lands on this prospect's own conversation reader, inside the prospect's url space, trail reading Prospects / Zayn Marchetti / conversation.
Read the thread. Four exchanges down sits the outbound bubble “Hello, I need to cancel this appointment”, and Clara's reply under it: “No problem, Zayn — your tour on Monday, Marchetti 11 at 4:15 PM has been cancelled.” That is exactly the reading Gera went looking for and could not find on the record he was on — because on that record there was no such message to find. Here there is, and the rail pointed at it.
The href is not a new url contract. It is the identical buildProspectConversationHref(...) string this page already hands its Clara summary card and its follow-ups rail — one destination for the page, not a third scheme. Stating the ceiling rather than hiding it: a TourEvent carries an action, a detail string and a timestamp and no conversation or message id, so there is nothing to anchor a single message with, and this link opens the thread rather than highlighting one bubble in it. That is the same limit ReengagementCardProps.conversationHref already documents one card away. When the entity-scoped message anchor generalises past tenants, this rail inherits it by changing nothing — the href arrives as a prop, built by the shared builder.
Asserted in the captured DOM — Hello, I need to cancel this appointment · conversation · Zayn. Asserted AFTER the click. The message body is the discriminating string: it is the tour history's own recorded reason, it exists on the conversation reader, and a click that failed to navigate would leave the detail page — where the full message body does not render.
A lead whose tour was cancelled off an email exchange (via email_state_machine (email)). The line reads From the email conversation — the same mechanism, a different word, because the reader is being told which thread to open and "conversation" alone would not say.
The vocabulary is four values and stops there. An unrecognised channel, or a trigger the module cannot pin to one channel, yields no line at all rather than a guess: agent_tool is reachable from every channel Clara speaks, so it is deliberately absent from the fallback map. A coin-flip "From the text conversation" on what was actually a call would be worse than silence, because the whole point of this line is to stop a PM guessing — it cannot do that by guessing itself.
Asserted in the captured DOM — From the email conversation · Leasing progress
A third lead, and the reason this frame is here at all: the tracker still carries "a cancelled tour does not appear in the leasing progress at all" as open work. It is not open. The short red Cancelled step sits between Tour and Application, exactly as Gera asked ("it should go to like the small little line saying tour cancelled") — shipped in #6533, with the data bug that made it blank on one record fixed in #6463.
And the Tour stage beside it stays brand-filled and "Reached", which is the Decisions-tab ruling rendered: "should a cancelled tour leave the Tour step UNFILLED?" → "Keep it filled — booking is the funnel event, as the code already decided." Recolouring that bar red would render "booked, then cancelled" identically to "never booked" — which is the entire argument for the marker being a POSITION rather than a colour.
So this PR builds nothing for that row. What it adds is the seam test that was missing: both halves were unit-tested in isolation — buildActivityLog emitting the flagged row, the rail turning a flagged row into a marker — and nothing walked a real cancelled-tour seed through the real page to the marker on screen. Something does now.
Asserted in the captured DOM — Cancelled · Reached · Leasing progress · From the text conversation
The third tracker row says the follow-up block "reads nothing like the other cards" — six stacked lines where the equivalent elsewhere is one row. That is fixed on main already, consolidated into the rail exactly as Gera asked ("I want to consolidate all that into that vertical progress") across #6520 / #6530 / #6532 on 2026-08-28: the purple Check-in scheduled pill, the green banner and the violet next-send callout are all gone, their sentences moved onto the nudge's own rail row, and a completed phone touch reads Phone call — the exact word he asked for. This PR changes nothing there and did not need to.
The BEFORE for that row is not in this artifact, and the reason is worth stating rather than quietly omitting. This tool refuses to photograph anything but an anonymized-by-construction data source, which in practice means propflow-stage — and stage carries zero prospects with an enrolled follow-up cadence (no outreachCadenceAnchorIso, no outreachTouchIndex, on any of its 129 prospect rows). The six-line card cannot be reproduced on the only data this artifact is allowed to see. The follow-up card visible in every frame above is the honest not-enrolled state — one notice, one row — and the claim about the enrolled one rests on ReengagementCard.followupFunnels.test.tsx §7, which asserts the pill string, the banner tone and the next-send hairline are all absent while their sentences are present as rows. All 115 of that card's tests pass on this branch, unmodified.
Fabricating a stand-in would have been easy and would have been a lie. An artifact that says which frame it could not take is worth more than one that quietly has no gap.