065ab98598
Don't trust client JSON at the storage layer: - sanitize_game_state() runs before merge AND on the merged result (heals legacy rows). Word Search: keep only finds whose cells actually spell a real word in that day's grid (validated when the puzzle exists, shape-only 4-12 alpha + cell-length otherwise), dedupe, renumber ci. Word: validate status enum, guess count/length/alpha, colour-row shape, terminal answer/why. - Completion is now derived from the real puzzle word count (foundWords == expected), not a client-sent `ms` — so stats can't be inflated by junk. - Date validated as YYYY-MM-DD at the API (400 otherwise) — no junk/future rows. Tests: sanitizer-rejects-junk + bad-date 400; existing tests updated to use real-shaped data (the sanitizer is a good forcing function). 237 pytest + 11 vitest green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>