ddcfab3a11
New per-row "Articles" button on the Sources table expands a read-only inline
panel of the source's ACTUAL ingested articles — so the automated metrics
(paywall/image/acceptance/duplicate) can be verified against evidence instead of
trusted blind. Distinct from "Check" (which re-samples the LIVE feed for
would-pass quality); this shows what's already in the DB, which is what the table
metrics are computed from.
- Backend: GET /api/admin/sources/{id}/articles?filter=&limit=&offset= (admin,
read-only). queries.source_articles + source_articles_summary — per article:
title, url, date, accepted, reason (the "why"), topic/flavor, paywalled
(domain rule), has_image, duplicate. Summary = counts + source-level paywall
rule.
- Frontend: expandable panel with a summary header ("27 ingested · 18 accepted
· … · paywall rule: ON (domain)"), filter chips (All/Accepted/Rejected/No
image/Duplicates), compact rows with title→link + badges + reason, Load more.
So "100% paywall" or "0% images" becomes clickable evidence: open two articles
to tell a real paywall from a mis-flagged domain, or a true image gap from an
enrichment failure. Test: test_source_articles_inspector. 241 pytest + 11 vitest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
3.3 KiB
Python
79 lines
3.3 KiB
Python
"""Locks the word-search placement qualities players actually feel:
|
||
|
||
1. Every word gets placed (exhaustive candidate search — nothing silently dropped).
|
||
2. Grids INTERLOCK like a real puzzle (the "clean isolated words" regression).
|
||
3. Words SPREAD across the board (the "all clumped in one corner" regression).
|
||
4. Same date/seed → same grid (cross-device players must see identical puzzles).
|
||
|
||
Thresholds were calibrated against all curated themes × 12 seeds × 3 tiers
|
||
(288 grids/tier): crossing fraction averaged ~0.7 (old algorithm: ~0.3, with a
|
||
third of small grids having ZERO crossings), worst quadrant share 0.42, and all
|
||
four quadrants always held word cells. Deterministic, so no flake margin needed.
|
||
"""
|
||
|
||
import random
|
||
import statistics
|
||
|
||
from goodnews.games import _WS_FALLBACKS, WS_TIERS, _WS_ORDER, _build_grid, _place_words, _zone
|
||
|
||
|
||
def _tier_grids(tier):
|
||
"""Yield (placements, size) for every curated theme × 12 seeds in a tier."""
|
||
t = WS_TIERS[tier]
|
||
for _, words in _WS_FALLBACKS:
|
||
for seed in range(12):
|
||
rng = random.Random(seed * 1000 + 7)
|
||
ws = list(words)
|
||
rng.shuffle(ws)
|
||
_, placements = _place_words(ws[: t["count"]], t["grid"], seed)
|
||
yield placements, t["grid"]
|
||
|
||
|
||
def _cross_fraction(placements):
|
||
"""Fraction of placed words sharing at least one cell with another word."""
|
||
owners: dict[tuple[int, int], list[str]] = {}
|
||
for w, cells in placements:
|
||
for cell in cells:
|
||
owners.setdefault(cell, []).append(w)
|
||
crossing = set()
|
||
for ws in owners.values():
|
||
if len(ws) > 1:
|
||
crossing.update(ws)
|
||
return len(crossing) / len(placements)
|
||
|
||
|
||
def test_all_words_placed():
|
||
for tier in _WS_ORDER:
|
||
for placements, _ in _tier_grids(tier):
|
||
assert len(placements) == WS_TIERS[tier]["count"]
|
||
|
||
|
||
def test_grids_interlock_without_clumping():
|
||
for tier in _WS_ORDER:
|
||
fracs = []
|
||
for placements, size in _tier_grids(tier):
|
||
fracs.append(_cross_fraction(placements))
|
||
# Spread: word cells must reach all four quadrants, and no quadrant
|
||
# may hoard more than half of them (perfectly even would be 0.25).
|
||
quad: dict[tuple[int, int], int] = {}
|
||
cells = {c for _, cs in placements for c in cs}
|
||
for r, c in cells:
|
||
quad[_zone(r, c, size)] = quad.get(_zone(r, c, size), 0) + 1
|
||
assert len(quad) == 4, f"{tier}: words confined to {len(quad)} quadrant(s)"
|
||
assert max(quad.values()) / len(cells) <= 0.5, f"{tier}: clumped in one quadrant"
|
||
# Interlock: every grid has some crossings; on average most words connect.
|
||
assert min(fracs) >= 0.3, f"{tier}: a grid came out as disconnected clean words"
|
||
assert 0.55 <= statistics.mean(fracs) <= 0.9, f"{tier}: avg crossing {statistics.mean(fracs):.2f}"
|
||
|
||
|
||
def test_grid_deterministic_and_honest():
|
||
"""Same inputs → byte-identical grid, and every reported word is really in it
|
||
(forward or reversed along some line — spot-checked via placements)."""
|
||
words = _WS_FALLBACKS[0][1][:9]
|
||
rows1, placed1 = _build_grid(words, 11, 42)
|
||
rows2, placed2 = _build_grid(words, 11, 42)
|
||
assert rows1 == rows2 and placed1 == placed2
|
||
_, placements = _place_words(words, 11, 42)
|
||
for word, cells in placements:
|
||
assert "".join(rows1[r][c] for r, c in cells) == word
|