champions/ → alumni_network/Status: ✅ Complete — shipped July 30, 2026 Surface: 🛑 HELD. Explicit approval granted July 30, 2026, covering the rename plus the “23.5 touch-ups” in §3. Prerequisite: 23.3 ✅ (removed the roadmap controller from this tree) Scale: 185 files, +1,805 / −1,738. 20 controllers, 75 view files, 20 test files, 3 helpers. Migration: none. Schema: unchanged.
The Alumni Network Admin lived at app/controllers/champions/ and /champions/*. That directory
name is the collision the hold gate exists for: it was one character from /champion_signups/
(Signup Admin, renamed in 23.2), and the two administered different products. grep -rn "champion"
returned both with nothing distinguishing a frozen portal’s admin from the live product’s admin.
23.4 renames the surface after the product it administers.
| From | To |
|---|---|
app/controllers/champions/ |
app/controllers/alumni_network/ |
app/views/champions/ |
app/views/alumni_network/ |
app/helpers/champions/ + app/helpers/champions_helper.rb |
app/helpers/alumni_network/ |
test/controllers/champions/ |
test/controllers/alumni_network/ |
module Champions |
module AlumniNetwork |
namespace :champions |
namespace :alumni_network |
layouts/champion_admin.html.erb |
layouts/alumni_network_admin.html.erb |
layouts/champions.html.erb + layouts/champions/ |
layouts/alumni_network.html.erb + layouts/alumni_network/ |
Champions::ChampionsController |
AlumniNetwork::MembersController |
resources :champions |
resources :members |
champions_*_path (137 helpers) |
alumni_network_*_path |
1,401 references across 154 files. A regex over source would have been wrong, because four helper
families contain champion and are not this namespace:
| Helper | What it actually is |
|---|---|
cp_champion_* (13 helpers) |
Devise scope for the member portal — devise_for :cp_champions |
cp_champions_landing_path |
The public belmontalum.com landing page, host-constrained |
alumnichampions_root_path |
The signup site’s root |
champion_signups_path |
23.2’s legacy 301 alias |
And one helper’s action contains the namespace word:
autocomplete_champions_champions_communities_path — action autocomplete_champions, namespace
champions, resource communities. A “replace the first champions_” rule mangles it.
So the rename was driven off bin/rails routes — the actual route table — with an explicit
keep-list and one special case. Script preserved at
scratchpad/rename_helpers.rb (not committed; it is single-use).
Verification: bin/rails routes after the change contains exactly 17 helpers matching
champion, and every one is on the keep-list above.
23.5 (Cp:: → An::, Champion → Member, 56-table migration) was cut — see
README §8. Three items 23.4’s original spec deferred to it therefore had no later
home, and were folded in here under the same approval:
resources :champions → resources :membersThe spec deferred this on the reasoning that the path should follow the model rename. With no model
rename coming, /alumni_network/champions would have been permanently doubled-up. The path is
what staff and agents reason over, so it was renamed independent of the model:
/alumni_network/members AlumniNetwork::MembersController Cp::Champion (unchanged)
The gap between members in the URL and Champion in the model is deliberate and documented in
routes.rb.
Cp::Champion::INDUSTRIES collapsed into Industry::ALL23.1 extracted the taxonomy to shared Industry::ALL but
could not delete the portal’s copy — a held write — so it guarded the pair with a drift test and
handed the deletion to 23.5. 23.4 deleted it. Five call sites now read ::Industry::ALL
(note the :: — inside module Cp, a bare Industry resolves lexically first; see CLAUDE.md).
The drift test is replaced, not deleted: test "Cp::Champion has no private industry list"
asserts the constant stays gone. If it can be written again, a second copy has reappeared.
layouts/champions.html.erb was the member-facing portal shell (12 Cp:: controllers) while
layouts/champion_admin.html.erb was the staff shell. Two surfaces, one word, neither matching
its own directory — the single clearest instance of the drift this phase exists to unwind. The
architecture test’s LAYOUT_SURFACES map recorded it as a known non-identity mapping; after 23.4
only the cp/ row is still non-identity, and that stays because 23.5 was cut.
StaffNotification#url again, and this time it is not hypothetical23.2 discovered that StaffNotification#url persists a route path to the database. The 23.4 spec
did not carry that lesson forward, and this namespace is worse:
Cp::NotifyAdminsJob:40-42 writes
champions_champion_path / champions_verification_path into that column on every new member
signup. Every row written before this rename points at /champions/....
Without redirects, the portal-admin verification queue 404s on click — the same failure 23.2’s redirects prevent for the signup inbox, on the surface where the notification volume is highest.
Mitigation: a wildcard, not an enumeration. The persisted and emailed URLs are not confined to one resource:
| Writer | Path shape |
|---|---|
Cp::NotifyAdminsJob |
/champions/champions/:id, /champions/verifications/:id |
Cp::SupportThreadsController:98 |
/champions/support_threads/:id |
Cp::FeedbackController:70 |
/champions/feedbacks/:id |
Cp::SupportThreadMailer |
/champions/support_threads/:id (in alumni inboxes) |
Cp::ContentSubmissionMailer |
/champions/content_submissions/:id (in alumni inboxes) |
An enumerated list would have missed at least the last three. One get 'champions/*path' covers
all of them, with the query string preserved. The one path needing a rewrite rather than a
prefix swap — /champions/champions/:id → /alumni_network/members/:id — is listed first.
These redirects are permanent. The rows are already written.
_sidebar.html.erb decides which nav item
is active by comparing request.path against 25 hard-coded path literals, not route helpers.
The rename was driven off the route table, so it rewrote every champions_*_path call and left
every literal pointing at /champions/....
The result is a fully working admin surface whose sidebar highlights nothing on any page. It renders identically apart from one CSS class, no test touched it, and it would have shipped.
Fixed, and pinned by test/views/alumni_network_sidebar_test.rb,
which asserts on the anchor for the current path rather than on the class appearing anywhere in the
body — bg-belmontblue is used by unrelated components, so a body-wide match passes even when every
literal is broken. Verified by sabotage: reverting one literal fails the suite.
Generalisation worth keeping: a rename driven off the route table only fixes what the route
table knows about. Hard-coded path strings — nav highlighting, mailer URLs, notification rows,
ActionItemsService — are invisible to it and are exactly where the damage is silent.
ActionItemsService pointed six navbar-bell links into the old pathsaction_items_service.rb built path: strings
literally, including two with query strings (?needs_leader=true, ?scope=flagged). Dropping a
query string is worse than a 404: staff land on an unfiltered list that looks like a working page
while answering a different question. The wildcard redirect preserves it, and there is a test.
champions_helper.rb was filed as Alumni Lookupapp/helpers/champions_helper.rb sat at root level, which under definition-by-exclusion
(Decision 6) means Alumni Lookup — while all six of its consumers were alumni_network/ views
and one of its methods calls Cp::EngagementScoreService. Moved to
app/helpers/alumni_network/admin_helper.rb.
This shrinks CP_REFERENCES_OUTSIDE_HELD_TREES by one entry, but not by decoupling anything:
the reference became same-surface rather than cross-surface. Recorded as such in the baseline
comment so the ratchet is not read as progress it did not make.
test/controllers/champions/legacy_signup_redirects_test.rb tests /signups/new → /champions,
which is the public portal landing under a host constraint — nothing to do with the admin
surface. It moved with the directory in the first git mv, then to test/controllers/cp/.
Lowered/renamed in the same commit as the removals, per the ratchet rule. No baseline was raised.
| Constant | Change |
|---|---|
HELD_TREE_FILE_COUNTS |
app/controllers/champions → app/controllers/alumni_network (20); app/views/champions → app/views/alumni_network (75); added app/helpers/alumni_network (3) |
ALUMNI_NETWORK_ADMIN_CONTROLLERS |
champions_controller.rb → members_controller.rb |
CP_REFERENCES_OUTSIDE_HELD_TREES |
app/helpers/champions_helper.rb removed (17 → 16 entries) — see §4.4 |
SURFACE_DIRS |
champions → alumni_network |
SURFACE_LAYOUTS |
cp now allows alumni_network; alumni_network allows alumni_network_admin |
SURFACE_OWNED_LAYOUTS |
champions champion_admin → alumni_network alumni_network_admin |
LAYOUT_SURFACES |
now identity except the cp/ row |
held_tree? |
/champions/ → /alumni_network/ |
app/helpers/alumni_network is new to the list, not new to the repo — it is coverage the
ratchet did not previously have.
bin/test — 5,314 runs, 15,076 assertions, 0 failures, 0 errors (was 5,305 pre-23.4; +9 new tests)bin/rails routes — 17 remaining champion helpers, all on the keep-list (§2)/champions/* paths 301 with the query string preserved, covered by test/controllers/alumni_network/legacy_redirects_test.rb — 9 tests including one end-to-end follow-redirect as a portal adminchampions_champion_path et al.) do not resolve — asserted, not assumedas: :champion_signups retired; the view that needed it now links signup_admin_root_pathCp::Champion::INDUSTRIES gone; one industry list repo-widebin/rails runner boots; routes file loads| Why | |
|---|---|
Cp:: / cp_* tables / Cp::Champion |
23.5 cut. Cp:: is a fossil but unambiguous — nothing else looks like it, so nothing has been misfiled into it (README §3). |
Alum-facing portal URLs (belmontalum.com) |
23.4 touches only the staff admin namespace. |
test/fixtures/staff_notifications.yml#read_notification |
Deliberately still on /champions/support_threads/2 — it is the fixture that keeps a realistic pre-rename row in the suite. Commented in place. |
roadmap_controller.rb phase prose mentioning /champions/... |
Historical record of what was built at the time. Rewriting it falsifies the record (23.6 link-rot policy). |
resources :members, both layout renames, and the INDUSTRIES collapse were pulled in from 23.5 (§3). Approved explicitly during planning because cutting 23.5 left them homeless.app/helpers/champions_helper.rb moved. Not in the spec’s file list — it was invisible to a champions/-scoped inventory because it sat at root level (§4.4).ActionItemsService paths (§4.2, §4.3). Both are the same failure mode: hard-coded path strings are invisible to a route-table-driven rename.