fix(worksheets): add borrowNotation and borrowingHints to validation fallback

Add missing subtraction-specific scaffold fields to the fallback displayRules
object in validation.ts to ensure all DisplayRules fields have defaults.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-08 14:55:31 -06:00
parent 5a8fc5735d
commit 3f700af643
1 changed files with 66 additions and 56 deletions

View File

@ -1,98 +1,105 @@
// Validation logic for worksheet configuration // Validation logic for worksheet configuration
import type { WorksheetFormState, WorksheetConfig, ValidationResult } from './types' import type {
import type { DisplayRules } from './displayRules' WorksheetFormState,
WorksheetConfig,
ValidationResult,
} from "./types";
import type { DisplayRules } from "./displayRules";
/** /**
* Get current date formatted as "Month Day, Year" * Get current date formatted as "Month Day, Year"
*/ */
function getDefaultDate(): string { function getDefaultDate(): string {
const now = new Date() const now = new Date();
return now.toLocaleDateString('en-US', { return now.toLocaleDateString("en-US", {
month: 'long', month: "long",
day: 'numeric', day: "numeric",
year: 'numeric', year: "numeric",
}) });
} }
/** /**
* Validate and create complete config from partial form state * Validate and create complete config from partial form state
*/ */
export function validateWorksheetConfig(formState: WorksheetFormState): ValidationResult { export function validateWorksheetConfig(
const errors: string[] = [] formState: WorksheetFormState,
): ValidationResult {
const errors: string[] = [];
// Validate total (must be positive, reasonable limit) // Validate total (must be positive, reasonable limit)
const total = formState.total ?? 20 const total = formState.total ?? 20;
if (total < 1 || total > 100) { if (total < 1 || total > 100) {
errors.push('Total problems must be between 1 and 100') errors.push("Total problems must be between 1 and 100");
} }
// Validate cols and auto-calculate rows // Validate cols and auto-calculate rows
const cols = formState.cols ?? 4 const cols = formState.cols ?? 4;
if (cols < 1 || cols > 10) { if (cols < 1 || cols > 10) {
errors.push('Columns must be between 1 and 10') errors.push("Columns must be between 1 and 10");
} }
// Auto-calculate rows to fit all problems // Auto-calculate rows to fit all problems
const rows = Math.ceil(total / cols) const rows = Math.ceil(total / cols);
// Validate probabilities (0-1 range) // Validate probabilities (0-1 range)
const pAnyStart = formState.pAnyStart ?? 0.75 const pAnyStart = formState.pAnyStart ?? 0.75;
const pAllStart = formState.pAllStart ?? 0.25 const pAllStart = formState.pAllStart ?? 0.25;
if (pAnyStart < 0 || pAnyStart > 1) { if (pAnyStart < 0 || pAnyStart > 1) {
errors.push('pAnyStart must be between 0 and 1') errors.push("pAnyStart must be between 0 and 1");
} }
if (pAllStart < 0 || pAllStart > 1) { if (pAllStart < 0 || pAllStart > 1) {
errors.push('pAllStart must be between 0 and 1') errors.push("pAllStart must be between 0 and 1");
} }
if (pAllStart > pAnyStart) { if (pAllStart > pAnyStart) {
errors.push('pAllStart cannot be greater than pAnyStart') errors.push("pAllStart cannot be greater than pAnyStart");
} }
// Validate fontSize // Validate fontSize
const fontSize = formState.fontSize ?? 16 const fontSize = formState.fontSize ?? 16;
if (fontSize < 8 || fontSize > 32) { if (fontSize < 8 || fontSize > 32) {
errors.push('Font size must be between 8 and 32') errors.push("Font size must be between 8 and 32");
} }
// V4: Validate digitRange (min and max must be 1-5, min <= max) // V4: Validate digitRange (min and max must be 1-5, min <= max)
// Note: Same range applies to both addition and subtraction // Note: Same range applies to both addition and subtraction
const digitRange = formState.digitRange ?? { min: 2, max: 2 } const digitRange = formState.digitRange ?? { min: 2, max: 2 };
if (!digitRange.min || digitRange.min < 1 || digitRange.min > 5) { if (!digitRange.min || digitRange.min < 1 || digitRange.min > 5) {
errors.push('Digit range min must be between 1 and 5') errors.push("Digit range min must be between 1 and 5");
} }
if (!digitRange.max || digitRange.max < 1 || digitRange.max > 5) { if (!digitRange.max || digitRange.max < 1 || digitRange.max > 5) {
errors.push('Digit range max must be between 1 and 5') errors.push("Digit range max must be between 1 and 5");
} }
if (digitRange.min > digitRange.max) { if (digitRange.min > digitRange.max) {
errors.push('Digit range min cannot be greater than max') errors.push("Digit range min cannot be greater than max");
} }
// V4: Validate operator (addition, subtraction, or mixed) // V4: Validate operator (addition, subtraction, or mixed)
const operator = formState.operator ?? 'addition' const operator = formState.operator ?? "addition";
if (!['addition', 'subtraction', 'mixed'].includes(operator)) { if (!["addition", "subtraction", "mixed"].includes(operator)) {
errors.push('Operator must be "addition", "subtraction", or "mixed"') errors.push('Operator must be "addition", "subtraction", or "mixed"');
} }
// Validate seed (must be positive integer) // Validate seed (must be positive integer)
const seed = formState.seed ?? Date.now() % 2147483647 const seed = formState.seed ?? Date.now() % 2147483647;
if (!Number.isInteger(seed) || seed < 0) { if (!Number.isInteger(seed) || seed < 0) {
errors.push('Seed must be a non-negative integer') errors.push("Seed must be a non-negative integer");
} }
if (errors.length > 0) { if (errors.length > 0) {
return { isValid: false, errors } return { isValid: false, errors };
} }
// Determine orientation based on columns (portrait = 2-3 cols, landscape = 4-5 cols) // Determine orientation based on columns (portrait = 2-3 cols, landscape = 4-5 cols)
const orientation = formState.orientation || (cols <= 3 ? 'portrait' : 'landscape') const orientation =
formState.orientation || (cols <= 3 ? "portrait" : "landscape");
// Get primary state values // Get primary state values
const problemsPerPage = formState.problemsPerPage ?? total const problemsPerPage = formState.problemsPerPage ?? total;
const pages = formState.pages ?? 1 const pages = formState.pages ?? 1;
// Determine mode (default to 'smart' if not specified) // Determine mode (default to 'smart' if not specified)
const mode = formState.mode ?? 'smart' const mode = formState.mode ?? "smart";
// Shared fields for both modes // Shared fields for both modes
const sharedFields = { const sharedFields = {
@ -107,7 +114,7 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
rows, rows,
// Other fields // Other fields
name: formState.name?.trim() || 'Student', name: formState.name?.trim() || "Student",
date: formState.date?.trim() || getDefaultDate(), date: formState.date?.trim() || getDefaultDate(),
pAnyStart, pAnyStart,
pAllStart, pAllStart,
@ -117,12 +124,12 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
digitRange, digitRange,
// V4: Operator selection (addition, subtraction, or mixed) // V4: Operator selection (addition, subtraction, or mixed)
operator: formState.operator ?? 'addition', operator: formState.operator ?? "addition",
// Layout // Layout
page: { page: {
wIn: orientation === 'portrait' ? 8.5 : 11, wIn: orientation === "portrait" ? 8.5 : 11,
hIn: orientation === 'portrait' ? 11 : 8.5, hIn: orientation === "portrait" ? 11 : 8.5,
}, },
margins: { margins: {
left: 0.6, left: 0.6,
@ -133,34 +140,37 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
fontSize, fontSize,
seed, seed,
} };
// Build mode-specific config // Build mode-specific config
let config: WorksheetConfig let config: WorksheetConfig;
if (mode === 'smart') { if (mode === "smart") {
// Smart mode: Use displayRules for conditional scaffolding // Smart mode: Use displayRules for conditional scaffolding
const displayRules: DisplayRules = formState.displayRules ?? { const displayRules: DisplayRules = {
carryBoxes: 'whenRegrouping', carryBoxes: "whenRegrouping",
answerBoxes: 'always', answerBoxes: "always",
placeValueColors: 'always', placeValueColors: "always",
tenFrames: 'whenRegrouping', tenFrames: "whenRegrouping",
problemNumbers: 'always', problemNumbers: "always",
cellBorders: 'always', cellBorders: "always",
} borrowNotation: "whenRegrouping", // Subtraction: show when borrowing
borrowingHints: "never", // Subtraction: no hints by default
...((formState.displayRules as any) ?? {}), // Override with provided rules if any
};
config = { config = {
version: 4, version: 4,
mode: 'smart', mode: "smart",
displayRules, displayRules,
difficultyProfile: formState.difficultyProfile, difficultyProfile: formState.difficultyProfile,
...sharedFields, ...sharedFields,
} };
} else { } else {
// Manual mode: Use boolean flags for uniform display // Manual mode: Use boolean flags for uniform display
config = { config = {
version: 4, version: 4,
mode: 'manual', mode: "manual",
showCarryBoxes: formState.showCarryBoxes ?? true, showCarryBoxes: formState.showCarryBoxes ?? true,
showAnswerBoxes: formState.showAnswerBoxes ?? true, showAnswerBoxes: formState.showAnswerBoxes ?? true,
showPlaceValueColors: formState.showPlaceValueColors ?? true, showPlaceValueColors: formState.showPlaceValueColors ?? true,
@ -172,8 +182,8 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
showBorrowingHints: formState.showBorrowingHints ?? false, showBorrowingHints: formState.showBorrowingHints ?? false,
manualPreset: formState.manualPreset, manualPreset: formState.manualPreset,
...sharedFields, ...sharedFields,
} };
} }
return { isValid: true, config } return { isValid: true, config };
} }