alumni_lookup

Phase 23 Launch Guide — Namespace & Naming Unwind

Spec: ../phases/phase-23/README.md Created: July 29, 2026 Update this file as each migration is written — not after.


1. Pre-Deployment: Migrations

Sub-phase Migration Risk Notes
23.1 none Test + shared-constant extraction
23.2 none Controller/view moves only
23.3 none 🟡 Medium No migration, but adds a validation to existing data — see below
23.4 none Route + directory moves only. Adds 4 permanent 301s — see §5
23.5 RenameCpTablesToAn 🚫 CUT. See phase-23 README §8

Phase 23 ships with no migration at all

23.5 carried the phase’s only schema change, and it was cut. Nothing in Phase 23 touches the database — every sub-phase is a code-and-routes deploy, revertible by reverting the deploy.

(Preserved for anyone reviving 23.5: rename_table is mechanically reversible, but the polymorphic UPDATE statements are not tracked by Rails. An explicit down must reverse both, or a rollback leaves the DB in a mixed state — and take a production snapshot first.)


2. Pre-Deployment: Rake Tasks

None. 23.5 was the only sub-phase that would have needed one, and it was cut.


3. Placeholder URLs

None. This phase introduces no external links.


4. Content Dependencies

None. No staff content is required for this phase to be useful.


5. Sensitive Features / QA Focus Areas

23.1 — Boundary enforcement + decoupling

No user-facing change. QA is on the test and the extracted constant.

23.2 — signup split (active surface, public-facing)

Alum-facing URLs did not change — only module and helper names. /sign-up, /sign-up/:id, /profile/:code, /opportunities/:slug, /privacy all resolve as before, so outreach links in alumni inboxes are unaffected.

Staff URLs did change: /champion_signups/signup_admin, /champion_signups/all/signup_admin/signups/all.

StaffNotification#url persists a route path to the database, so notification rows written before this deploy point at /champion_signups/.... Eight 301 redirects cover them. This is the thing most likely to be reported as a bug after deploy, and it needs a click-through on real production rows — not just fixtures.

23.2 — do not “clean up” the legacy redirects

They are permanent. The database rows they serve are already written. The one intentional oddity is as: :champion_signups, kept so a held-surface view keeps resolving without a held-surface write; it retires with 23.4.

23.3 — Shared layer + dev-process extraction

Spec: ../phases/phase-23/23.3-shared-layer.md

🔴 Highest-risk item: the industry validation is applied to existing data

champion_signups.industry has been unvalidated since Phase 19. 23.3 adds inclusion: { in: Industry::ALL }. If any existing row holds an off-list value, that record becomes unsavable on its next update — which breaks the returning-visitor flow for exactly the alumni whose data is oldest, and does so only when they come back, not when we deploy.

Run this before the deploy, on production:

ChampionSignup.where.not(industry: [nil, ""]).where.not(industry: Industry::ALL).pluck(:id, :industry)

Shared artwork renders on all four surfaces

The seals moved out of a held tree. A missed render site is a 500, not a silent miss.

Identity field vocabulary map

The numbers behind 23.3 §6.3 came from dev data and probably understate production. Re-run there before treating the fix as sized:

high   = Cp::ProfileChange::HIGH_PRIORITY_FIELDS
actual = Cp::ProfileChange.distinct.pluck(:field_name)
puts "declared but never logged:    #{(high - actual).inspect}"   # dev: ["pref_name", "maiden_name"]
puts "logged but not high-priority: #{(actual - high).inspect}"
Cp::ProfileChange.where(field_name: %w[pref_first_name college_last_name legal_first_name]).count  # dev: 35

Dev-process extraction

23.4 — Alumni Network Admin (held surface)

No migration, no schema change, no Devise change. The portal is on hold, so the member-facing side is smoke-level — but the staff admin is what moved, and two of its defects are silent.

🔴 Highest-risk item: persisted StaffNotification#url rows

Exactly the 23.2 hazard, on a surface with higher notification volume. Cp::NotifyAdminsJob writes /champions/champions/:id and /champions/verifications/:id into the database on every member signup; Cp::SupportThreadsController and Cp::FeedbackController write their own rows; two portal mailers put /champions/... links in alumni inboxes.

Do not “clean up” these redirects later. They are permanent — the rows are already written.

🔴 Second silent defect: sidebar nav highlighting

The admin sidebar compares request.path against 25 hard-coded literals. If any still says /champions/..., the surface works perfectly and simply never highlights the current page.

Routine


6. Sub-Phase Deployment Stages

Every sub-phase deploys independently. Each is green on its own. With 23.5 cut there is no big-bang left in the phase, and no migration.

One hard ordering constraint, already satisfied:

23.3 before 23.4. 23.3 moved the roadmap controller out of champions/. Shipping 23.4 first would have relocated the phase-status source of truth to alumni_network/ — a second wrong home — and forced /phase-wrap plus CLAUDE.md rules 5 and 7 into the same commit or wrapping breaks silently. Shipped in the right order.

(The other constraint was 23.1 before 23.5 — Cp::Champion::INDUSTRIES had to leave the frozen model before the class rename, or the alum-facing signup flow would go down. 23.5 is cut, and 23.4 deleted the duplicate outright, so both halves are moot.)


7. Post-Deployment Verification

# 23.2 — new routes resolve
bin/rails routes | grep -E "signup_admin|signup/" | head -30

# 23.2 — legacy staff paths still redirect (protects persisted notification URLs)
curl -sI https://<host>/champion_signups | head -3               # expect 301 -> /signup_admin
curl -sI https://<host>/champion_signups/1 | head -3             # expect 301 -> /signup_admin/signups/1
curl -sI https://<host>/champion_signups/opportunities | head -3 # expect 301 -> /signup_admin/opportunities

# 23.2 — no persisted notification URL is left unreachable
echo 'StaffNotification.where("url LIKE ?", "/champion_signups%").count' | bin/rails console

# 23.3 — legacy industry values (run BEFORE deploying the validation)
echo 'ChampionSignup.where.not(industry: [nil, ""]).where.not(industry: Industry::ALL).pluck(:id, :industry)' | bin/rails console

# 23.3 — roadmap moved off the held surface
bin/rails routes | grep roadmap                     # expect root-level /roadmap, no cp_roadmap
ls app/controllers/champions/roadmap_controller.rb   # expect: No such file
grep -rn "champion-portal" .claude/ CLAUDE.md .github/ docs/ | grep -v "alumni-network"  # expect none

# 23.4 — new routes resolve
bin/rails routes | grep "alumni_network" | head -20

# 23.4 — the only route helpers left containing "champion" are the keep-list:
#   cp_champion_* (Devise), cp_champions_landing, alumnichampions_root
bin/rails routes | awk '{print $1}' | grep -i champion | sort -u

# 23.4 — legacy staff paths still redirect (protects persisted notification URLs)
curl -sI https://<host>/champions | head -3                      # expect 301 -> /alumni_network
curl -sI https://<host>/champions/champions/1 | head -3          # expect 301 -> /alumni_network/members/1
curl -sI https://<host>/champions/verifications/1 | head -3      # expect 301 -> /alumni_network/verifications/1
curl -sI "https://<host>/champions/communities?needs_leader=true" | head -3  # query string preserved

# 23.4 — how many persisted rows the redirects are actually carrying
echo 'StaffNotification.where("url LIKE ?", "/champions%").count' | bin/rails console

# 23.4 — one industry list repo-wide
echo 'Cp::Champion.const_defined?(:INDUSTRIES, false)' | bin/rails console   # expect false

# 23.4 — no sidebar nav literal left on the old path (the silent defect)
grep -c "'/champions" app/views/alumni_network/_sidebar.html.erb   # expect 0

8. Rollback Plan

Sub-phase Rollback
23.1 / 23.2 / 23.6 Revert the deploy. No schema change, so no data risk.
23.4 Revert the deploy — no schema change, so no data risk. But the revert is not clean for anyone who already followed a redirect. See below.

⚠️ A 301 is cached by the browser, so reverting 23.4 is not symmetrical

301 Moved Permanently is cacheable by default and browsers cache it aggressively and indefinitely. Once a staff member’s browser has followed /champions/verifications/alumni_network/verifications, it will keep going straight to the new path without asking the server again. Revert the deploy and that path no longer exists, so they get a 404 from a URL they never typed — and re-deploying the old code does not fix it, because the browser is not consulting the server at all.

This is the correct trade for the reason the redirects exist (persisted database rows and links already sitting in alumni inboxes really are permanent, and 23.2 set the precedent with eight of them). It just means the rollback story is:

Do not “solve” this by downgrading to 302. A temporary redirect for rows that are permanently written is a lie the cache would have to be told again on every request, forever. | 23.3 | Revert the deploy. No schema change. But if the industry validation blocked saves before you noticed, no data was corrupted — writes were rejected, not mangled. Fix forward by extending Industry::ALL rather than reverting | | 23.5 | 🚫 Cut. No migration ships in this phase. |

No sessions are invalidated by Phase 23. That warning existed for 23.5’s Devise scope rename (devise_for :cp_champions:an_members), which is not happening. 23.4 did not touch Devise.


9. Open Items