Pool admin: empty-pool safety net + honest removal copy (Codex audit)
Two hardening fixes from Codex's audit: - _pick_answer falls back to the curated baseline if the live pool is empty, so an admin tombstoning every answer in a variant can't divide-by-zero the daily picker. Test added (test_picker_survives_empty_live_pool). Chosen over a minimum-count block: robust without refusing legitimate removals. - Removal copy is now honest — "Removed from future puzzles (today's answer is already set)" — since a tombstone doesn't rewrite today's generated daily_puzzles row. Panel intro updated to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -76,7 +76,7 @@
|
|||||||
async function removeWord(w) {
|
async function removeWord(w) {
|
||||||
try {
|
try {
|
||||||
wpPool = await delJSON('/api/admin/word/pool/' + encodeURIComponent(w));
|
wpPool = await delJSON('/api/admin/word/pool/' + encodeURIComponent(w));
|
||||||
wpMsg = `Removed “${w}”. It won’t appear as an answer — restore it any time below.`;
|
wpMsg = `Removed “${w}” from future puzzles (today’s answer is already set). Restore it any time below.`;
|
||||||
await refreshLookup();
|
await refreshLookup();
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
@@ -817,8 +817,9 @@
|
|||||||
|
|
||||||
{:else if section === 'games'}
|
{:else if section === 'games'}
|
||||||
<h2>Daily Word pool</h2>
|
<h2>Daily Word pool</h2>
|
||||||
<p class="muted">Look up a word and add it to the answer pool. Only real, 5- or 6-letter
|
<p class="muted">Look up a word to add or remove it from the answer pool. Only real, 5- or 6-letter
|
||||||
words in the guess dictionary qualify, so the daily answer is always solvable.</p>
|
words in the guess dictionary qualify, so the daily answer is always solvable. Removals take
|
||||||
|
effect for future puzzles and can be restored any time.</p>
|
||||||
|
|
||||||
<div class="wp-lookup">
|
<div class="wp-lookup">
|
||||||
<input type="text" bind:value={wpWord} oninput={onWpInput} maxlength="6" autocapitalize="off"
|
<input type="text" bind:value={wpWord} oninput={onWpInput} maxlength="6" autocapitalize="off"
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ def _recent_answers(conn: sqlite3.Connection, variant: str, limit: int) -> set[s
|
|||||||
|
|
||||||
def _pick_answer(conn: sqlite3.Connection, date: str, variant: str) -> str:
|
def _pick_answer(conn: sqlite3.Connection, date: str, variant: str) -> str:
|
||||||
pool = answer_pool(conn, variant)
|
pool = answer_pool(conn, variant)
|
||||||
|
if not pool: # safety net: removals must never empty the pool — fall back to curated
|
||||||
|
pool = sorted(_POOL.get(variant, []))
|
||||||
recent = _recent_answers(conn, variant, max(1, len(pool) // 2))
|
recent = _recent_answers(conn, variant, max(1, len(pool) // 2))
|
||||||
start = _seed(date, "word", variant) % len(pool)
|
start = _seed(date, "word", variant) % len(pool)
|
||||||
for i in range(len(pool)):
|
for i in range(len(pool)):
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ def test_import_dedupes_and_validates(conn):
|
|||||||
assert new5[1] in games.answer_pool(conn, "5")
|
assert new5[1] in games.answer_pool(conn, "5")
|
||||||
|
|
||||||
|
|
||||||
|
def test_picker_survives_empty_live_pool(conn):
|
||||||
|
"""An overzealous admin could tombstone every answer in a variant. The live
|
||||||
|
pool then reads empty, but the daily picker must not divide-by-zero — it falls
|
||||||
|
back to the curated baseline rather than crashing."""
|
||||||
|
for w in list(games._POOL["5"]):
|
||||||
|
games.remove_pool_word(conn, w)
|
||||||
|
assert games.answer_pool(conn, "5") == [] # live pool truly empty
|
||||||
|
ans = games._pick_answer(conn, "2026-06-11", "5") # must not raise
|
||||||
|
assert ans in set(games._POOL["5"]) # fell back to curated
|
||||||
|
|
||||||
|
|
||||||
def test_import_relifts_removed_word(conn):
|
def test_import_relifts_removed_word(conn):
|
||||||
w = _a_curated("5")
|
w = _a_curated("5")
|
||||||
games.remove_pool_word(conn, w)
|
games.remove_pool_word(conn, w)
|
||||||
|
|||||||
Reference in New Issue
Block a user