fix: validate daily quartet paths by tile ids#6
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request transitions the puzzle validation logic from word-based to path-based, ensuring that guesses are validated against specific sequences of tile IDs rather than just the resulting word string. Key changes include the introduction of path signatures, updates to the recursive search functions in scripts and library code to track tile ID paths, and enhanced validation in validateGuess and validateExactQuartetPuzzle. Feedback was provided regarding a performance optimization in the getFourTileWordPaths function to pre-normalize tiles before the recursive search to avoid redundant regex operations.
| export const getFourTileWordPaths = (puzzle: TilePuzzle): QuartetPath[] => { | ||
| const puzzleWordSet = new Set(puzzle.words.map((word) => word.word)) | ||
| const paths: QuartetPath[] = [] | ||
|
|
||
| const search = (prefix: string, usedTileIds: Set<number>, tileIds: number[]) => { | ||
| if (tileIds.length === MAX_TILES_PER_WORD) { | ||
| if (puzzleWordSet.has(prefix)) { | ||
| paths.push({ word: prefix, tileIds: [...tileIds], signature: pathSignature(tileIds) }) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| for (let tileId = 0; tileId < puzzle.tiles.length; tileId += 1) { | ||
| if (usedTileIds.has(tileId)) { | ||
| continue | ||
| } | ||
|
|
||
| usedTileIds.add(tileId) | ||
| search(prefix + normalize(puzzle.tiles[tileId]), usedTileIds, [...tileIds, tileId]) | ||
| usedTileIds.delete(tileId) | ||
| } | ||
| } | ||
|
|
||
| search('', new Set<number>(), []) | ||
| return paths.sort(sortQuartetPaths) | ||
| } |
There was a problem hiding this comment.
The getFourTileWordPaths function performs an exhaustive search of all 4-tile paths on the board. Currently, it calls normalize on each tile inside the innermost loop of the recursion. Given a 20-tile board and a depth of 4, this results in over 116,000 calls to normalize (which includes a regex replacement).
Since the tiles are constant for the duration of the search, you can pre-normalize them once at the start of the function to improve performance.
export const getFourTileWordPaths = (puzzle: TilePuzzle): QuartetPath[] => {
const normalizedTiles = puzzle.tiles.map(normalize)
const puzzleWordSet = new Set(puzzle.words.map((word) => word.word))
const paths: QuartetPath[] = []
const search = (prefix: string, usedTileIds: Set<number>, tileIds: number[]) => {
if (tileIds.length === MAX_TILES_PER_WORD) {
if (puzzleWordSet.has(prefix)) {
paths.push({ word: prefix, tileIds: [...tileIds], signature: pathSignature(tileIds) })
}
return
}
for (let tileId = 0; tileId < normalizedTiles.length; tileId += 1) {
if (usedTileIds.has(tileId)) {
continue
}
usedTileIds.add(tileId)
search(prefix + normalizedTiles[tileId], usedTileIds, [...tileIds, tileId])
usedTileIds.delete(tileId)
}
}
search('', new Set<number>(), [])
return paths.sort(sortQuartetPaths)
}There was a problem hiding this comment.
Pull request overview
This PR tightens “quartet” validation by treating a valid four-tile solution as an exact tile-id path (not just a word string), preventing alternate segmentations/paths from being accepted at runtime and during daily puzzle generation/verification.
Changes:
- Updated
validateGuessto require both normalized word match and exacttileIdspath match. - Reworked exact daily quartet validation to enumerate and compare four-tile path signatures, with richer diagnostics for extra/missing paths.
- Updated daily puzzle generator/add scripts (and regenerated artifact) to enforce “exactly five configured four-tile paths, no extras,” plus added regression tests (including 2026-05-16 path proof).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/lib/puzzle.ts |
Enforces exact tile-id path matching in runtime guessing and adds path-aware quartet validation/enumeration. |
src/lib/puzzle.test.ts |
Adds regression tests for alternate paths and validates 2026-05-16 has exactly 5 quartet paths. |
src/data/generated-daily-puzzles.ts |
Regenerated header comment to reflect path-based quartet validation rules. |
scripts/generate-daily-words.mjs |
Validates generated boards using exact four-tile path signatures (not word-string sets). |
scripts/add-daily-puzzle.mjs |
Ensures candidate boards are “exact quartet boards” based on path signatures. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
verdict: request changes blockers:
issue #5 and this review task both require patching src/data/daily-puzzles.json for 2026-05-16 and regenerating src/data/generated-daily-puzzles.ts. this branch leaves src/data/daily-puzzles.json unchanged, and the generated artifact diff is only the header comment. the PR body says the source rows are already path-clean, but then the PR no longer proves/fixes the reported prod invalid board as requested. either patch the daily source, or update the issue/review contract with evidence that the source-data requirement is obsolete before merging.
lines 108-122 dedupe requiredQuartets into generatedWords, then validate only path signatures/count/extra/missing. there is no check that the five configured quartet row words are unique. a source puzzle with two configured rows spelling the same word through two different tile paths can pass hasExactTargetQuartets. runtime buildPuzzleFromQuartets then collapses duplicate target words through a Map keyed by word, so one of the intended quartet rows disappears from puzzle.words/isQuartet. that violates the issue requirement to reject duplicate segmentations of a target word, not merely extra unconfigured paths. concerns:
verified locally:
what would unblock:
|
|
Addressed the release-gate blockers from the latest review. What changed:
Verification:
New head: |
Summary
Root Cause
Runtime validation accepted any selected tile path whose concatenated text matched a puzzle word. The daily validators also collapsed four-tile solutions into
Set<string>, so multiple tile-id paths for the same word could hide behind one word string.Fix
validateGuess.validateExactQuartetPuzzlewith path-level extra/missing diagnostics.scripts/generate-daily-words.mjsandscripts/add-daily-puzzle.mjsto validate exact four-tile paths, not just unique word strings.src/data/generated-daily-puzzles.tsfrom the updated generator.2026-05-16 Data Patch
The new path-aware checker proves the checked-in 2026-05-16 source puzzle already has exactly five accepted four-tile paths, so I did not make an arbitrary source-row swap. The generated artifact was regenerated after the generator/checker change. Path proof:
Verification
npm run check:daily-wordspasses.npm testpasses: 55 tests.npm run buildpasses.npm run --silent lint -- --format stylishpasses.npm exec -- eslint .passes.Note: the local terminal wrapper reports
ESLint output (JSON parse failed: EOF...)for plainnpm run lint, while direct eslint and the same npm lint script with explicit stylish output both exit 0.The Case Against / Risks
validateGuess: alternate segmentations of an otherwise known word are now rejected unless that exact tile-id path exists in puzzle word data. That is the point of the fix, but it may surprise anyone expecting word-string-only validation.src/data/daily-puzzles.jsonfor 2026-05-16 because the new checker showed the current source rows are already path-clean. If the production data differed from this repo snapshot, that environment needs the regenerated app with the runtime validator fix.Closes #5