Canonical sources: Portal philosophy, posture, and language live in
/docs/planning/alumni-network/source/README.md.
Use these sources (includingCHRIST_CENTERED__IDENTITY_STATEMENT.md) when writing specs or user-facing copy. Prefer quoting/paraphrasing over inventing new language.
Purpose: Track ideas, nice-to-haves, and intentionally deferred features for the Alumni Network. Not a task list — just a place to capture things so they aren’t lost.
When to Reference: During Sub-Phase Planning Checkpoint (see
CLAUDE.md)Last Updated: March 2026
These are significant features planned for after Beta launch. Prioritization will be based on alumni feedback and engagement data.
| Feature | Description | Priority | Effort | Status |
|---|---|---|---|---|
| Alumni Business Directory | Alumni can list their businesses for other Bruins to discover (“Hire a Bruin”) | High | Medium | Backlog |
| Native Mobile App | iOS/Android apps with push notifications | High | Very Large | Backlog |
| Newsletter Tool | Hard-coded sections + community content sections for staff-curated newsletters | Medium | Medium | Backlog |
| Real-Time Messaging | WebSocket-based instant messaging (currently async) | Medium | Large | Backlog |
| Map View | Visual map of alumni across the country | Medium | Medium | Backlog |
| Advanced Search | More filters, saved searches, search history | Medium | Medium | Backlog |
| TBD | Large | Backlog | ||
| — | — | Superseded | ||
| Staff Reporting & Analytics | Community health dashboard and PDF report exports (Phase 6.2/6.3) — activity metrics & CSV exports already delivered in Phase 6.1; see Phase 6 README | Medium | Medium | Backlog |
Added: January 2026 (Phase restructure)
Description: Alumni with businesses can list them for discovery by other Bruins. Supports the “hire a Bruin” and “support Bruin businesses” use cases.
Key Features:
Data Model:
# cp_businesses table
t.references :champion # Business owner
t.string :name, null: false
t.text :description
t.string :industry
t.string :city
t.string :state
t.string :website
t.string :phone
t.boolean :verified, default: false # Staff can verify
t.timestamps
Priority: High — frequently requested; supports alumni economic network
Added: January 2026 (Phase restructure)
User Spec: “hard-coded + community content sections”
Description: Staff-curated newsletters with both fixed content sections and dynamic community-specific sections.
Key Features:
Priority: Medium — valuable for re-engagement but email platform may handle this
Added: January 2026 (Phase restructure)
Description: Upgrade current async connection-gated messaging to WebSocket-based real-time delivery.
Key Features:
Implementation: ActionCable WebSocket channels
Priority: Medium — current async model works fine for MVP; connections system (Phase 10) already solid
Added: January 2026 (Phase restructure)
Description: Visual map showing alumni locations across the country.
Key Features:
Implementation: Leaflet.js or Google Maps API
Priority: Medium — visually impressive but directory search achieves same goal
Added: January 2026 (Phase restructure)
Description: Enhanced search capabilities beyond current directory filters.
Key Features:
Note: “Champions Like You” recommendations (Phase 5.4) already address the discovery use case. Current directory filters handle most search scenarios.
Priority: Medium — current filters handle most use cases
Superseded by: Phase 10: Connections (February 2026)
Rationale: The Connections system replaces the planned Mentorship Center with a broader, less hierarchical approach. Instead of formal mentor/mentee roles, alumni request connections by type (Say Hi, Career Advice, Networking). This avoids the “loaded meaning” of mentorship while achieving the same goal: helping alumni find people 5 steps ahead or behind them for meaningful conversations.
Added: January 2026 (Phase 2.3/2.4)
Context: As more staff get access to the portal, the current CRM changes workflow (buried in /settings/affinaquest/crm_changes) needs to be more accessible and streamlined.
Current State:
Potential Improvements:
/champions/crm_changes with streamlined UIPriority: Medium — works for now, revisit as portal_admin usage grows
Added: August 2026 (Phase 22.6 — held, not deferred work)
Context: Signup-reported student groups were logged as CrmDataChange rows from 22.4 until 22.6, when they were pulled back out. The blocker is not code: nobody has decided how an alum-reported group becomes a validated affinity_code, or who owns that call, so the rows accrued in a queue staff work by hand and could not be acted on. affinity_other (free text) is the sharper version of the same question.
Current State:
ChampionSignupCrmLogger no longer writes affinity_code / affinity_other rowsaffinities is listed in CrmDataChange::EXPORT_HELD_SOURCE_TABLES, so the rows 22.4 already wrote stay out of the staff page, the CSV, and “Mark All Exported”champion_signups.affinity_codes and affinity_other still hold what alumni reported, which is what a re-enable would diffalumni_affinities remains the local record; BruinQuest is the system of recordTo Re-Enable:
affinities with free text held back?)EXPORT_HELD_SOURCE_TABLES and restore the logger’s affinity branch (deleted in 22.6, recoverable from git)affinity_code earns its own column in the grouped CSV or stays an ad-hoc row — alumni_affinities is one-to-many, so it cannot join the alumni rowPriority: Blocked on an Advancement Services decision, not on engineering time.
Added: July 2026 (deferred from Phase 24 — see phases/phase-24/README.md §5, §8)
Context: Phase 24 imports CRM employment as free text. “VUMC Vanderbilt University Medical Center” appearing 12 times is one employer, not 12 entities — but the employments table has no concept of an employer as a thing, only a string per row.
Current State:
employments.employer_name is free text straight from the CRM, plus a normalized, trigram-indexed employer_name_normalizedemployer_name is nullable, so any future employer table must tolerate rows with no employer at allPotential Improvements:
employers table with employments.employer_id, so an employer has an identity, a count, and a pageGROUP BY employer_name counts spellings, not companiesPriority: Low — fuzzy search covers the asks driving Phase 24. Revisit if staff start asking “how many alumni work at X” rather than “who works at X”, which is the question the string can’t answer.
admin.alumnilookup.com)Added: December 4, 2025 (Phase 1 planning)
Idea: Create a separate admin portal at admin.alumnilookup.com that consolidates settings from both Lookup Portal and Alumni Network.
Benefits:
Considerations:
Status: ⚠️ Superseded in part — reconciled July 30, 2026 (Phase 23.6).
Phase 23 Decision 1 rejected a generic /admin/ on exactly the premise this item
assumes. Consolidating “all administrative functions” into one undifferentiated admin
surface reintroduces the ambiguity the phase existed to remove, because two products
have staff admins and they are not interchangeable:
| Product | Its staff admin | Status |
|---|---|---|
| alumnichampions.com signup ecosystem | /signup_admin/ |
✅ Active |
| belmontalum.com member portal | /alumni_network/ |
🛑 Held |
An admin path must name the product it administers. /champion_signups/ vs
/champions/ — one character apart, administering different products — is the
concrete failure this idea would recreate at the domain level, and it was violated
often enough to require a hold gate and an enforcement test.
What survives: the underlying complaint (“where is that setting?”) is real, and
/settings/ is genuinely split across surfaces. The answerable version is a
cross-surface settings index that links into each product’s own admin, not a
fourth surface that absorbs them. That is a navigation problem — see 2.2, which is the
better-shaped version of the same need.
Do not implement 2.1 as written. It argues against the shipped architecture.
Added: December 4, 2025 (Phase 1 planning)
Idea: Users with permissions to multiple portals should have an easy way to switch between them.
Proposed Solution: Header dropdown showing available portals based on user permissions:
Status: NOT IMPLEMENTED — Should add before general availability
Priority: Medium — Quality of life for staff who work in both portals
Added: December 4, 2025 (Phase 1.1 implementation)
Idea: Admin interface to edit district display names without direct database access.
Context: Districts are imported from CSV with long MSA names (e.g., “Nashville-Davidson–Murfreesboro–Franklin”) but we want short display names (e.g., “Nashville”). The csv_name column preserves the original for import matching, while name is the editable display name.
Scope:
Priority: Medium — seed script auto-generates short names, but staff will want to customize ~50 key metros
Status: Seed script handles most cases. Manual DB edits sufficient for now.
Added: December 4, 2025
Idea: Type-ahead autocomplete for ZIP code entry that shows city/state as user types.
Priority: Low (ZIP entry is one-time during signup)
Added: December 4, 2025
Idea: Client-side image cropping before upload to ensure consistent profile photos.
Libraries to consider: Cropper.js, react-image-crop
Priority: Medium (better UX, but not blocking)
Added: December 4, 2025
Idea: Add service worker and manifest for Progressive Web App capabilities.
Benefits:
Priority: Medium (good for mobile-first, enables future native app transition)
Added: December 17, 2025 (UX polish for Lookup Portal)
Context: Batch Search tool allows staff to search for multiple alumni by name/email. Current implementation works but has several UX gaps.
Enhancements:
Fix result table column alignment. Use fixed-width columns or sticky headers.
When a search returns multiple matches, allow user to select which result to export (radio buttons or clickable cards).
Audit all alumni export formats across the application. Create shared export format applied to batch search.
Toggle to hide rows with 0 matches.
Radio buttons before export: All, Matches only, Non-matches only.
Copy Table as TSV button, individual cell copy, XLSX export option.
Priority: Medium-High (staff-requested feature)
Effort: Low–Medium per item
Implemented: January 2026 (Phase 1.9.1) — Settings page with email change flow, confirmation email, CRM data change logging.
Implemented: December 2025 (Phase 1C) — Single “First Name” field with collapsible section for legal name and maiden name.
Added: December 22, 2025 (Phase 1.6.2)
Idea: When a verified alum is the only person in their district, show an “Invite another Belmont Alum” link/button on the dashboard that launches an invite flow.
Context: A placeholder link exists on the dashboard today but does not trigger any flow.
Priority: Medium — reinforces growth and community-building
Implemented: January 2026 (Phase 1.9.3) — Simplified directory cards, large photos with role badge, education display with college SVG icons, mobile-first layout.
Added: January 2026 (Phase 5.3 fixes). Updated: February 2026 (Phase 9 status).
Purpose: Track features, pages, and settings that need better user education — through tooltips, callouts, welcome emails, or in-app guidance.
Status: All tooltip calls were removed in February 2026 during UX overhaul. The tooltip infrastructure (controller, helper, Stimulus controller, model methods) is preserved and ready for re-integration. When re-adding tooltips, design them around the ideal onboarding flow and current feature set.
Suggested tooltips for re-implementation:
| Feature | Location | Tooltip Content Idea | Priority |
|---|---|---|---|
| Directory Search | Directory | “Search for alumni by name, filter by college, district, or graduation year.” | High |
| Community Discovery | Home / Communities | “Communities connect you with alumni who share your interests, location, or college.” | High |
| Connection Requests | Directory card / Profile | “Send a connection request to start messaging this alum.” | High |
| Career Resources | Careers page | “Access resume guides, interview prep, and alumni mentors.” | Medium |
| Privacy Settings | Settings → Privacy | “Control who can see your location, contact info, and profile.” | Medium |
| Notification Settings | Settings → Notifications | “Choose how you hear about new messages, events, and community activity.” | Medium |
| Connections Preferences | Settings → Connections | “Set limits on who can send you connection requests.” | Medium |
| Profile Completion | Dashboard | “A complete profile helps fellow Bruins find and connect with you.” | Medium |
| Board Discussions | Community Board | “Start conversations, ask questions, and share updates with your community.” | Low |
| Location | Callout Content | Status |
|---|---|---|
| Messages Index | “You’re in control. Privacy Settings let you choose who can message you…” | ✅ Implemented (Phase 5) |
| Directory Index | “Your privacy matters. Privacy Settings let you control who can see your location…” | ✅ Implemented (Phase 5) |
| Connections Page | “Connections you make can reach you via direct messaging…” | ⏳ Planned |
| Profile Edit | “Your profile helps other alumni find and connect with you…” | ⏳ Planned |
| Email Trigger | Content to Include | Status |
|---|---|---|
| Account Verified | Welcome! Here’s how to get started… | ⏳ Backlog — Phase 9.6 spec ready, deferred March 2026 |
| First Login | Quick start guide: Find alumni, Update Profile, Join Communities | ⏳ Backlog — Phase 9.6 spec ready, deferred March 2026 |
| First Connection Made | “Nice! You connected with [Name]. Here’s how messaging works…” | ⏳ Planned |
| Idea | Description | Priority |
|---|---|---|
| Onboarding Checklist | Dashboard widget showing progress | Medium |
| Feature Spotlight | Modal popup for new features after major releases | Low |
| Guided Tour | Step-by-step tour on first login (Shepherd.js) | Low |
| Contextual Help | ”?” icons next to complex settings | Medium |
| Resource | Description | Status |
|---|---|---|
| Help & FAQ | Searchable FAQ page | ✅ Implemented (Phase 1.9) |
| Privacy Guide | Detailed explanation of privacy settings | ⏳ Planned |
| Getting Started Guide | Walkthrough for new alumni | ⏳ Planned |
| Video Tutorials | Short videos showing key features | Future |
Added: August 4, 2026 (surfaced while fixing the Analytics/Breakdown timeouts)
Context: The timeout fix was purely about query shape — no measurement definition was changed. But profiling surfaced several counting decisions that are currently implicit and worth deciding on deliberately. Figures are from the dev database, August 2026.
1. Only the first major is counted. Education::AggregateScope selects the
major with ORDER BY eaos.id ASC LIMIT 1. 4,870 educations have more than one
concentration_level = 'major' — every one of those alumni is attributed to a
single major. Same class of issue as the “show what we have on file” rule in
.claude/skills/debug/SKILL.md. Decide: first-major, primary-major (needs a flag),
or count the alum once per major.
2. Minors and concentrations are invisible to stats. 22,044 minor and 8,123 concentration rows never enter the aggregate scope at all.
3. 563 alumni can never match a college filter. Of the 604 alumni on the
legacy Degree fallback (1.2% of the population), only 41 rows carry a
college_code. They silently drop out of every college-filtered figure rather
than appearing as “unknown.”
4. College attribution prefers current school over granting school.
COALESCE(NULLIF(NULLIF(current_school_code,''),'00'), granting_school_code). An
alum who earned a credential in one school but is now enrolled in another is
counted under the school they are in now. 180 rows fall through to granting.
Confirm this is intended — it is a real semantic choice, not an accident.
5. Education-first is now overwhelmingly the real path: 50,474 of 51,078
alumni (98.8%) resolve through educations + education_areas_of_study. The
degrees + majors fallback covers 604. major_code is NULL on all 53,447
education rows, so that column in the aggregate scope is effectively dead.
6. The tabs do not agree on who counts when college AND year are both set.
Overview and Analytics apply the two filters independently (any credential
from that college, any credential from that year — not necessarily the same one).
Breakdown, Matrix and Demographics require one credential satisfying both. On
dev data, college=CE + year=2015 gives 46 engaged alumni on Overview and 42 on
Breakdown. This predates all of the August 2026 work and is now explicit in
BaseService as #population_engaged_buids (loose) vs #eligible_buids
(strict), pinned by test/services/engagement_stats/population_semantics_test.rb.
Pick one. Unifying them is a one-line change; deciding which is correct is
the actual work, and it will move published numbers either way.
7. TimeoutProtection cannot interrupt a running query — it never could.
Staging logs show Completed 302 Found in 15005ms (ActiveRecord: 14999.2ms)
immediately followed by Heroku H12 ... service=30000ms status=503 on the same
request id. Timeout.timeout raises via Thread#raise, which cannot preempt a
thread blocked inside libpq, so the 15s guard only fires once the query returns
on its own. Users see Heroku’s 30s 503 (“Something Went Wrong”), not the intended
flash. A database-level statement_timeout is the mechanism that actually works;
the current guard mostly adds a misleading message. Same class of hazard as the
memoized-Redcarpet bug in .claude/skills/debug/SKILL.md.
8. EngagementStatsController duplicates the service layer. It carries its own
#filtered_alumni, #engaged_alumni_count, #build_filtered_engagement_scope and
#capped_level_to_points_sql, which is why the v1.0.82 service fixes missed the
Matrix quadrant drill-down entirely. Both copies now do the same thing, but they
will drift again. #quadrant_alumni, #load_top_alumni_data and the CSV exports
should move into services (EngagementStats::QuadrantService), leaving the
controller to set filters and render. Also note #load_quadrant_alumni issues one
query per alum in the quadrant for their activity list — negligible today (~0.04s
for 73 alumni) but it is a real N+1.
9. Modal/AJAX endpoints have no timeout guard. TimeoutProtection is
only: [:index], so #quadrant_alumni runs until Heroku’s 30s H12 and the modal
that issued the fetch silently swallows the 503 — the spinner just stops, with no
error shown. Either give these endpoints their own guard returning a JSON error,
or have the client check status/content-type (see “Modals with API Calls” in the
debug skill).
Also worth revisiting: TimeoutProtection’s message (“Please filter by college
or year to reduce the data set”) has misdiagnosed the cause every time it has
fired, and its advice makes the query more expensive. It also redirects to
request.referer, and Timeout::timeout raising asynchronously mid-query is the
same hazard documented for memoized Redcarpet renderers.
Related: the four EngagementStats::*Service namespace violations already
listed in this backlog are unaffected by this work.
Added: February 18, 2026 (Hotfix for duplicate daily digest emails)
Context: The daily/weekly digest job used deliver_later, causing dedup race conditions. Fixed in v1.0.45 by switching to deliver_now. This is fine up to ~500 recipients.
Current State: ✅ Fixed with deliver_now (v1.0.45). Works reliably for current scale.
When to Revisit: When daily digest recipient count exceeds ~300-500.
Long-Term Fix — Synchronous Dedup + Async Delivery:
Write the EmailLog dedup record synchronously (with ‘queued’ status) before deliver_later:
log = Cp::EmailLog.log_email(champion: champion, email: champion.email,
type: 'daily_digest', subject: 'Your Belmont Champions Update', status: 'queued')
Cp::NotificationMailer.daily_digest(champion, notifications, ...).deliver_later
Implementation Steps:
'queued' to Cp::EmailLog::EMAIL_TYPES status validationsqueued recordsEmailLoggingInterceptor to find+update existing queued logfailed callback for Sidekiq delivery failuresqueued records (> 1 hour old → mark failed)Priority: Low (current fix handles up to ~500 recipients reliably)
Added: December 4, 2025 — corrected and closed August 7, 2026 (Phase 27.1)
Idea: Tool for staff to merge duplicate alumni accounts if someone signs up multiple times before verification.
The entry was already stale when Phase 27 found it. ChampionSignupMerger had existed
for some time — newest row wins, blanks fill from older rows, open text combines with date
tags, sources soft-delete — with a staff UI behind it: BUID assignment with suggested
alumni matches, per-BUID merge, and a bulk pass. What was actually missing was never the
tool. It was the key: both entry points required a non-blank BUID, and BUID assignment
is a manual staff action, so a signup with no matching Alumni row could never be merged
at all.
Phase 27.1 added merge_duplicates_for_email (staff-reviewed, one pair at a time, refused
when the rows resolve to different BUIDs), fixed duplicates_by_email, and surfaced email
duplicates alongside the BUID ones in Signup Admin.
Phase 28.1 removed all of that (August 14, 2026) — not because it was wrong, but
because a partial unique index on LOWER(email) WHERE deleted_at IS NULL removed the
population it served. An address names at most one active row, so an email-keyed merge has
nothing to merge and the shared-family-inbox conflict it refused can no longer arise. The
tests were what forced the deletion: every one of them described a state the database can
no longer hold. ChampionSignupMerger now asserts that neither merge_duplicates_for_email
nor merge_all_duplicates_by_email! responds, so the key cannot grow back.
BUID is now the only merge key, and the only duplicate shape the app can produce — two different addresses on one BUID, which at 95% BUID coverage is ordinary.
Still open, deliberately: this entry says alumni accounts. Merging covers
champion_signups only. Reconciling duplicates across alumni / champion_signups /
cp_champions is Phase 26’s question and is blocked on the portal unfreezing.
Added: December 4, 2025
Idea: Build Alumni Network features with RESTful API endpoints to support future native mobile app.
Status: Keep in mind during implementation. Controller actions should be API-friendly.
Added: December 4, 2025
Status: ✅ Partially addressed in Phase 1.9.4 — dashboard shows region fallback when alum is first in their district with “You’re the first here!” celebration and invite CTA.
Remaining: Directory filtering could still benefit from region expansion for small districts.
Items intentionally deferred from specific phases. Updated July 2026.
Spec: phases/phase-23/README.md. Phase 23 was re-scoped July 29, 2026 and again July 30, 2026, both times after measuring value against churn — the items below were cut deliberately.
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
Cp:: → An:: and Cp::Champion → Member (all of sub-phase 23.5) |
23.5 | Cut July 30, 2026, before any work began. 389 files, 4,413 Cp:: references, 56 tables, 109 FK/index schema lines, and the phase’s only migration — whose polymorphic type-string UPDATEs are the one change in Phase 23 that fails silently (a missed column makes associations return nothing rather than error). The phase’s own ranking already answered “prevents misroutes? No” — Cp:: is a fossil but an unambiguous one, so nothing has ever been misfiled into it. The counter-argument was purely about timing (“on-hold is the only zero-conflict window”), which does not buy the largest file count and the only irreversible step in the phase. 23.4 absorbed the three consequences that mattered: resources :members, both layout renames, and the Cp::Champion::INDUSTRIES collapse. See phase-23 §8 |
Backlog — revisit only with the monolith split, on the same terms as lookup/ |
lookup/ namespace for Alumni Lookup (Lookup::, /lookup/*) |
23 | Measured at ~927 route-helper refs across 155 files (settings_* alone is 536), on the only active surface in the phase — so the only sub-phase that could break daily staff work — while preventing zero known misroutes, since nothing competes with root-level. Superseded by definition-by-exclusion: once signup/, signup_admin/, cp/, and alumni_network/ exist, “in the lookup” means everything else. See phase-23 Decision 6 |
Backlog — revisit only if the monolith split is revived |
Rename people_controller → alumni_import (it is an alumni CSV import, not a “people” surface) |
23 | Was carried by the dropped lookup/ sub-phase; unaccompanied now |
Backlog |
Move app/views/opportunities/ → app/views/shared/opportunity_fields/ (shared partials used by both the public form and the staff CMS) |
23 | Same — carried by the dropped sub-phase. Note it must not nest under champion_signups; that would break the staff CMS |
Backlog |
Relocate EngagementStats::RetentionService and EngagementStats::DiscussionBoardsService out of engagement_stats/ |
23.1 | These are wholesale Cp:: analytics living in engagement_stats/ — a live violation of CLAUDE.md’s “Never put Cp:: analytics in engagement_stats.” Relocating them is held-surface work and would expand 23.1 beyond enforcement. Allowlisted in the 23.1 architecture-test baseline instead. 23.4’s held approval did not cover them — it was scoped to the rename, and folding ~45 more references into a 1,401-reference diff would have wrecked reviewability |
Backlog — allowlisted, ratchet down later |
Merge statistics into engagement_stats |
23 | Investigated: they are not duplicative — statistics is demographic/grad-year bucketing, engagement_stats is activity scoring |
Rejected, not deferred |
Relocate the three OnboardingInsights::* services and ActionItemsService |
23.1 | Found while capturing the 23.1 baseline, not during planning. All four are root-level (nominally Alumni Lookup) but query Cp:: — ActionItemsService alone holds 20 references, the largest single coupling in the repo. Same disposition as the engagement_stats pair: allowlisted, not moved, because the fix is held-surface work |
Backlog — allowlisted, ratchet down later |
Split AlumniHelper’s signup-domain methods into a shared presenter |
23.3 | Nine of its methods are signup presentation (champion_signup_lean_*, prospect_champion_seal, has_completed_champion_signup?) living in a root-level helper, which under definition-by-exclusion reads as Alumni Lookup. Nothing is broken — Rails’ global helper inclusion means the location has no functional effect — so moving nine methods and their tests is real churn for a documentation-shaped benefit. 23.3 adds a header flagging them as reusable instead |
Backlog — inventory recorded, not moved |
| A canonical US state list | 23.3 | state appears on zip_codes and address fields with no shared list. Nothing is broken today, and creating a constant with no caller is an orphan (CLAUDE.md rule 11). Filed so the second app to need a state dropdown finds a decision instead of a blank page — this is how industry diverged |
Backlog — create on first real caller, in the shared layer |
| Markdown link checker in CI | 23.6 | 23.6 swept all of docs/**, not just docs/planning/**: 115 broken relative links → 34. 68 were de-linked in historical phase specs (prose kept — see the policy below), 11 repointed at targets that had moved, 2 were Phase 23’s own. Ad-hoc fixing does not stop regression; a checker does. Two things it must tolerate or it cries wolf on arrival: qa/PHASE_21_LAUNCH_GUIDE.md’s documented javascript:alert(1 XSS test payload, and absolute app routes in the compliance drafts (/privacy, /community-guidelines, /cookie-policy) which are Rails paths, not files |
Backlog |
| Write the two missing canonical source documents | 23.6 | 19 of the 34 residual broken links point at ALUMNI_CHAMPIONS__ONBOARDING_PHILOSOPHY.md and HOPE_TRANSFORMS__FUNDING_PRIORITIES.md, both promised by alumni-network/source/README.md and neither ever written. This is a gap in the source corpus, not link rot — and CLAUDE.md forbids inventing theological language, so they cannot be filled by an agent. Left linked deliberately: the links are the record of what is owed |
Backlog — needs an author, not a fix |
| Monolith split into five apps + API | 23 | Previously proposed and rejected; worth reapproaching after namespacing, since clean namespaces are step one either way. The 23.1 allowlist doubles as the real coupling inventory. Note that four of the five apps are now namespaced by directory; the fifth (Lookup) is defined by exclusion, and the split is the one scenario that would justify reviving both lookup/ and 23.5 |
Backlog — revisit with data after a few phases of ratcheting |
Rename app/helpers/alumni_network/admin_helper.rb’s generic methods into shared |
23.4 | render_sort_header, format_engagement_score and completion_badge_class are surface-agnostic table/formatting helpers that happen to live in a held tree. Moving them is a shared-layer write, which is allowed — but they have exactly one consumer today, and a shared file that cannot name a second consumer is misfiled, not shared (CLAUDE.md). Filed so the second consumer finds a decision |
Backlog — move on first cross-surface caller |
Spec: phases/phase-27/README.md. Added August 7, 2026 while building 27.2.
| Item | Found | Reason Deferred | Status |
|---|---|---|---|
| 27.2 QA | Raised as a possible replacement for magic links when sign-in didn’t feel like signing in. It wasn’t the cause — an OTP that doesn’t establish a session behaves identically, and the real gap was #show never adopting the session (fixed in 27.2). Where OTP genuinely wins is cross-device: read the email on your phone while working on a laptop, which a link cannot solve. Costs a new endpoint, a code store or signed code, its own throttle, and funnel friction |
✅ Shipped in 27.7 on exactly the predicted trigger — cross-device. ChampionSignupSigninCode: 15 minutes, one use, five attempts, its own signin-code/ip throttle, riding in the SAME email as the link so there is no second send. 27.8 then made it the lead and demoted the link to a fallback: the link opens the hub on whatever device reads the mail, which loses the reader’s place; the code returns them to the browser they left. The “funnel friction” cost priced in here did not materialize, because the code is optional — the link still works |
|
| Slim secondary header bar for signed-in state | 27.2 interview | The sign-in/sign-out affordance is currently grouped with the Belmont logo in the existing sticky bar. A thin strip below it has more room, at the cost of vertical space on a conversion funnel. Pinned deliberately rather than built | Revisit in 27.3, when “Not you?” and the recognized tier add a third and fourth state to that group |
last_seen_at with the denormalized last_return_visit_at stamp |
27 spec §5 | ✅ Reconciled at the Phase 27 wrap (August 12, 2026): they do not merge, and the note that framed them as duplicates was wrong. ChampionSignupSession#last_seen_at describes a device and only exists for someone who ticked “keep me signed in” — so a returned scope built on it would silently under-count every alum who did not, which is most of them. champion_signups.last_return_visit_at describes a person, exists for everyone, and has six consumers (returned / never_returned / returned_or_enriched scopes, Signup Admin’s GREATEST(...) sort, the return-activity badges, and Alumni::SignupProfile#returned_at). The 27.3 comment calling last_seen_at “a better return-activity signal” is corrected in champion_signup_session.rb. Nothing to remove |
✅ Closed — no work |
| Vendor the five jsDelivr JavaScript modules locally | 27.6 | Found while writing the privacy policy, and it is the last third-party origin on the alum-facing surface. config/importmap.rb pins flowbite, trix, chart.js, chart.js/auto and @kurkle/color to cdn.jsdelivr.net, and javascript_importmap_tags emits <link rel="modulepreload"> for each — an eager fetch on every page load, including /sign-up. Removing the preload does not help: application.js imports flowbite and trix unconditionally. Two distinct exposures: jsDelivr sees every visitor’s IP (the same §3.6 fact pattern as the fonts), and third-party JavaScript executes on the page where alumni type their contact details. phase-27 §9 scoped this out (“note the link; do not pull it in”), so 27.6 corrected the claim instead — both policies now name jsDelivr. Fix is bin/importmap pin --download for the five pins |
Backlog — do this with CSP enforcement, which it makes materially cheaper; pinned as a ratchet in signup_compliance_test.rb so no new origin can be added quietly |
| Self-host Material Icons (and Sofia Sans) | 27.6 | Google Fonts turned out to be in seven layouts, not the one §3.6 named — but only signup and application loaded Montserrat. signup_admin, tools and settings load Material Icons; application also loads Sofia Sans and a Chart.js copy from cdnjs.cloudflare.com. These are staff-only surfaces, so the privacy stakes are lower than the alum-facing one 27.6 closed, and an icon font is a bigger job than a text font (ligature CSS, codepoint coverage) |
Backlog — lower priority than the jsDelivr row above |
| The two held layouts still load Google Fonts | 27.6 | layouts/alumni_network (Montserrat + Material Symbols) and layouts/alumni_network_admin (Material Icons) were left untouched — editing them is a held-surface write. The work is deleting three lines and rendering shared/_fonts |
Do at portal unfreeze; shared/_fonts already exists and names its consumers |
cp/profile/_completion_banner leaks its own doc comment onto the page |
27.8 | Found by the repo-wide ERB comment scanner 27.8 added, which is the second live instance of the bug it was written for: the header comment shows Usage: <%%= render ... %>, so the comment closes at that tag and lines 7–15 (the weighted-scoring table) render as page copy on the portal’s profile edit page. The fix is deleting one ERB tag from a comment. Not done because app/views/cp/** is a held surface and CLAUDE.md rule 2 requires explicit approval — and because a one-line fix is not worth spending an approval on a frozen page nobody is loading. test/views/erb_comment_integrity_test.rb excludes held trees by path and names this instance in a comment, deliberately rather than adding it to a baseline: a named exclusion reads as debt, a baseline entry reads as clean. Delete the exclusion in the same commit that fixes it |
Do at portal unfreeze — or sooner with one line of approval |
roadmap_controller.rb |
27.2 | ✅ Resolved in 27.1, which added all six 27.x entries. Phase 26 is still absent — CLAUDE.md rule 7’s source of truth does not know that phase exists, and 26 is the one blocked on the portal unfreezing, so nothing else will surface it | Backlog — add Phase 26’s roadmap entry |
Spec: phases/phase-28/28.3-step-one-and-prefill.md. Added August 14, 2026 while building 28.3.
| Item | Found | Reason Deferred | Status |
|---|---|---|---|
status is derived from field presence, not funnel position — replace it with an explicit furthest_step column |
28.3 | This has now caused two separate regressions and will cause a third. ChampionSignup#calculate_status infers where someone is in the funnel by asking which fields are filled, so moving a field between steps silently changes everyone’s status — and before_save :ensure_status_updated re-derives it on every save, so no data fix survives. (1) ZIP moved into step 1 around November 2025 and every v2 signup became “Completed” on creation. (2) Phase 25.5 then collapsed the Alumni Champion Signup filter from six options to two because of it (alumni.rb:362) — a derived field nobody trusted quietly removed four filters from a staff tool. 28.3 repaired the rule (top rung now needs a ZIP plus evidence of progress) but not the mechanism. The durable fix is an explicit furthest_step column, backfilled across 244 rows, with every reader of completed? / scope :completed / status_label re-pointed. Deferred for cost and blast radius — it touches Signup Admin reporting and Alumni prospect logic — not because the derived version is sound |
Backlog. Do this before moving another field between steps. Sequence it ahead of any funnel restructure, not after |
| Revisit Phase 25.5’s collapsed Champion Signup filter | 28.3 | 25.5 cut four of six filter options on the reasoning that “every v2 signup is complete on creation, so four of those options could only ever surface v1 stragglers.” That premise is now false — after 28.3 a signup reports its real position, so “signups in progress” distinguishes something again. The options were removed, not hidden, so restoring them is real work and wants the furthest_step row above first |
Backlog — blocked on nothing, but cheaper after furthest_step |
| The hub’s missing-ZIP banner is now the primary ZIP collection path | 28.3 | ZIP moved off step 1, so every new signup lands on the hub with the amber “We’re missing where you live” banner showing. That banner was built for an edge case and is now the main road: worth measuring whether it converts, and whether “Where you are now” is the right destination for someone who came to type five digits | Backlog — measure before changing anything |
Raised July 30, 2026 while planning 23.3. These are not backlog items to forget — they are drafted as Phase 26. Listed here so a search for the symptom finds the phase.
| Item | Found | Status |
|---|---|---|
Two contradictory write policies for the same fields — Cp::SyncChampionToAlumni writes alumni; Alumni::SignupProfile never writes and resolves at read time. Same alum, same ZIP, different outcome depending on which app they used |
23.3 planning | Phase 26, Decision A. 23.3’s vocabulary map removes the last visible sign of this conflict — do not treat 23.3 as having fixed it |
Cp::SyncChampionToAlumni has 17 test references and zero application callers — the only bridge from a portal edit to crm_data_changes. Dev counts: change_source: "champion_portal" = 1 row, Cp::ProfileChange = 327 |
23.3 planning | Phase 26.3. Harmless while the portal is on hold; the moment it unfreezes, alum-reported changes silently stop reaching Advancement Services |
cp_profile_changes is superseded by crm_data_changes — the newer table carries all three subject FKs, natural keys, source discrimination and export lifecycle |
23.3 planning | Phase 26.4. ⚠️ It lost its forcing moment. This was filed on the assumption that 23.5’s cp_* → an_* migration would force the question — port the table and you make it permanent by inertia. 23.5 was cut, so no migration is coming and nothing will surface this on its own. Phase 26.4 has to raise it unaided |
“Newest active signup” is a private policy — correct, but known only to Alumni::SignupProfile. A second consumer picking differently produces a different answer for the same person, silently |
23.3 planning | Phase 26.1 (Alumni::Identity) |
Field gaps: alumni has no street column; champion_signups has no preferred-name column; champion_signups has both address and street; cp_champions.state is limit: 2 while others are unbounded |
23.3 planning | 23.3 records them in IdentityField::MAP; Phase 26.5 resolves them |
Measured coupling inventory (23.1 baseline, July 29, 2026). CP_REFERENCES_OUTSIDE_HELD_TREES in test/architecture_test.rb is the live version of this list; it may shrink but not grow.
Cp:: in code — down from 34 once comments are excluded. Half of what looked like coupling was documentation about coupling, which is worth knowing before estimating a monolith split off a raw grep.Feature doc: docs/features/ALUMNI_OPPORTUNITIES.md. Everything below was a deliberate MVP boundary, not an oversight.
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
belmontalum.com / Cp:: Opportunities surface |
21 | Deferred with the whole portal. Models are namespace-neutral, so this is an additive nullable champion_id column + a second controller on the same tables — a migration, not a rewrite (spec §7) |
Blocked on the portal resuming |
| Anonymous view analytics | 21.2 | Only submissions are tracked in MVP; an anonymous view has no signup to hang a ChampionSignupEvent on |
Backlog |
| Public index page listing all opportunities | 21.2 | Staff prefer targeted links; no index means an opportunity only gets the traffic staff send it. 21.7’s listed flag is the piece that would make one safe — an index would show listed forms only |
Backlog |
| Per-response status workflow (applied → confirmed → completed) | 21.4 | MVP is lead capture + notify + export; staff follow up offline. reviewed_at is read/unread for the badge only, explicitly not a fulfillment state |
Revisit if staff need fulfillment tracking |
| Export across all opportunities | 21.4 | Answer keys differ per opportunity, so a union CSV is sparse and grows a column with every new form. Export is always scoped to one | Won’t build as specced |
| 21.2 | ✅ Shipped in 21.8 as send_receipt_email, opt-in per opportunity. The boundary held while the email could only repeat the page; structured event details gave it something a person needs later, in a place they can find it |
Done | |
“Add to calendar” (.ics) on the thank-you page and the receipt email |
21.8 | The natural follow-on now that starts_at is a real timestamp. Only meaningful for an opportunity with a precise time, which an ongoing one deliberately has none of |
Backlog |
| Cover images on opportunity cards | 21.9 | The event card’s strongest visual element and the one thing 21.9 did not carry over. Needs an Active Storage attachment plus a staff upload field — a real feature, not a restyle | Backlog |
| A “past opportunities” view | 21.9 | Expiry hides an opportunity outright (21.8); the Alumni Network keeps past events browsable. Nothing has asked for it here | Backlog |
| Per-opportunity timezone | 21.8 | Every opportunity is a Belmont event, and config.time_zone is Central. Revisit if a virtual one draws a national audience |
Backlog |
| Recurring / multi-date opportunities | 21.8 | An ongoing opportunity covers “no fixed date” and a range covers “these days”. A real recurrence model — separate dates, per-date capacity — is a different feature, not a column | Backlog |
| 21.5 | ✅ Shipped in 21.7 as checkbox. The fence held as designed: a code change, and now available to every opportunity |
Done | |
| New field types beyond the six (file upload, number, …) | 21.5 | A code change by design — that’s the fence that keeps this a row editor rather than a form builder. Once added, available to every opportunity | Backlog |
| Conditional / branching form logic and custom per-field validation | 21.5 | Explicitly out of scope; that’s a form builder | Won’t build |
| SMS opt-in on the opportunity form | 21.2 | Phone stays a plain optional field; keeps the form short | Revisit if staff start texting opportunity leads |
| Staff notification batching | 21.4 | One StaffNotification per response, and as of 21.7 one email per response on opted-in forms |
Trigger: a widely shared link floods the queue or an inbox despite the rate limit → collapse to “N new responses” per opportunity per hour |
| Drag-handle field reordering after responses exist | 21.5 | Allowed (order is display-only and doesn’t affect answer keys), but simple up/down is enough | Backlog |
rake opportunities:export/import for staging→production content promotion |
21.5 | Re-entering through the CMS on production takes ~10 min and re-verifies the editor. Build only if re-entry proves annoying | Backlog — see launch guide §2 |
| Global/default notification recipient list | 21.7 | Rejected at interview: recipients are per-opportunity by nature (board vs. newsletter vs. general), and a global default would mean a widely shared link mails somebody by default — the exact inbox-flood risk 21.4 wrote its no-email rule to avoid | Won’t build as specced |
Move the staff response detail’s inline signup → alumni walk onto OpportunityResponseDigest |
21.7 | The view does the same three-hop lookup the digest now does, inline. A second consumer is the argument for the shared service; deliberately not folded in during 21.7 to keep the diff scoped | Backlog |
Revisit Cp::WelcomeContentGenerator.markdown_to_html sanitization |
21.5 | It sets neither escape_html nor filter_html and ends in .html_safe. Safe for its own developer-authored YAML input, but worth a separate look. Out of scope for Phase 21, which shipped its own OpportunityMarkdown renderer instead |
Backlog |
Importer doc: docs/features/CSV_IMPORTERS.md. Spec: phases/phase-24/. All deliberate scope boundaries.
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Employment history | 24 | The source feed emits current employment only. A history table would be populated with fiction, not data | Won’t build without a history-bearing source |
Employer entity resolution / canonical employers table |
24 | Fuzzy search answers “who works at X” without committing to a canonical table. See §1.8 above for the full write-up | Backlog |
| Staff editing employment in the app | 24 | Manual edits would be silently reverted by the next import unless field protection is designed first — that is CrmDataChange territory |
Deferred to Phase 22 |
| Industry classification | 24 | Not in the source export; would need its own source | Backlog |
Employment columns on alumni |
24 | alumni is Belmont-provided source data and stays clean; a joined table is the right shape for one-to-many anyway |
Won’t build |
Employment on any belmontalum.com / Cp:: surface |
24 | Held surface | Blocked on the portal resuming |
| Wiring Phase 22.4’s signup employment diff | 24 | This phase provides the data; Phase 22 consumes it. 22.4’s “alumni has no employment to diff against” premise is now false — see phase-24 §2 | In scope for Phase 22 |
Spec: phases/phase-25/. All deliberate scope boundaries from the July 29, 2026 planning interview.
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
alumni#show |
25 | Not selected in planning. One canonical signup (newest active) drives every derived value, so acting on stale data would take an alum with two signups where the older one is the fuller — rare enough not to earn a banner | ✅ Closed — Phase 28.1 (August 14, 2026). The premise is gone: an address names at most one active signup, so the “two signups, older one fuller” case it warned about cannot occur by email. The BUID case (two addresses, one alum) remains and is already banner’d on the signup record itself |
signup_version / signup_date / last_return_visit / last_enriched CSV columns |
25 | Not selected. The history high points land on the profile, where a human is reading one record; an export column would carry them for 49K rows nobody filters on | Backlog |
| Consent state + held-CRM-update count on the profile | 25 | Not selected. Both are already visible in Signup Admin, which is where the person acting on them works | Backlog |
| Free-text city / ZIP search | 25 | Out of scope — the location search surface is the district autocomplete, and Phase 25 made that see signup data. A second location input is a different feature | Backlog |
| Recomputing v1 interest spectra for list views | 25 | A ChampionQuizService call per row, 50× a search page and 49,000× an export, to populate a decorative chip. The profile page recomputes because it handles one record |
Won’t build as specced |
Writing signup affinities into alumni_affinities |
25 | The CRM is the system of record for confirmed groups. Signup-reported ones reach it through the crm_data_changes export and come back on a sync — writing them locally would manufacture CRM facts |
Won’t build — by design |
| Removals of signup-reported affinities | 22.4 / 25 | Unchanged from Phase 22.4: a form that omits a group is silence, not a departure | Backlog — by design |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| (None from 18.2 deliverables) | Phase 18.2 | All 18.2 deliverables shipped; remaining work is planned in 18.3-18.7 (not deferred) | Tracked in phase plan |
Swap remaining secondary CP partials to Alumni::EducationProfile (cp/profile_wizard/_step_confirm_education, cp/careers/_career_connect_cards, cp/leadership/{community,welcome_message,members}) |
Phase 18.4 | Out of scope for 18.4 (core profile/search paths only); these views still render via alumni.degrees and work correctly |
In scope for Phase 18.6 |
Migrate Api::AlumniController V1 serializer to call Alumni::EducationProfile directly (currently preserved via Entry#as_json contract) |
Phase 18.4 | Planned for 18.5 (API backwards compatibility phase) | Tracked in Phase 18.5 |
Rename AlumnusEngagementDecorator -> AlumniEngagementDecorator (alumni is singular and plural per coding standards) |
Phase 18.4 | Out of scope for read-path work; pure rename | Backlog |
V2 API enrichment block — optional ?include=educations (or /api/v2/) that exposes the full educations + areas_of_study structure for downstream apps wanting richer data than the 18.4 check-in blob |
Phase 18.5 | Out of scope for backwards-compatibility phase; V1 contract preserved with strictly-additive _source field |
Backlog — candidate for V2 |
Alumni search filter “Data source” (education / degree_fallback / none) with a matching column on results — exposes the legacy-fallback roster in the UI. Stopgap rake task alumni:legacy_fallback_buids ships in 18.5 |
Phase 18.5 | Belongs with UI rollout phase where alumni search/show pages are already being touched | Deferred to Phase 18.9 UI rollout |
Top Minors / Concentrations breakdown chart on stats page (concentration_level IN ('minor','concentration')) |
Phase 18.6 | Top Majors filtered to concentration_level = 'major' for parity with legacy; minor/concentration is purely additive |
Backlog — no phase target |
Remove side-by-side ?source=legacy\|education toggle and Coverage banner from /statistics and /engagement_stats |
Phase 18.6 | Toggle exists only to validate parity during 18.6/18.7; legacy view must stay live until exports cut over | ✅ Shipped in Phase 18.9 (Group B) |
Collapse aggregate_stats_v2:* cache namespace back into engagement_stats_* |
Phase 18.6 | Separate namespace during cutover so legacy and new caches don’t fight | ✅ Shipped in Phase 18.9 (Group B) |
Drop legacy degrees table and associated model/scope code |
Phase 18.6 | Read path fully decommissioned in Phase 18.9 (all reads via Education; major filter migrated to education_areas_of_study.major_code). degrees is no longer part of the site except the intentional EducationProfile→degrees fallback for alumni without educations rows, which goes inert once the production education import lands. Table retained for legacy/rollback only and CAN be dropped. colleges and majors are NOT removable — they remain active reference tables (colleges is the canonical college-code→label source; majors backs the major dropdown, community naming, and banner-import validation) |
degrees deferred to post-18.9 cleanup tag (coverage ≥ 99%); colleges/majors retained as reference |
Alumni.filter_by_major still reads legacy degrees (educations carry no major_code) |
Phase 18.9 | Resolved: education_areas_of_study.major_code now carries the CRM “Area of Study: External Id” (program code, valid for majors/minors/concentrations). filter_by_major matches on it via educations |
✅ Shipped in Phase 18.9 |
Drop denormalized enrollment fields from alumni (current_school_code, current_program_desc, intended_degree_code, expected_graduation_year) |
Phase 18.8 | Shipped as deploy 1 of 2 with read-path migration first; destructive schema drop intentionally postponed until post-import validation | Deferred to Phase 18.8 deploy 2 |
Remove Csv::AffinaquestContactImporter#clear_enrollment_fields_if_awarded! |
Phase 18.8 | Legacy cleanup deferred until deploy 2 when alumni denormalized enrollment columns are dropped | Deferred to Phase 18.8 deploy 2 |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
Full affinity browse/search UX on public signup (affinity_codes, lifestage_interest, vocation) modeled after /profile/wizard/affinities |
Phase 19.2 follow-up | Light interest capture (checkboxes + note) shipped in 19.3; full affinity dataset deferred to keep scope focused | Deferred to Phase 19.4 |
Full role interaction on public signup (/sign-up?step=role) including direct-pick and quiz outcomes |
Phase 19.2 follow-up | Stub page shipped for manual path testing (You → Belmont → Interests → Job → Role) before quiz/result work was restored |
✅ Done (Phase 19.3) |
| Final post-implementation revision pass before closing Phase 19 | Phase 19.5 wrap | 19.1-19.5 deliverables are complete, but final polish/revision scope will be confirmed after feedback | Pending decision: close as-is or define Phase 19.6 |
| ~3-day nudge email for signups who completed step 1 but none of the 3 optional sections | Phase 19.7 | Deferred (2026-06-25) until the delayed-job/queue story is confirmed; would also want a nudge_sent_at column. 19.7 ships only the immediate welcome email + profile link |
Backlog — revisit with delayed-job/scheduler decision |
| Manual 4-slider direct interest-rating path (alternative to the quiz on the Interests step) | Phase 19.7/19.8 | Raised during 19.7 planning; quiz-only ships first. Add if quiz drop-off warrants a faster self-rate path | Backlog — pending quiz drop-off data |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
Gate news/events/photos content in the weekly digest mailer (cp/notification_mailer/weekly_digest) by feature flag |
Phase 20.2 | Digests are a separate notifications concern; with content flags off at launch there’s little new content to surface, and any links resolve to the dashboard via the controller guards rather than erroring | Backlog — revisit if a flag stays off long-term |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Apple Sign In | Phase 1.3 | Focus on Google first | Backlog |
| Facebook OAuth | Phase 1.3 | Requires FB Developer App | Backlog |
| Microsoft / Entra sign-in | Phase 28.2 | Belmont is a Microsoft shop (confirmed Aug 14, 2026), so the 20 belmont.edu / bruins.belmont.edu signups can never match on a Google identity. Deferred because it buys them little: those addresses belong to current staff and recent grads who will click with a personal account anyway, and 28.2’s BUID fallback already resolves that to the right record. It would only matter for someone whose sole email identity is the university one. 28.2’s resolver makes it cheap when wanted — a config entry, a microsoft_uid column, one more branch |
Backlog — revisit if step-4 misses concentrate in that domain |
| SSO account linking | Phase 1.3 | Complexity (edge case handling) | ✅ Resolved for the signup surface — Phase 28.2. The edge cases that deferred it are enumerated as the four-way resolver. Row stays open for the Lookup/staff surface |
| — | ✅ Phase 1.9.1 |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Sync imported affinities to profile | Phase 1.4 | Requires BUID verification first | Backlog |
| Affinity export mechanism (cp_affinities → crm_data_changes) | Phase 1.4 | Was “awaiting Affinaquest export format” — but that blocked on the held portal’s cp_affinities. The Lookup-side alumni_affinities table (unique on buid + affinity_code) makes signup-reported orgs diffable today |
✅ Resolved — Phase 22.4 (July 28, 2026). ChampionSignupCrmLogger logs additions under source_table: "affinities"; they export with everything else at /settings/affinaquest/crm_changes |
| Track affinity adds/removes for Advancement Services | Phase 1.4 | Same unblock. Adds only — absence from a signup form is not an assertion that someone left an organization, so removals stay deferred by design | ✅ Adds resolved — Phase 22.4 (July 28, 2026). Removals remain Backlog by design, not by omission — a form that omits a group is silence, not a departure |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| @Mentions in discussions | Phase 3 | Complexity (name resolution, notifications) | Backlog |
| Discussion board search | Phase 3 | Full-text search across posts/comments | Backlog |
| Discussion bookmarks | Phase 3 | Save posts for later reading | Backlog |
| Mute discussion threads | Phase 3 | Stop notifications for specific threads | Backlog |
| Additional sort options (Top, Controversial) | Phase 3 | Beyond MVP needs | Backlog |
| Multiple images per post | Phase 3 | Currently single image | Backlog |
| User temp bans | Phase 3 | 24hr/7day/permanent bans by CL | If needed |
| Advanced discussion analytics | Phase 3 | Exportable reports beyond basic metrics | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Profile wizard header alignment | Phase 4 | Low priority — vertical rhythm on step 3 | Backlog |
| Message reactions sizing | Phase 4 | Minor polish; already functional | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Almost Alumni BUID verification | Phase 5.2 | Students have BUID but no degree records | Discussion |
| Degree import hook (Almost Alumni → Alumni upgrade) | Phase 5.2 | Automatic upgrade when degree recorded | Pre-MVP |
| Almost Alumni upgrade email notification | Phase 5.2 | Notify when upgraded from Student to Alumni | Pre-MVP |
| Admin “Recheck Education” action | Phase 5.2 | Staff action to manually trigger upgrade | Pre-MVP |
19.1-19.5 implementation is complete. Final phase closeout is intentionally pending a short revision pass and may be tracked as 19.6 if scope exceeds minor copy/polish.
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Conversational connect scripts | Phase 10 | Pre-written message templates | Backlog |
| Connection notes tracker | Phase 10 | Private notes on connections | Backlog |
| Connection expiration / dormancy | Phase 10 | Auto-flag inactive connections | Backlog |
| Proactive match suggestions | Phase 10 | System-suggested connections | Backlog |
| Delayed / queued sends | Phase 10 | Schedule messages for later | Backlog |
| Connection-based recommendation changes | Phase 10 | Adjust algorithm based on connection graph | Backlog |
| Group connections | Phase 10 | Connect with multiple alumni at once | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Activity-gap personalization | Phase 11 | Prioritize ideas based on activity gaps | Backlog |
| Success metrics dashboard (role ideas) | Phase 11 | Admin dashboard with role adoption metrics | Backlog |
| Multi-role support | Phase 11 | Allow multiple Champion roles; currently single-role | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Generosity-as-formation language (F-03) | Phase 12 | Add giving framing to contribution touchpoints | Backlog |
| Source guardrail phrases (F-04) | Phase 12 | Incorporate prescribed vocabulary more broadly | Backlog |
| i18n infrastructure (F-06) | Phase 12 | Extract user-facing strings to i18n YAML | Backlog |
| Roles woven into experience (F-01) | Phase 12 | Weave roles into dashboard, empty states, notifications | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Photo album submissions | Phase 14 | News/events done first; albums deferred | Backlog |
| Rich text submission editor | Phase 14 | Plain text + image for MVP | Backlog |
| Champion self-edit after submission | Phase 14 | Must ask staff via thread currently | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| CSP enforcement mode | Phase 16.5 | Deployed in report-only mode first; switch to enforcement after log monitoring confirms no violations. 27.6 made this cheaper by removing fonts.googleapis.com and fonts.gstatic.com from the alum-facing surface — and identified the remaining blocker: cdn.jsdelivr.net and cdnjs.cloudflare.com are still in script_src because five JS modules are pinned there. See the jsDelivr row under Phase 27 deferrals |
Backlog — bundle with vendoring the jsDelivr pins |
| GDPR rights management | Phase 16 | No EU users in beta audience; deletion/export infrastructure is in place | Backlog (If EU expansion) |
| Phase 16 | ✅ Resolved by Phase 27 Decision E, implemented in 27.3/27.6 — not deferred, decided. The old trigger (“if tracking added”) was the wrong condition and would have read as pre-approval for a case it never considered: 27.3’s 6-month remembered-device cookie is not tracking, so it never trips that trigger, yet it is exactly the non-strictly-necessary cookie the deferral was implicitly guarding against. Consent is collected at the point of use instead — an unticked “Keep me signed in on this device for 6 months” checkbox on /sign-in. Still no banner, because there is no banner-worthy category: no analytics, no pixels, no ad tech, no sale or sharing. A banner on a conversion funnel is a tax, and a badly-built one is its own liability |
Won’t build — revisit only if an actual analytics or advertising cookie is ever added, which would be a new decision, not this one |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Compact mobile hero (condensed height for returning users) | Phase 13.2 | Low priority polish | Backlog |
| ✅ Delivered in Phase 13.8 (What’s New summary bar below hero) | ✅ Complete | ||
| Mobile sidebar fold (2 items above fold, rest collapsed) | Phase 13.2 | UX polish; desktop parity first | Backlog |
| Card standardization (unified card component across tiers) | Phase 13.2 | Refactor; no new functionality | Backlog |
| Icon color variation per section type | Phase 13.2 | Visual polish | Backlog |
| ✅ Delivered in Phase 13.3 (ActivityFeedService with scoring engine) | ✅ Complete | ||
| ✅ Delivered in Phase 13.7 (Champion Nudge Engine + Role Microsite) | ✅ Complete | ||
| Personalization (activity-gap based content ordering) | Phase 13.5 | Requires activity history | Backlog |
| Stage transition animations & coach marks | Phase 13.6 | Polish layer | Backlog |
Champion Dashboard Layout (separate _stages/champion.html.erb) |
Phase 13.5.1 | Existing champion/active_champion stage partials sufficient | Backlog |
| “Your Impact” sidebar widget (connection/community/post counts) | Phase 13.5.2 | Nice-to-have; can add in future iteration | Backlog |
| Dismissable contextual prompts (seen_tooltips JSONB pattern) | Phase 13.5.3 | Not needed for initial launch | Backlog |
coaching_in_digest notification preference toggle |
Phase 13.5.3b | Always shows for Tier 2+; add opt-out later if requested | Backlog |
| Champion scoring boost in directory/recommendations | Phase 13.5.4 | Requires tuning; defer to avoid unintended ranking effects | Backlog |
| ✅ Delivered in Phase 13.7 (dashboard nudge card on engaged_member stage) | ✅ Complete | ||
| Bulk approve/decline | Phase 14 | One at a time currently | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Video career resources | Phase 7 | PDF-first; add video if demand emerges | Backlog |
| OCPD content CMS | Phase 7 | Hardcoded section sufficient; content changes rarely | Backlog |
| Resource ratings/feedback | Phase 7 | Gauge demand first | Backlog |
| In-app PDF viewer | Phase 7 | Browser handles PDF viewing | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| — | ✅ Implemented (Phase 8.6) — VAPID web push for staff (Lookup Portal) and Champions | ||
| Onboarding email drip series | Phase 9.6 | Portal needs more users before optimizing activation flow | Backlog (High) |
| SMS/phone platform (Twilio) | Phase 1.9 | Cost/complexity for beta | Backlog |
| Item | Original Phase | Reason Deferred | Status |
|---|---|---|---|
| Seeded question daily scheduler job | Phase 9.2 | Auto-publish on schedule | Backlog |
| Seeded question A/B testing | Phase 9.2/9.3 | Test variants for engagement | Backlog |
| Seeded question response analytics | Phase 9.3 | Engagement metrics per question | Backlog |
Rationale for deferral: Build up the user base first — analytics are more valuable once there is meaningful activity to measure. Phase 9.4 covers onboarding metrics. Full spec in phases/phase-6/README.md.
| Item | Original Phase | Description | Priority |
|---|---|---|---|
| Activity Metrics Dashboard (6.1) | Phase 6 | Staff dashboard: total Champions, active users, growth trends, event/story submission counts, time period filters | Medium |
| Region Health Dashboard (6.2) | Phase 6 | Per-region health scores (active %, events, posts, CLC assigned), color-coded table with drill-down | Medium |
| Reports & Exports (6.3) | Phase 6 | Monthly summary, region detail, CLC performance report templates; PDF/CSV export; scheduled CRM export | Medium |
cp_profile_changes TableAdded: December 9, 2025
Context: The cp_profile_changes table was created in Phase 1.1 as a changelog for CRM exports. It has been superseded by the unified crm_data_changes table.
Current Status:
crm_data_changes insteadCp::ProfileChange model exists but unusedMigration Plan:
crm_data_changes table (Affinaquest Phase 1)crm_data_changescp_profile_changes tableCp::ProfileChange modelTarget: Next cleanup sprint
Added: January 14, 2026
Idea: Built-in RSVP system for Alumni Network events, rather than relying on external RSVP links.
Features:
Why Deferred: Current system uses external RSVP links to simplify MVP. Built-in RSVP adds complexity around capacity management, notifications, and cancellations.
Priority: Medium — valuable for engagement tracking but external links work for now
Implemented: Phase 8 (Notifications & Digests) — Bell icon, unread badge, notification dropdown, per-type preferences, daily/weekly email digests.
Implemented: Phase 8 (Notifications & Digests) — New members, new events, discussion activity notifications with per-type preferences and digest scheduling.
Added: December 8, 2025
Idea: For verified alumni (those with BUID linked), show their CRM-imported affinities from alumni_affinities and allow bulk add to their cp_affinities profile.
Flow:
alumni_affinities for their BUIDcp_affinitiesPriority: Medium — nice engagement boost, but self-selection works fine for MVP
Added: January 2026 (Phase 5.2 implementation)
Context: Current students may exist in the Alumni table with a BUID but have no degree records.
Current Behavior:
help_find_you stepFuture Enhancement Options:
Added: December 23, 2025 (Phase 1.9 planning). Deferred: Cost/complexity for beta.
Future Value (High):
Implementation Notes:
Filed July 30, 2026, while building the shared layer. Each was found in the tree and deliberately left alone — see 23.3-shared-layer.md §13.
AlumniHelperDeferred from: 23.3 §7.3 — inventory only.
Nine methods in root-level app/helpers/alumni_helper.rb are signup-domain
presentation, not Lookup presentation: champion_signup_lean_key/name/classes/icon,
prospect_champion_seal, prospect_champion_seal_image, has_completed_champion_signup?,
alumni_champion_champion?, affinity_chip_classes. Under Decision 6, root-level
reads as Alumni Lookup, so the file’s location asserts something false.
Why it stayed: nothing is broken, all callers work, and include_all_helpers
means location has no functional effect. Moving nine methods with their tests is
real churn for a documentation-shaped benefit. A header now names them instead.
Do it when: something else already touches this file, or Signup Admin gains its own lean rendering (at which point the shared home stops being cosmetic).
Deferred from: 23.3 §6.4 — no caller, so creating it would be an orphan (CLAUDE.md rule 11).
state appears on zip_codes, alumni, cp_champions, and champion_signups,
and there is no canonical list anywhere. Nothing is broken today. It is filed
because it is the next industry: the first app to need a state dropdown will
hardcode one, and the second will hardcode a different one.
Decision, so the second app finds one instead of a blank page: it belongs in
the shared layer as UsState::ALL, alongside Industry. Note the width trap
recorded in IdentityField::WIDTH_CONSTRAINTS — cp_champions.state is
limit: 2 while the other two are unbounded, so the canonical form is the
two-letter code and the full name is display only.
Cp::Champion#normalize_names makes pref_first_name unusableFound: 23.3 §6.3, while building IdentityField. Held-surface behavior
change — out of 23.3’s scope (§13).
Cp::Champion’s before_save :normalize_names assigns
pref_first_name = first_name on every save where first_name is present. So:
cp_champions.pref_first_name is a mirror of first_name, not a preferred namepref_first_name rows in cp_profile_changes are shadows of first_name edits, carrying no independent signalchampion_signups is not the only source missing a preferred-name field — cp_champions effectively has none eitherThe comment calls the column “legacy but still used by some display methods,”
which is true and is also how it survived. Recorded as
IdentityField::PREF_NAME_IS_MIRRORED and pinned by a test, so fixing it fails
loudly and prompts updating the map.
Belongs to Phase 26 — it is a write-policy question, not a namespacing one.
Cp::ChampionRolesHelper#champion_icon_svg is a third copy of the markFound: 23.3 §5, while moving the artwork.
23.3’s spec listed this method as a render site to update. It is not — it holds a
hardcoded inline <svg> string rather than rendering the shared partial, and it
is a different mark (the hexagonal Alumni Champion icon, fill #b21029) from
what shared/champion_interest/_icon renders via ApplicationHelper#role_icon_svg.
So the artwork has three homes, two of which are now shared and one of which is a string literal inside a held helper. Consolidating it is a held-surface change with a visible result, which puts it outside a pure-refactor sub-phase.
cp_communities.industry is still unvalidatedFound: 23.3 §6.1. ChampionSignup now validates industry against
Industry::ALL and Cp::Champion already did, but Cp::Community does not —
the third table holding the taxonomy (§4.2). Adding the validation is a one-line
held write; it was not in the approved scope for 23.3.
Before starting any sub-phase, the Sub-Phase Planning Checkpoint (see CLAUDE.md) includes a backlog review:
Last updated: March 2026