Skip to content

integrate: post-submission fixes (#38-#41, #43, #44) — merge only after the results - #42

Draft
prodelt wants to merge 20 commits into
mainfrom
integrate/post-submission
Draft

prodelt wants to merge 20 commits into
mainfrom
integrate/post-submission

Conversation

@prodelt

@prodelt prodelt commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Draft. Do not merge before the judging results are published — the submitted version is the tag submission-2026-10-05 (80d8375); commits after the deadline do not count.

One branch merging every post-submission draft so CI runs once on the combination:

Integration fixes on this branch:

Local: typecheck green, lint 0 errors (1 old warning), unit 903 passed / 3 skipped, initial JS 149.8 KB gzip of 150.

After merging: pnpm supabase:functions so finish-race applies the same plausibility verdict.

🤖 Generated with Claude Code

prodelt and others added 13 commits October 6, 2026 00:20
…r data on sign-out

The daily challenge and own text get their ids in @typing-race/curriculum
(dailyId/isDailyId, ownTextId/isOwnTextId, isFreePracticeId). The server
does not know these ids and answers unknown_scale, which the outbox keeps
and re-sends forever, so sync stayed "pending" and sign-out stopped.

- submitAttempt stores free practice locally but never queues it
- free practice an older build queued is dropped from the outbox
- wipeLocalIfSynced/forgetLocal also remove typing-race:daily* and
  typing-race:sprint-best:* (the guide-seen flag stays: it is a per-browser
  UI hint with no learner data)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Own text records under ownTextId (yq.owntext), not the stock real-text id,
  and is labelled "Свій текст"/"Own text" on the result screen
- Free practice (daily and own text) is ignored by mastery/progress,
  key confidence and weak spots (derive), the coach's last attempt, XP,
  the streak and daily-goal minutes (gameStats), and the result reward
- /own hides the practice/test toggle (always practice); "Again" on an
  own-text result re-runs the same text from memory, or opens /own when
  it is gone
- The "cut to N characters" notice shows on the pre-start card
- Daily done-today and the day count are kept per language

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, pause on blur

- Score from packages/metrics (computeMetrics) over the keystroke log, so
  accuracy is over all keystrokes and a wrong letter left in the line
  counts. The engine gains stop(): a time-boxed run ends as completed and
  finish() returns the log so far. startSprint gates the input at 60 s of
  typing time, so no key after the minute is logged; the time is capped
  at the minute, or the time used if the line ends sooner (stated on
  /formulas in uk and en).
- A pre-start card with the exercise layout check (shared useLayoutCheck
  and LayoutStatus extracted from PreStart); Start stays disabled on a
  mismatch. Play Mode during the run; the typed-layout notice too.
- Leaving the window pauses the clock; Escape pauses and resumes; the
  exercise pause dialog resumes.
- The line comes from seededRandom with a fresh seed per sprint (ADR 0005).
- The countdown starts from SPRINT_SECONDS, derived from SPRINT_MS.
- The locked line uses "ви". The line uses the text-size setting, hides
  its count-up time and its ninety word stations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… closes every tooltip

Testers could not find the new modes: Sprint sat behind /review, Own text behind a Map key,
Daily was a small link. Home now has a sixth, quiet row of four cards (glyph, name, one line).
Sprint is dimmed until Stage 2 opens and Mistakes until there are weak spots; their Tip says
how they open. Daily shows a "готово" chip once cleared today. The row is a lazy chunk so the
initial JS stays under 150 KB, and a coach-mark step points at it.

On screens up to 900 px tall the session steps become one line of chips and the vertical
rhythm tightens, so Continue's Start button is never clipped (it already was at 1024x768).

The status bar's stats, the layout switch and Home's weak-spot chips now use the shared Tip,
so Esc hides their bubble. Daily's "Днів поспіль" becomes "Викликів поспіль" so it no longer
reads as the Streak. CoachMarks' keydown listener joins the seam exemptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ground note

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CoachMarks listened for keydown on the document directly, which turned
tools/architecture.test.ts red on main. The capture-phase listener now
lives in apps/web/src/seams/keys.ts (takeKey), imported from its own file
so it stays in the lazy CoachMarks chunk. Behaviour is unchanged: Esc
closes the guide wherever focus is and the screen never sees it; Enter
still steps on from the bubble. A component test pins both down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The results' "race again" always called quickMatch, so friends from a
private room were thrown into public quick match. Now a private room's
rematch opens a new private room and broadcasts its code on the old
room's channel ('rematch' event, allowed by the existing realtime
policy); whoever is still on the results sees "<name> calls everyone to
another race" and the button becomes «Приєднатися», which joins by code.
If that room cannot be joined, a new one is opened instead. Quick-match
results behave as before.

The decision and its execution are pure (features/race/rematch.ts) and
unit-tested, along with a Results component test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After delete-account the server has no session left, so signOut's
/auth/v1/logout answered 403 and the browser logged it. For a deleted
account the stored session is dropped first; signOut then finds no
token, makes no request, and still clears this browser and emits
SIGNED_OUT. Ordinary sign-out still ends the session on the server.

Tested with a real supabase-js client against an in-test fake fetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…egrate/post-submission

# Conflicts:
#	apps/web/src/features/exercise/PreStart.tsx
…the keys seam

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
typing-race Ready Ready Preview Oct 6, 2026 6:43am UTC

prodelt and others added 5 commits October 6, 2026 08:31
…, and the audit's copy gaps

Accessibility (D2):
- The status bar's Level, Streak and Race Rating, the rail's Season card, Home's weak chips and
  Profile's badges are named groups/items whose name says the value; the stage's scroll area is a
  named region ("Вміст екрана").
- e2e: expectFocusablesNamed reads Chromium's accessibility tree and fails on any focusable node
  without a role or a name; viewport-1024 and the new viewport-390 run it on every screen.

Phone, 390x844 (D3):
- /about gets a phone layout: one column, type sized to the screen, the hero card and lede no
  longer cut (scrollWidth 462 -> 390). The method cells no longer stack on top of each other:
  the shell's .stage grid placement leaked onto them at every width.
- The bottom dock spans the full width with five even tabs; the rail's side tooltips are hidden
  on a phone.
- Sprint, Daily and Own text ask for a physical keyboard on a phone like every training route
  (Sprint's header overlapped itself at 390).
- e2e/viewport-390.spec.ts: 14 routes, no sideways scroll, nothing cut, axe, named focus stops,
  and the dock's even spacing.

Copy and logic:
- First run: picking a language switches the status bar's УКР/ENG at once, and the switch picks
  in the flow (past step 1 it returns to the level step for the new layout).
- The attempt-type buttons keep one pair of names in both states: «Тренувальна спроба» /
  «Залікова спроба».
- Academy: "mechanics" is decided per exercise, from whether its lines hold letter groups that
  are not words; «the · you» and other word drills no longer say «Рядки не є словами».
- Result: after a cleared practice attempt the headline asks for the test only when «Далі»
  opens it (practiceAdvice); otherwise it says the floor is cleared. Unit-tested.
- Profile badges: locked is a dashed edge, a lock glyph and muted text at >= 4.5:1, never
  opacity, so axe passes in light, dark and low vision.
- e2e: the coach-line assertions follow the shortened result copy; the weak-transition line is
  checked to name both fingers in full and not to overflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… characters

A jury-style audit found that one beforeinput carrying the whole line was
graded as a passing test (9 949 SPM, "exercise mastered"), and that an
automation tool's 4 264 SPM became the personal best. It also found that
SPM counted wrong keystrokes, so 7 errors on a 59-character line read 12%
above 60·N/T, outside the jury's ±10%.

- SPM = 60 000 × min(correct character keystrokes, text length) / elapsed
  ms, which is 60·N/T on a finished attempt. Accuracy is unchanged.
- The input seam takes an insertText of several characters as ignored
  (reason "burst"), like a paste. Compositions are still judged per char.
- packages/metrics gives each attempt a verdict, metrics.implausible:
  "burst" when the median interval between character keystrokes is under
  25 ms, "tooFast" above 1 500 SPM. Such an attempt is kept and shown as
  "Не зараховано: набір не схожий на ручний", but counts toward nothing:
  the progress fold, Academy progress, XP and level, streak and daily
  minutes, personal best, speed trend and session sizing all skip it
  through curriculum's typedByHand. finish-race reads the same verdict.
  DERIVED_VERSION is 2.
- The Formulas page (uk, en), ADR 0003 (amendment), CONTEXT.md and
  docs/security.md state both rules.
- The e2e harness types at 45 ms a key (about 1 300 SPM) and the CDP
  harness waits the same by default; the product has no test escape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te/post-submission

# Conflicts:
#	apps/web/src/app/state/gameStats.ts
#	apps/web/src/features/result/reward.ts
…only)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… best

The plausibility verdict from #43 now reaches the two free-practice modes
that keep their own records: a daily attempt flagged implausible clears no
day, and a sprint score carrying a verdict sets no personal best.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@prodelt prodelt changed the title integrate: post-submission fixes (#38-#41) — merge only after the results integrate: post-submission fixes (#38-#41, #43, #44) — merge only after the results Oct 6, 2026
race-real.spec.ts «a Ukrainian race with the system on US explains
itself» failed whenever the private room drew the «Київ стоїть…» text
(one of the four Ukrainian race texts, picked by `order by random()`):
1 try in 4, both tries 1 run in 16.

Cause: the real-key harness looks up what the active layout prints on a
physical key in the curriculum's QWERTY, which lists only the keys an
English exercise uses. It has no BracketLeft, BracketRight, Backslash or
Slash, and for those the harness fell back to the intended character. So
«ї» pressed "on US" arrived as «ї», not «]». «ї» is a ЙЦУКЕН letter, and
the race's foreign-letter watch rightly reset on it: R b ї d ␠ c is a
streak of 2, never FOREIGN_STREAK (3), so the notice never came. The
trace agrees: the engine counted all six keys (errors 1..6), the race
never re-mounted, focus held, no page error, the notice slot stayed
empty. The product is right: a real US layout prints «]» there.

Fix: the harness knows what a real US layout prints on the four keys the
curriculum leaves out, and throws on any other missing key instead of
typing a character the active layout cannot produce. The key mapping is
a pure `keystrokeOf`, pinned by tools/real-keyboard.test.ts: no ЙЦУКЕН
key pressed on US types a Cyrillic letter, «Київ с» types R b ] d ␠ c,
and those six keys reach FOREIGN_STREAK. All three failed before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@prodelt

prodelt commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

CI green on dc4ac8b (all 4 jobs, first attempt). Includes #45: the CDP harness no longer types «ї» for a US keyboard, which made race-real.spec.ts:107 fail whenever the private room drew the «Київ…» text (1 in 4).

This branch was successfully deployed

1 active deployment
Preview — dc4ac8bf Deployed Oct 6, 2026 by vercel[bot]
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.

1 participant