Skip to content

fix: validate daily quartet paths by tile ids#6

Merged
donovan-yohan merged 2 commits into
mainfrom
fix/issue-5-invalid-quartet-combos
May 16, 2026
Merged

fix: validate daily quartet paths by tile ids#6
donovan-yohan merged 2 commits into
mainfrom
fix/issue-5-invalid-quartet-combos

Conversation

@donovan-yohan
Copy link
Copy Markdown
Owner

Summary

  • Require runtime guesses to match both the normalized word and the exact stored tile-id path.
  • Make daily puzzle validation/generation enumerate four-tile path signatures instead of collapsing by word string.
  • Add regression coverage for alternate segmentations and for the 2026-05-16 board's exact five quartet paths.

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

  • Added exact tile path comparison in validateGuess.
  • Added path-signature enumeration to validateExactQuartetPuzzle with path-level extra/missing diagnostics.
  • Updated both scripts/generate-daily-words.mjs and scripts/add-daily-puzzle.mjs to validate exact four-tile paths, not just unique word strings.
  • Regenerated src/data/generated-daily-puzzles.ts from 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:

application: [8,9,10,11] ap+plic+at+ion
institution: [16,17,18,19] inst+it+uti+on
intelligence: [12,13,14,15] in+tel+lige+nce
significantly: [0,1,2,3] sig+nifi+can+tly
somebody: [4,5,6,7] so+me+bo+dy
count=5

Verification

  • npm run check:daily-words passes.
  • npm test passes: 55 tests.
  • npm run build passes.
  • npm run --silent lint -- --format stylish passes.
  • npm exec -- eslint . passes.

Note: the local terminal wrapper reports ESLint output (JSON parse failed: EOF...) for plain npm run lint, while direct eslint and the same npm lint script with explicit stylish output both exit 0.

The Case Against / Risks

  • This intentionally tightens 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.
  • The path checker enumerates ordered four-tile paths; if future UI rules ever allow unordered selection or alternate canonical paths, this invariant would need to be revisited.
  • I did not change src/data/daily-puzzles.json for 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

Copilot AI review requested due to automatic review settings May 16, 2026 06:28
@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

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

Project Deployment Actions Updated (UTC)
quartiles Ready Ready Preview, Comment May 16, 2026 6:59am

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread src/lib/puzzle.ts
Comment on lines +269 to +294
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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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)
}

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 validateGuess to require both normalized word match and exact tileIds path 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.

@donovan-yohan
Copy link
Copy Markdown
Owner Author

verdict: request changes

blockers:

  1. the PR does not satisfy the explicit 2026-05-16 data-patch acceptance criterion.

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.

  1. scripts/generate-daily-words.mjs still accepts duplicate configured quartet words as long as the duplicate paths are configured rows.

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:

  • the regression coverage exercises validateExactQuartetPuzzle, but not the actual daily generation script path with a duplicate configured target word. add a script/data-level regression or explicit source validation so check:daily-words fails for that class.
  • the tighter runtime validation is good and the shorter-word smoke coverage still passes, but exact-path semantics mean only stored PuzzleWord paths are accepted now. that behavior needs to stay intentional/documented.

verified locally:

  • npm run check:daily-words: passed
  • npm test: passed, 55 tests
  • npm run build: passed
  • npm run lint: the plain command exits 2 in this terminal wrapper with "ESLint output (JSON parse failed: EOF...)" after build/tests pass; direct local equivalents pass:
    • npm run --silent lint -- --format stylish: exit 0
    • ./node_modules/.bin/eslint .: exit 0
  • CI/checks on PR fix: validate daily quartet paths by tile ids #6 head 9ede4e9 are green.

what would unblock:

  • either actually patch 2026-05-16 source data per issue Fix duplicate 4-tile combinations in daily puzzle generation #5, or get the acceptance criterion changed with evidence that the repo snapshot is already clean.
  • add uniqueness/source validation for configured quartet words/paths in scripts/generate-daily-words.mjs and scripts/add-daily-puzzle.mjs, with regression coverage that fails the duplicate-segmentation configured-row case.

@donovan-yohan
Copy link
Copy Markdown
Owner Author

Addressed the release-gate blockers from the latest review.

What changed:

  • Patched src/data/daily-puzzles.json for 2026-05-16 to a new exact-path-safe board and regenerated src/data/generated-daily-puzzles.ts.
  • Added shared source validation in scripts/daily-puzzle-source.mjs requiring configured quartet words to be unique in addition to existing date/count/tile validation.
  • Wired that validation into both scripts/generate-daily-words.mjs and scripts/add-daily-puzzle.mjs; add-daily now validates existing source rows and every newly generated row before writing.
  • Added scripts/daily-puzzle-source.test.mjs covering two configured rows that spell the same target word through different exact tile paths.
  • Updated README/generated header wording to document unique configured quartet words and exact tile-id path semantics.

Verification:

  • npm run check:daily-words passed.
  • Transient invalid-source proof: replacing 2026-05-16 with two configured abcdefghij rows using different paths makes npm run check:daily-words fail with configured quartet words must be unique.
  • npm test passed: 56 tests / 3 files.
  • npm run build passed.
  • npm run lint still exits 2 in this local terminal wrapper with ESLint output (JSON parse failed: EOF...); repo-local ./node_modules/.bin/eslint . passed with exit 0.
  • gh pr checks 6 --repo donovan-yohan/quartiles passed after push.

New head: f8b94de7527b6d48b1f9ba94f48cbc331dbbad87.

@donovan-yohan donovan-yohan merged commit f3ec5a2 into main May 16, 2026
3 checks passed
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.

Fix duplicate 4-tile combinations in daily puzzle generation

2 participants