Status: ✅ Complete — August 12, 2026 Effort Class: Small. No migration, no new table, no new route. Surfaces: Signup (alum-facing) only Held surfaces touched: None. One held-surface defect was found and left unfixed — see §6. Prerequisites: 27.7, whose shipped flow this was written against.
27.7 shipped. Somebody walked the flow. What did it get wrong?
Four things, none of which a test could have told us because each is a decision rather than a defect — except the fourth, which is a defect and had no test looking for it.
| Symptom | § | |
|---|---|---|
| 1 | The sign-in form existed on three pages, and only one had the code box | §2, §3 |
| 2 | The email led with the link, which loses your place | §4 |
| 3 | The landing page offered “Get Started” to somebody already signed in | §5 |
| 4 | Developer commentary rendered as page copy on a public page | §6 |
Not an interview sub-phase. Requested directly, scoped in one exchange, with two product decisions confirmed before building (keep the box but swap the form for a link; convert the thank-you page too).
The through-line worth keeping: three of the four are a second copy of something — a second sign-in form, a second call to action, a second reading of what “home” means. The fourth is a comment that documented a second copy. 27.2 and 27.5 both created one deliberately and both drifted within a sub-phase. See §7.
27.2 gave the “email me a link” form a page of its own and left the landing page’s copy
in place, with an entry_point param so each confirmation returned to its own page. The
thank-you page had a third copy, inlined rather than rendered from the partial.
The copies had already drifted: 27.7 added the code box and the “keep me signed in”
checkbox to /sign-in only, so submitting the landing form got you an email whose primary
instruction — “enter this code on the page you were on” — pointed at a page with nowhere
to enter it. That is the argument against a duplicated form generally, and it arrived
within one sub-phase of the duplication.
Both copies are now links to /sign-in. Removed with them: the entry_point param and
its hidden field, signin_entry_point?, the two-branch link_sent_path, and the landing
page’s link_sent / link_request banners (nothing redirects there with either any
more). The link_requested event type stays valid so historical rows keep their meaning,
but has no writer — asserted, so a third form cannot quietly reintroduce one.
The thank-you page’s link carries return_to, matching the anonymous prompt on the
opportunity form itself: signing in from a receipt returns to that receipt.
link_sent_path appends the submitted email, and /sign-in prefills both forms from it,
autofocusing the code box when it is filled. The person types six digits and nothing else.
Two properties this had to preserve, both tested:
27.7 put the code below the link, framed as “reading this on a different device?”. That is
backwards for the common case. The link opens the profile hub on whatever device reads the
mail, abandoning whatever the person was doing; the code returns them to the browser they
left — with its stored return_to, so an opportunity form is still there.
The email now leads with the code and offers the link under “reading this somewhere else”. The subject follows, and only says “code” when there is one, so a codeless send stays truthful. Pinned as an order in both parts: a test asserting both are present passes on either arrangement.
Reported the same day: click the header logo while signed in, land on /sign-up, and the
page offers Get Started. That button carries no signup_token, and handle_who_you_are
reads a missing token as creating — so following it opens a second signup under the same
address. The “Welcome back, Chip” banner sat directly above it.
The banner was 27.2-era mitigation: it told the truth about who you were and left the rest of the page arguing the opposite. Three CTAs disagreed with it (hero “Get Started”, bottom “Start Signing Up”, the “Already signed up?” section) plus one line of copy promising you’d choose an interest area “after you sign up”.
Fixed on both axes, per the request.
Home moves. Signup::BaseController#signup_home_path is now the single definition of
where “home” is: the profile when signed in, the landing page otherwise. The header logo
uses it, so there is no branch in the markup, and sessions#create / #new were collapsed
onto it — they had grown a private profile_path_for doing the signed-in half.
The landing page has two modes. One page, one education_mode flag read by each section,
because the middle of the page — what a Champion is, the four interest areas — is genuinely
identical for both audiences. Splitting it into two templates would have produced the same
drift the duplicated sign-in form did in §2, on more content.
| anonymous | signed in | |
|---|---|---|
| Hero | Stay connected. Make an impact. → Get Started | You’re an Alumni Champion, Name. → Go to my profile |
| Interest areas | “you’ll tell us where you lean after you sign up” | “you can change where you lean anytime from your profile” |
| Bottom | Ready to jump in? → Start Signing Up | Want to add more? → Open my profile |
| Returning section | Already signed up? → Sign in | hidden |
The education itself is untouched in both, which was the point — the fix had to keep the page worth visiting, not delete it.
Tested as absences as well as presences. A returning CTA is precisely the failure, and
a presence-only assertion cannot see one; assert_select ... false on the create form’s
href is the one that would catch a regression. Sabotage-verified by forcing
education_mode = false: three of the four new tests fail, and the fourth is the
educational-content assertion, which is supposed to hold in both modes.
Not changed: Get Started with an active session but no token starts a fresh record.
That behavior is still correct and still reachable — “Not you? Start a new signup” on the
resume banner clears the session first, which is the shared-device case it exists for. What
changed is that no page offers it to somebody who did not ask.
%>“Things you’ve said yes to” rendered four lines of developer commentary as page copy, on
an alum’s own hub and on the public thank-you page. The receipt partial’s doc comment
showed an example using the <%%= escape — which escapes the output delimiter but does
nothing for the scanner, so the comment closed at that tag and the rest was emitted.
Nothing could have caught it as written: valid HTML, 200 response, no warning, and the
existing test asserted the answers were present, which they were — the leak renders above
them. test/views/erb_comment_integrity_test.rb now scans every view on every build.
Held surfaces are excluded by path, and one real instance sits behind that exclusion
(cp/profile/_completion_banner.html.erb:6, leaking its own header onto the profile edit
page) — unfixed because writing to Cp:: needs approval, and named here so the exclusion
is a known debt rather than a clean baseline. Routed to /debug.
Every item in §1’s table is a second copy of a decision, and each drifted within a sub-phase of being created:
| Created | Second copy of | Drifted by |
|---|---|---|
| 27.2 | the sign-in form (landing + /sign-in) |
27.7, which added the code box and consent checkbox to one of them |
| 21.6 | the sign-in form again (thank-you page, inlined) | never rendered from the partial, so it missed both |
| 19.x | the “home” destination (logo href vs. sessions#create’s own profile_path_for) |
27.7, when signed-in became a real state |
| 21.7 | the answer-rendering rule, written as a census in a comment | 27.5, which found the fifth consumer the count denied |
The rule that would have prevented all four is already in CLAUDE.md §7 — “the shared layer is the first answer, not the last resort” — and each was a case where a second consumer looked cheaper than a shared one at the moment of writing. It is cheaper, once. The cost lands on whoever changes the behavior next, and they do not know the second copy exists.
The operational form, added to /patterns in this wrap: when a page needs a control
another page already has, the default is a link to that page, not a copy of the
control. Duplicate only when both copies render from the same partial and the partial
names both consumers. Two of the three fixes here were “delete the copy, link instead” —
no shared abstraction needed at all.
| Area | Files |
|---|---|
| One front door | signup/signups/steps/_landing, signup/opportunities/thank_you, signup/signups/_link_request_form, signups_controller#request_link / #link_sent_path |
| Email prefill | signups_controller#link_sent_path, signup/sessions/new |
| Code-first email | champion_signup_mailer#profile_link_email, both profile_link_email templates |
| Education mode | signup/signups/steps/_landing, signup/signups/_header, Signup::BaseController#signup_home_path, Signup::SessionsController |
| ERB leak | signup/opportunities/_response_receipt, test/views/erb_comment_integrity_test.rb |
Removed: the entry_point param and its hidden field, signin_entry_point?,
entry_point, the two-branch link_sent_path, Signup::SessionsController#profile_path_for,
the landing page’s link_sent and link_request banners, and two inline copies of the
link-request form.
No migration. No new route. No new model. ChampionSignupEvent gains no type;
link_requested loses its last writer and stays valid for historical rows, asserted.
| File | Added |
|---|---|
test/views/erb_comment_integrity_test.rb |
new — scans every view on every build |
test/controllers/signup/hub_additions_test.rb |
leak absent on both consumers of the receipt |
test/controllers/signup/signups_controller_test.rb |
education mode (5), header logo (2), one-front-door redirects |
test/controllers/signup/sessions_controller_test.rb |
prefill, prefill escaping, one destination, neutrality-with-echo |
test/mailers/champion_signup_mailer_test.rb |
code-before-link order in both parts, subject follows |
bin/test: 5805 runs, 0 failures, 0 errors.
Two verified by sabotage rather than trusted: the ERB detector (empty HELD_TREES →
reports the known cp/ instance) and education mode (force education_mode = false →
3 of 4 fail; the fourth asserts the educational content, which holds in both modes).
Reported the day 27.8 reached staging, walking the flow from /sign-up:
The flow from /sign-up, where an existing email address is detected goes “email me my link” and then the code shows up at the top of the email (even though the link to the profile is below).
Correct, and a regression this sub-phase introduced. §4 made the code both unconditional
and primary, reasoning only about /sign-in. But /profile-link has a second caller:
step 1’s duplicate-check notice (27.1), which fetches and deliberately keeps the person
on their half-filled signup form. Nothing on their screen accepts six digits. They asked
for a link and got a puzzle.
§2 deleted entry_point and was right to. That param asked which page rendered the form,
which was cosmetic — it picked a redirect target and nothing else.
The axis that actually matters is a different question: is there a code box in front of this person right now? It has a consequence, so it earns a branch.
mode |
Caller | |
|---|---|---|
code (default) |
/sign-in — its code box appears the instant the send completes |
Leads with the six digits; profile link demoted to a small text line |
link |
step 1’s duplicate notice — they stay mid-form | Prominent profile button, no code issued at all |
This is not the deleted param returning under a new name. It is the distinction that
was hiding underneath it. That entry_point happened to correlate with it, for the two
callers that existed in 27.2, is exactly why deleting it looked free.
issue! a code, only omit it from the email. issue! supersedes
any outstanding code, so a link request from step 1 would silently kill a code the same
person was part-way through typing in another tab.link_sent — which reveals the code box — now reads from the same predicate the email
reads. One decision, two consumers, so the page cannot offer a box for a code that was
never sent. Deriving them separately is how this class of bug returns.class="button" is absent
in code mode and present in link mode.The event metadata changed with it: entry_point: "sign_in" (always one value) became
mode: "code" | "link", which records something that varies and that decided what was sent.
§7’s rule — link first, share second, copy never — is unchanged and still right. What this postscript adds is its failure mode: when you collapse two things into one, check whether they differed on an axis you weren’t looking at. §2 verified the two forms were visually identical and posted to the same endpoint. It did not ask what the recipients of the two sends were looking at when the mail arrived, and that was the difference.
Deduplication is a claim that two things are the same. Like any claim, it is worth stating what would make it false before acting on it.