diff --git a/.claude/agent-memory/code-reviewer/MEMORY.md b/.claude/agent-memory/code-reviewer/MEMORY.md index 5b5687a..ee05e8c 100644 --- a/.claude/agent-memory/code-reviewer/MEMORY.md +++ b/.claude/agent-memory/code-reviewer/MEMORY.md @@ -1,2 +1,3 @@ - [Anti-cheat invariant and its known bypass](project-anti-cheat-invariant.md) — pano coords are the answers; the import-walk test does not cover the debug API that leaks them. -- [Quality gate blind spots](project-quality-gates-blind-spots.md) — no-undef is now on, but no gate checks React prop/state contracts or whether /docs matches the code. +- [Quality gate blind spots](project-quality-gates-blind-spots.md) — no-undef is now on, but no gate checks React prop/state contracts, e2e stub drift, or whether /docs matches the code. +- [Scoring ladder vs leaderboard boards](project-scoring-ladder-and-boards.md) — no single ladder any more: each board is credited by its own, and the headline score is credited to none. diff --git a/.claude/agent-memory/code-reviewer/project-quality-gates-blind-spots.md b/.claude/agent-memory/code-reviewer/project-quality-gates-blind-spots.md index b46b8ac..0322779 100644 --- a/.claude/agent-memory/code-reviewer/project-quality-gates-blind-spots.md +++ b/.claude/agent-memory/code-reviewer/project-quality-gates-blind-spots.md @@ -14,8 +14,21 @@ tests and no `@testing-library` dependency - every suite is lib/API level. **Closed since 2026-08-30:** `eslint.config.mjs` now adds `no-undef: error` and `no-unused-vars: warn` on top of `next/core-web-vitals`, with browser+node globals. That kills the original failure (six `useState` declarations deleted, -ten setter call sites surviving green). Lint currently emits 4 pre-existing -`no-unused-vars` warnings; treat any new one as a signal. +ten setter call sites surviving green). Verified 2026-08-31: lint is 0 errors / +16 warnings, all `react-hooks/set-state-in-effect` or `react-hooks/refs` and all +matching an established local pattern (localStorage read in an effect, callbacks +mirrored into refs). Re-count with `npx eslint . -f json` rather than trusting a +remembered number; treat a warning of a NEW rule id as the signal. + +**Partly closed 2026-08-31:** a Playwright lane exists (`npm run test:e2e`), but +every API is stubbed in `tests/e2e/helpers.js`, whose own header says the stubs +mirror the real route shapes and must be updated with any contract change. That +is a manual promise no gate enforces: the `gameResult.bands` field was added to +`/api/guess` and the stub was not updated, so the UI block gated on it rendered +in zero tests. The stub also serves the SAME panorama URL every round, so +`PanoramaViewer`'s `key={imageData.url}` never changes and "next round loaded" +assertions pass without a viewer remount. On any diff touching an API response +shape or the round transition, open `helpers.js` and check it moved too. **Still open, verified 2026-08-30 on the Phase 6 docs review:** - React prop/state contracts. `no-undef` does not see a prop a parent forgot to diff --git a/.claude/agent-memory/code-reviewer/project-scoring-ladder-and-boards.md b/.claude/agent-memory/code-reviewer/project-scoring-ladder-and-boards.md new file mode 100644 index 0000000..552585b --- /dev/null +++ b/.claude/agent-memory/code-reviewer/project-scoring-ladder-and-boards.md @@ -0,0 +1,33 @@ +--- +name: project-scoring-ladder-and-boards +description: Scoring is region-relative and each board is credited by its OWN ladder as of 2026-08-31 - what that did to the country board, and what the headline score now means +metadata: + type: project +--- + +As of 2026-08-31 there is no single scoring ladder. `calculateScore(distance, +bands)` takes one, `bandsForBbox(bbox)` builds it from a region's bbox diagonal +(reference 10 km, factor clamped at >= 1), and there are two different consumers: + +- `/api/guess` grades the HEADLINE `gameResult.score` against the region the + player PICKED. This number is credited to no board — it is display only. +- `submitRoundScore` credits each board from the raw distance against THAT + board's own ladder, so a country round can no longer buy district points. + Each level's award comes back as `levels[].points`. + +**Why:** without scaling, a country round was a guaranteed string of zeros. The +first fix (picked-region ladder + flat fan-out) let a country round credit +district boards at country precision; the per-level fan-out closed it. + +**How to apply:** measured ladders — country 5 pts <= 6,380 m, TPHCM 442 m, +TPHCM-Q7 62 m, HN-BADINH 50 m. `SCORE_BANDS` is NOT "the district ladder", and +calling it that in UI copy or docs is wrong: only 20 of 58 playable districts sit +at factor 1.00; median is 1.51 (TPHCM-BINHTAN, 15.1 km) and max 4.73 +(TPHCM-CANGIO, 47.3 km). It is the ladder for any region up to a 10 km diagonal. +Consequence the user has not explicitly signed +off: the Vietnam board now pays +5 for any guess within 6.4 km, so it measures +rounds played rather than accuracy, and mixes with points banked under the old +50 m ladder. Anything that bands, colours, or labels a score must say which +ladder it means; the boards store no record of which one applied. Related: +[[project-anti-cheat-invariant]] (`bands` leaks nothing — the picked region is +already client-side).