alumni_lookup

Phase 23.4 — 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.


1. What This Sub-Phase Is

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

2. The Route-Helper Rename Was Generated, Not Grepped

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.


3. Scope Added Because 23.5 Was Cut

23.5 (Cp::An::, ChampionMember, 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:

3a. resources :championsresources :members

The 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.

3b. Cp::Champion::INDUSTRIES collapsed into Industry::ALL

23.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.

3c. Both “champion” layouts renamed

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.


4. Findings — Things Not in the Plan

4.1 🔴 StaffNotification#url again, and this time it is not hypothetical

23.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.

4.2 🔴 The admin sidebar highlights nothing

_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.

action_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.

4.4 champions_helper.rb was filed as Alumni Lookup

app/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.

4.5 A portal test was misfiled in the admin tree

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/.


5. Architecture-Test Baselines

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/championsapp/controllers/alumni_network (20); app/views/championsapp/views/alumni_network (75); added app/helpers/alumni_network (3)
ALUMNI_NETWORK_ADMIN_CONTROLLERS champions_controller.rbmembers_controller.rb
CP_REFERENCES_OUTSIDE_HELD_TREES app/helpers/champions_helper.rb removed (17 → 16 entries) — see §4.4
SURFACE_DIRS championsalumni_network
SURFACE_LAYOUTS cp now allows alumni_network; alumni_network allows alumni_network_admin
SURFACE_OWNED_LAYOUTS champions champion_adminalumni_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.


6. Acceptance

Deliberately unchanged

  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).

7. Spec Deviations

  1. 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.
  2. 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).
  3. Redirects are a wildcard, not the enumerated list the 23.2 precedent suggested (§4.1).
  4. Two regressions were found and fixed that the spec did not anticipate — sidebar nav literals and ActionItemsService paths (§4.2, §4.3). Both are the same failure mode: hard-coded path strings are invisible to a route-table-driven rename.