Skip to content

Canonical degree vocabulary + user major API - #1187

Open
ritamzico wants to merge 4 commits into
mainfrom
ritam/major-vocabulary-plumbing
Open

Canonical degree vocabulary + user major API#1187
ritamzico wants to merge 4 commits into
mainfrom
ritam/major-vocabulary-plumbing

Conversation

@ritamzico

Copy link
Copy Markdown
Contributor

Invisible plumbing for a user-level declared major. No UI in this PR.

Major information today lives only on the GradTrak plan document, so it misses everyone who only uses Catalog / Grades / Scheduler. user.majors / user.minors have existed on the schema and in the GraphQL API since forever, but nothing read or wrote them — the frontend documents didn't select them. This wires them through so features outside GradTrak can eventually use them.

What's in here

The degree vocabulary is now single-source in packages/common. degree-programs-types.json existed as three byte-identical copies; only two were imported, and the MajorSearch/ one was already orphaned. All three are deleted, replaced by packages/common/src/lib/degreePrograms.ts — a plain .ts module rather than a JSON import, so the backend can use it without depending on resolveJsonModule being set consistently across workspaces. The file was generated by script from the existing JSON, not retyped.

majors / minors are now selected by the frontend user documents — added to READ_USER, UPDATE_USER, and IUserInput. The backend already accepted them in UpdateUserInput and already returned them from formatUser, so no schema change was needed.

updateUser now validates them. It previously spread input straight into findByIdAndUpdate with no validation, so any authenticated user could write arbitrary strings to majors. Nothing sends them today, which is why it went unnoticed — but once targeting reads the field, unvalidated values are both a silent-mismatch bug and a junk-data vector.

apps/backend/src/bootstrap/graphql/generated/persistedOperations.ts and previousPersistedOperations.ts are regenerated, since the GraphQL document changes made the allowlist stale.

⚠️ Behavior change worth flagging

updateUser now rejects major/minor values outside the canonical 117/113-entry lists. Nothing currently sends such values, so there's no practical impact, but it is a real change to the API contract.

This is deliberately strict, and revisitable: the read-only data check in the stacked backfill PR has not been run yet, so we don't know whether any existing plan.majors strings fall outside the vocabulary. If they do, strict rejection could block a user from re-saving a legitimate pre-existing value once the picker ships. Happy to switch to normalizing, or to extending the vocabulary, once we have those numbers.

Verification

The degree list is unchanged — verified against HEAD~1's JSON three ways:

  • Source-level: all six arrays identical in strings and order (all 172, majors 117, minors 113, certificates 9, simultaneousDegrees 8, summerMinors 9). VALID_MAJORS.size == 117 and VALID_MINORS.size == 113 — equal to the array lengths, so no duplicates collapsed into the sets.
  • Rendered in a browser against a full local docker stack, both changed import sites: GradTrak onboarding Add Majors (117) / Add Minors (113), and Dashboard → Edit Plan → Major(s) (117) / Minor(s) (113). Identical strings in identical order, no console errors.

updateUser exercised against a real Mongo, 8/8:

  • valid values accepted; bogus major and bogus minor both rejected with the offending value named
  • a minor-only value is rejected as a major (the two vocabularies are enforced separately)
  • a rejected write leaves the stored document untouched
  • an update omitting majors/minors preserves them; an explicit [] clears them
  • all 117 majors and 113 minors from the picker list are accepted — strict rejection cannot block anything the UI can produce

Also: npm test --workspace=backend 8/8 (includes the persisted-operation allowlist tests), npm run check:operations clean, type-check and lint clean on backend and frontend, frontend production build succeeds. grep -rn "degree-programs-types" apps packages returns nothing.

Not in this PR

The Profile/Account picker UI, the backfill migration seeding user.majors from plan.majors (stacked follow-up, which also adds the unique index on plan.userEmail), career interest tags, and any profile subdocument restructuring. plan.majors is untouched — GradTrak works exactly as before.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjugtfWTM7g5oef3CV245z

Move the three byte-identical copies of degree-programs-types.json into a
single typed module in packages/common, and point the two GradTrak pickers
at it. The MajorSearch copy was already orphaned.

user.majors / user.minors already existed on the schema and in the GraphQL
API but were dormant — nothing selected them. Add them to READ_USER,
UPDATE_USER and IUserInput.

updateUser now rejects major/minor values outside the canonical list.
Nothing sends such values today, so there is no practical impact, but it
is a change to the API contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjugtfWTM7g5oef3CV245z
@ritamzico
ritamzico added this pull request to stack #1205 September 9, 2026 21:28
…y-plumbing

# Conflicts:
#	apps/backend/src/bootstrap/graphql/generated/persistedOperations.ts
@NathanDai5287

Copy link
Copy Markdown
Contributor

@copilot i have no context on this change. explain this PR like i am nontechnical and have no context. keep it very concise

Copilot AI commented Sep 10, 2026

Copy link
Copy Markdown

@copilot i have no context on this change. explain this PR like i am nontechnical and have no context. keep it very concise

This PR quietly adds one missing profile detail: a student’s major/minor, so Berkeleytime can use it across the app later (not just in GradTrak). It also makes sure those values come from one official list, so data stays consistent and clean. No visible UI changes in this PR.

@NathanDai5287

Copy link
Copy Markdown
Contributor

@copilot how to review?

Copilot AI commented Sep 10, 2026

Copy link
Copy Markdown

@copilot how to review?

Quick review checklist:

  1. Confirm no UI behavior changed (this PR is backend/data plumbing only).
  2. Verify majors/minors now round-trip in user read/update GraphQL docs.
  3. Focus on validation strictness in updateUser (only canonical major/minor values accepted) and whether that API contract change is acceptable.

) => {
for (const value of values) {
if (!vocabulary.has(value)) {
throw new Error(`Invalid ${field}: "${value}"`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets change this to a GraphQL BAD_USER_INPUT error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants