Skip to content

#352 Validate Answer Values Against Question Options And Add Length Limits - #433

Merged
b-at-neu merged 3 commits into
devfrom
352-validate-answer-values-against-question-options
Aug 15, 2026
Merged

#352 Validate Answer Values Against Question Options And Add Length Limits#433
b-at-neu merged 3 commits into
devfrom
352-validate-answer-values-against-question-options

Conversation

@cielbellerose

Copy link
Copy Markdown
Collaborator

Closes #352

Summary

  • Answer values were never validated server-side beyond "an array of strings" — no membership check against a question's options, no "exactly one value" rule for single-value question types, and no length limits anywhere.
  • Adds one shared pure validator, getAnswerValueError(question, value) (lib/constants.ts), that both answer-writing server actions call as the real backstop, and both applicant editors call inline so the client shows the exact same message before ever hitting the network.
  • Adds option-count/length caps to the existing validateOptions refinement so an admin can't author a choice that's unselectable by the new answer-side limits.

Changes

  • lib/constants.ts — new constants (ANSWER_SHORT_MAX_LENGTH, ANSWER_LONG_MAX_LENGTH, ANSWER_OTHER_MAX_LENGTH, QUESTION_OPTION_MAX_LENGTH, QUESTION_MAX_OPTIONS, ANSWER_MAX_VALUES); getAnswerValueError() — exhaustive switch over QuestionType enforcing membership, value-count, and length rules; two new checks added to validateOptions (option count ≤ QUESTION_MAX_OPTIONS, each option ≤ QUESTION_OPTION_MAX_LENGTH).
  • prisma/actions/applications.ts (createOrUpdateApplicationAnswer) — envelope schema capped with a pre-DB size guard (.max(ANSWER_LONG_MAX_LENGTH) per value, .max(ANSWER_MAX_VALUES) on the array); question select widened to options/allowOther; calls the validator before any upsert.
  • prisma/actions/profile.ts (updateGlobalAnswer) — same three changes.
  • components/features/application-stepper.tsx — the Controller's rules.validate also runs the validator so the inline message matches the server's.
  • components/features/application-question.tsxmaxLength on the short/long/"Other" inputs, a live counter for long_answer, autosave blocked client-side when the value is invalid, and the failure toast now surfaces the action's own { error } message instead of always the generic copy.
  • components/features/profile-question.tsx — same treatment; generalized formatErrorvalidationError to cover every answer-validation message, not just format mismatches.
  • components/ui/options-chip-editor.tsxmaxLength on the option input and addOption refuses past QUESTION_MAX_OPTIONS, so the admin can't type into a rejected state.

Deliberately a write-path-only change: no render path calls the new validator, so a stored value that no longer fits its question (e.g. an option an admin later deleted) still renders exactly as before — that mismatch UX is #354's job.

Testing plan

  • Choice membership — start a draft, answer a single-choice question, then (as admin, second tab) delete that option. Re-pick the now-stale radio in the first tab: toast reads "That choice is no longer available. Refresh the page and answer again.", and reloading shows the answer unchanged.
  • Multiple choice — select several options and save; confirm they persist. With allowOther on, add an "Other" text and confirm it saves alongside the options.
  • Duplicate "Other" text — with an option checked, type that same option's text into "Other": rejected with "That answer is already one of the choices — select it from the list instead."
  • Single-value types — for short_answer/long_answer/single_choice, confirm normal answering and clearing (empty) both still save.
  • Length caps — paste 600 characters into a short answer: it truncates at 500 and saves. Paste 6,000 into a long answer: truncates at 5,000, counter turns red at the cap. Paste 300 into an "Other" box: caps at 200.
  • Server backstop — invoke createOrUpdateApplicationAnswer from the browser console with a two-element value on a short_answer question, and with a choice value not in options: both come back { error: … } and nothing is written.
  • Profile parity — repeat the membership, length, and clearing checks on /profile; messages are identical to the application flow.
  • Legacy values still render — hand-edit a stored answer to exceed a cap (or reference a since-removed option), reload the editor: it displays, focusing/blurring without editing writes nothing, and the reviewer's application detail view is unchanged.
  • Admin options — try adding a 51st option and a 250-character option in both the global-question dialog and the position question form; both are refused (chip input silently stops at 50/refuses the 250-char text past 200 chars; a saved-but-oversized legacy question surfaces the FormMessage under Options on next edit).
  • File upload unaffected — upload and remove a file answer on a file_upload question in both the profile and application flow.
  • Auth/roles — attempt createOrUpdateApplicationAnswer/updateGlobalAnswer against another user's application/profile from the console; both remain scoped to the caller (unchanged requireOwnership/user.id scoping).
  • Both themes at 375px and 1280px; keyboard-only pass over one question of each type.

Automated checks

  • npm run prettier:check
  • npm run eslint:check
  • npm run tsc:check

Notes

  • Overlaps Preserve Stored Answers When a Question Type or Options Change #354 and Derive Answer Question Labels Server-Side and Verify Question Ownership #351 in applications.ts, profile.ts, application-question.tsx, profile-question.tsx — kept deliberately disjoint per the plan: this PR adds one validator call on the write path and touches no render/partition logic.
  • The option caps apply on question write, so pre-existing questions above them keep working until an admin next edits one, at which point they must trim to save — called out as a small admin-facing behavior change beyond the ticket's letter.
  • ANSWER_MAX_VALUES is a pre-DB size guard, not a product rule — membership + uniqueness already bound a real multiple_choice answer to options.length + 1.

@cielbellerose cielbellerose self-assigned this Aug 13, 2026
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aplio Ready Ready Preview Aug 15, 2026 4:44pm

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 13, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 1 · needs revision

2 open — 2 🟠 Medium (see inline)

⚠️ Vercel deployment red — Neon branch quota, not a review finding. See .claude/docs/PIPELINE.md → Preview-database concurrency.

Comment thread components/features/application-question.tsx
Comment thread components/features/profile-question.tsx
@cielbellerose cielbellerose added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Aug 13, 2026
@cielbellerose

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-M1, R1-M2 · 980f464

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Aug 13, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 1 · approved

0 open — clean

⚠️ Vercel deployment red — Neon branch quota, not a review finding. See .claude/docs/PIPELINE.md → Preview-database concurrency.

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 13, 2026
@b-at-neu b-at-neu assigned b-at-neu and unassigned cielbellerose Aug 14, 2026
@b-at-neu b-at-neu added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed approved Review passed, ready to merge needs revision Review found issues that need fixing labels Aug 14, 2026
@b-at-neu
b-at-neu force-pushed the 352-validate-answer-values-against-question-options branch from 980f464 to 4bd58d3 Compare August 14, 2026 03:16
@b-at-neu

Copy link
Copy Markdown
Collaborator

Revision — human-directed rebase

rebase: lib/constants.ts (comment merge — dropped stale multi-line comment dev already condensed), lib/utils.ts (comment merge — kept new ActionError JSDoc, dev's condensed isBypassAllowed JSDoc), prisma/actions/applications.ts (comment merge — kept new validation call, dev's condensed trim comment), prisma/actions/profile.ts (same), components/features/application-question.tsx (comment merge — kept dev's condensed autosave comment), components/features/profile-question.tsx (same) · 4bd58d3

All conflicts were dev's PR #418 (#418 tighten comment discipline) condensing comments in spots this PR's commits also touched — no logic conflicts. Resolved by keeping this PR's new code/exports and applying the current one-line comment bar to any comment landing in the merged result. prettier:check/eslint:check/tsc:check all pass post-rebase.

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Aug 14, 2026
b-at-neu
b-at-neu previously approved these changes Aug 14, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review — Cycle 4 · approved

0 open — clean

@b-at-neu b-at-neu added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 14, 2026
@b-at-neu b-at-neu assigned cielbellerose and unassigned b-at-neu Aug 14, 2026
@b-at-neu b-at-neu added needs revision Review found issues that need fixing and removed approved Review passed, ready to merge labels Aug 15, 2026
@b-at-neu b-at-neu assigned b-at-neu and unassigned cielbellerose Aug 15, 2026
@b-at-neu b-at-neu added revising Revise agent working (in-flight) and removed needs revision Review found issues that need fixing labels Aug 15, 2026
cielbellerose and others added 3 commits August 15, 2026 12:38
Adds a shared getAnswerValueError validator (lib/constants.ts) enforcing
choice membership, single-vs-multi value counts, and per-value/array
length caps. Both answer-writing actions call it as the real backstop;
both applicant editors call it inline so the client shows the same
message before round-tripping to the server. Option count/length are
capped via the existing validateOptions refinement so an admin can't
author a choice that's unselectable by the new answer-side limits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Distinguish a re-thrown { error } from a genuine unexpected throw so
autosave catches never leak an authorization guard's raw message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Condenses comments added by this PR to the one-line default from
ENGINEERING.md §7 (post #421/#418) and drops issue/section provenance
refs; no behavior change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@b-at-neu
b-at-neu force-pushed the 352-validate-answer-values-against-question-options branch from 6926b61 to f7602f8 Compare August 15, 2026 16:43
@b-at-neu

Copy link
Copy Markdown
Collaborator

Revision — Cycle 4

rebase-only cycle (already approved, clean) · onto origin/dev · f7602f8
· rebase: prisma/actions/applications.ts (orthogonal imports — dev's APPLICANT_EDITABLE_APPLICATION_STATUSES alongside this PR's ANSWER_LONG_MAX_LENGTH/ANSWER_MAX_VALUES)
· rebase: components/features/application-question.tsx (layered maxLength/aria-invalid/counter onto dev's mismatch-notice fitted/orphaned rendering; dropped the now-dead refusal-return onSave path superseded by the ActionError-throw design)
· rebase: components/features/application-stepper.tsx (kept dev's isGlobal-gated validate/orphaned-detection structure, layered in getAnswerValueError; onSave now throws ActionError per the R1-M1 fix)
· rebase: components/features/profile-question.tsx (merged validationError/noticeId/labelId state, layered maxLength/aria-invalid/counter onto dev's mismatch-notice rendering)

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Aug 15, 2026
@b-at-neu b-at-neu added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 15, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review — Cycle 5 · approved

0 open — clean

@b-at-neu b-at-neu added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 15, 2026
@b-at-neu b-at-neu assigned cielbellerose and unassigned b-at-neu Aug 15, 2026
@b-at-neu
b-at-neu merged commit 0d5432b into dev Aug 15, 2026
7 checks passed
@b-at-neu
b-at-neu deleted the 352-validate-answer-values-against-question-options branch August 15, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Review passed, ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Validate Answer Values Against Question Options and Add Length Limits

2 participants