# Working Notes

## Learner

- CFOP speedsolver. Notation, slices, wide moves, finger tricks are all free — never explain them.
- Favourite puzzle is the 4x4. That is the emotional centre of the mission; tie lessons back to it explicitly, or they will feel like a detour.
- Never solved anything blind before. Zero blind vocabulary as of lesson 1.
- Practice: 20–30 min most days. **Hard constraint on lesson size** — one sitting, one win.

## Stated preferences

- Chose 3-style corners over Old Pochmann, knowing it is the slower on-ramp.
  Do not quietly substitute OP "to get a success faster" — that decision was made deliberately.
- Wants consistency *and* speed (sub-5 4BLD), not just a single novelty success.

## Teaching decisions

- **Corner buffer: UFR (Speffz `C`).** Standard for 3-style, ergonomic, and the community default.
- **Edge buffer: DF (Speffz `U`).** M2's buffer, and it parallels r2's DFr buffer on the 4x4.
- **Cycle-break convention: lowest unsolved Speffz letter.** Deterministic, so drills can auto-grade.
  Advanced solvers pick breaks to shorten memo — teach that only after the convention is automatic.
- Lessons are graded ramps: recall → one target → few targets → full memo.
  Real random scrambles throughout, since artificially short memos are not achievable
  (even a 3-move scramble averages 9.3 edge targets — measured, not guessed).

## Daily tool

`speffz-trainer.html` (workspace root, meant to be bookmarked) — spaced-repetition recall of all
48 stickers, both directions, requested by the learner.

**Now WaniKani-shaped, on the learner's request: three sessions a day.** Scheduling is by the
clock, not the calendar — `srs.js` stores absolute timestamps and steps
`4h → 8h → 1d → 2d → 4d → 8d → 16d → 32d`, with `1h` reserved for cards you just missed.
The first three steps land inside one day, so morning / afternoon / evening each have something
ripe. A day-granular scheduler cannot express this; it can only ever say "tomorrow".

Consequences worth remembering before touching this again:

- **Promotion requires the card to have been due.** Answering early is practice and buys no
  interval. This replaced the earlier once-per-day promotion cap, and it is a stronger rule —
  cram inflation is now structurally impossible in *any* mode, not just the practice button.
- A miss drops **two** stages and returns within the hour (WaniKani's behaviour).
- Migration from the old day-based v1 format runs on load and is **written back once**. It must
  stay idempotent: the conversion is relative to "now", so re-deriving it on every page open
  would push every due time further into the future each time. There is a regression test for
  exactly this.
- The page keeps a 20s ticker so a tab left open wakes up when the next batch ripens. It
  **offers** a session and never opens one — see below.

### Never start a timed card without the learner (bug, fixed)

The ticker used to auto-start a session when cards ripened. With the tab in the background that
meant the first card sat there timing an absent person, producing a 1000s+ "recall" that then
poisoned the log. Three defences, all worth keeping in any timed trainer:

- **Explicit Begin.** Every entry point (`enter()`) offers a ready screen; nothing but a click or
  Enter starts a card. Guard the global Enter handler against `BUTTON`/`INPUT` targets or the
  focused button fires it twice and reshuffles the queue mid-start.
- **The clock stops when the tab is hidden.** `visibilitychange` shifts `t0` forward by the time
  away. If the card was ever hidden, or recall exceeds `MAX_TIMED_MS` (30s), the attempt is
  recorded as *untimed* (`r: null`) — right answer, no speed evidence, card keeps its place.
- **Unknown timing cannot promote.** `quick` requires an actual measurement, so a missing time is
  never silently treated as fast.

`srs.js` also purges log entries over `OUTLIER_MS` (120s) on load, which cleaned the entries the
bug had already written. A single such entry is enough to flatten a chart's whole y-axis.

### Batches, not a trickle

Cards answered in one sitting fall due a minute or two apart — an artefact of how fast you typed,
not a schedule anyone intended. That surfaced as a standby table reading "1 card in 23 min,
3 cards in 24 min, 4 in 25 min…", but the display was the lesser half: `queue()` was handing over
whatever happened to be ripe *this minute*, so pressing Begin gave a one-card session, then
another a minute later.

`srs.js` now groups due times into **batches** (`clusterMs`, default 20 min, chained on successive
gaps). A batch is served whole as soon as its earliest card is ripe, and `spread()`/`upcoming()`
report per batch. The screenshot case collapsed from 19 rows to 4, and a Begin at +23 min serves
all 16 reviews instead of 1.

Two invariants worth not breaking: anything **overdue is always served** even if it sits in an
older batch (never strand a straggler), and a future batch is **never pulled forward** just
because a neighbouring one ripened.

Two design choices worth keeping in later trainers:

- **Correct-but-slow does not promote a card.** Under a blindfold a letter you had to work out is
  a letter you lose.
- **Progress bar shows maturity, not the strict learned count.** The strict count needs several
  spaced reviews before it moves at all, and a bar stuck at zero for a week kills the habit.

### Measuring speed honestly (learner reported being discouraged)

Said they were proud of recalling some stickers at 1–2s but the tool kept calling them too slow.
Two real faults behind that, both fixed:

- **I was timing prompt → submit, so typing counted as thinking.** Typing `UBL` is three
  keystrokes slower than typing `A` and says nothing about whether you knew it. Now
  `recall` = prompt → *first keystroke* is what gets graded, with `total` kept only for the record.
- **A flat bar treats a card met an hour ago like one held for a week.** The bar now ramps
  `3.5 → 3.2 → 2.8 → 2.4 → 2.2 → 2.0s` across stages, so early practice is not gated on speed.
  Speed pressure arrives once the association exists.

Feedback copy now compares against *your own previous best on that card*, and flags a personal
best. Never phrase a slow answer as a verdict — slow is the normal state of a card still being
learned, and the bar exists so the scheduler knows what to keep showing.

### `progress.html`

Charts of recall speed, accuracy, speed by face, speed by direction, stage mix, and
fastest/stickiest tables. Every attempt is logged to `data.log` in `srs.js` (capped at 4000).

Grouped **by session, not by day** — with three sessions a day, daily buckets would hide the
within-day improvement and give one lonely point on day one. A session is a run of answers with
no gap over 30 minutes.

Two things the first real look at this page exposed:

- **Empty history is the normal first-run state, not "you have done nothing".** Logging only
  began when the page was built, so an existing deck had cards but no attempts. Rendering that
  as `0 attempts logged` beside a row of em-dashes was actively demoralising. There is now a
  first-run panel explaining that the schedule is intact and timing starts here, and the stage
  chart still draws, since it comes from the cards rather than the log.
- **"Cards automatic" was flattery.** It counted stage alone, so cards that reached a long
  interval via the v1 migration — or via lenient early grading — were called automatic without
  ever having been recalled quickly. `stats.learned` now also requires a recorded recall under
  the final bar. Honest, self-healing, and it made a nonsense reading ("0 attempts, 18 automatic")
  impossible.

Scope defaults to edges. Flip on corners at lesson 2, not before — 96 cards at once is a wall.

**Bug found on day 2 (fixed): extra practice was grading through the scheduler.** Drilling a card
repeatedly in one sitting promoted it several boxes, pushing cards up to 44 days out and leaving
the next day empty. Reproduced: 15 rounds of extra practice emptied the following day entirely.

Two root-cause fixes, both in `srs.js` — keep them in any future trainer:

- **Practice mode** (`grade(id, ok, ms, {practice:true})`) leaves the schedule alone on correct
  answers. A miss still demotes: forgetting is real evidence whenever it appears.
- **At most one promotion per card per calendar day** (`c.p`). Spacing is the thing being
  measured, so a second correct answer the same day cannot buy a second interval. This holds even
  in normal mode, so no future UI can re-introduce the same inflation.

Also added: `upcoming()` so an empty queue says *when* the next batch lands rather than just
"nothing due", `ahead()` for pulling reviews forward, and `rebuild()` to repair a deck that was
already inflated.

**The actual day-2 cause, found by the learner, not me: the day rolled over at midnight.**
They practised after midnight, so the session landed on the *same* app-day as the afternoon they
next opened it — nothing due, new-card ration already spent. Reproduced exactly: 00:30 session
and 14:00 the same date both mapped to one app-day.

Fixed with a **04:00 local rollover** (`dayCutoffHour`, Anki's default and for this reason).
A session between midnight and 4am now belongs to the previous study day. This also repaired the
existing data for free: the new index during daytime is one higher than the old one, so
already-stored cards became due rather than receding.

Process note worth keeping: I had a reproduced bug in hand (inflation) and let it explain a
symptom it did not actually cause. Reproducing *a* cause is not the same as confirming *the*
cause. The trainer now prints its own due-date spread and hours-since-study on the empty-queue
screen, and distinguishes the two diagnoses, so the next mystery is self-service.

## M2 findings (lesson 2) — hard-won, do not re-derive

`M2` is **not** a clean buffer/target swap. It also swaps **UF↔DB** and the U/D + F/B centres.
Everything awkward about the method follows from that one fact.

- The 16 off-slice setups from the Speedsolving wiki all verify exactly as
  "exchange buffer with target, plus that side effect". They form **four patterns**
  (R face / L face / R side / L side), which is how they should be taught — not as 16 strings.
- **Rotation-free, on the learner's preference.** The wiki writes 8 of them with an `x'`
  rotation; conjugating it away gives strictly better algorithms, because `x' U x = B` and `M2`
  is a half turn so the rotation passes straight through it. `x' U' R U` becomes `B' R B`.
  Those 8 setups went 34 moves → 26, no cube rotation anywhere in the method, and the structure
  came out *more* teachable: two carriers (U to lift the target to UB, B to take it round the
  back), each with an R and an L version. Re-verified end to end afterwards.
- **The pair rule is real and I measured it.** Executing traced memos with no rule: 147/500
  solved. Treating C↔W (and I↔S) as swapped when they fall in an *even* position: 500/500.
  The reason is not arbitrary — by then the pending side effect really has swapped UF and DB.
- **I, Q and S do have standalone algorithms — they are just long.** No sequence exists within 8
  moves (meet-in-the-middle over every face and slice turn, both side-effect models), which is why
  the first searches came up empty. The wiki's "Additional M-slice M2 Algs" section has them at
  10–11 moves; all verified. Chosen:
  `I = D M' U R2 U' M U R2 U' D' M2`, `S = D2 F2 U M' U' F2 D2 U' M' U`,
  `Q = U B' R U' B M2 B' U R' B U'`.
  Several published candidates for these **fail verification** — always check before trusting one.
- **The pair rule applies to C, I, W, S but NOT to A or Q.** The rule exists because the side
  effect moves UF and DB, so shots to those stickers do not commute with it. A and Q live on UB,
  which the side effect leaves alone. Getting this wrong costs a working method.
- `M2` has **three** swap channels, not one: A↔U, I↔S, C↔W. The plain setups use the first; every
  x'-rotated setup uses I↔S. Any diagnostic that asks "did the target land on UB" is wrong for
  half the table.
- Edges-only never hits parity: corners solved ⇒ even edge permutation ⇒ even target count.
  This is worth stating in every lesson that touches it, because it is the whole reason the
  edges-first route is cheap.

`assets/m2.js` holds the full 22-target table, `checkSetup()` (accepts *any* correct setup, not one
canonical string), `diagnoseSetup()` (says where the target actually landed) and `execute()`.
Verified end-to-end: **2018/2018** even-parity scrambles fully solved, all six M-slice targets
exercised. With the pair rule disabled: 200/753 — the rule is load-bearing, and that check is in
the suite so it stays that way.

`Cube.edgeScramble()` produces scrambles that leave the corners solved, by composing
corner-fixing edge 3-cycles under conjugation (conjugation preserves "fixes every corner").
So practice needs no hand-solved corners, and parity is impossible by construction.

## Component library

`assets/cube.js` is the workhorse and is verified end-to-end: on 5000 random scrambles the traced
memo provably solves the cube, for edges and for corners from two different buffers.
Exposes `apply`, `trace`, `scramble`, `scrambleFor`, `render`, `sticker`, `matches`.
Reuse it for M2, r2 and centre lessons rather than writing anything new.

`assets/srs.js` — Leitner scheduler with localStorage, day-level. Reuse for the letter-pair,
r2-target and commutator trainers rather than rewriting the scheduling.

`assets/quiz.js` — fixed-length lesson drills (`Drill.choice`, `Drill.typed`). Different job from
srs.js: quiz.js is for a lesson's practice section, srs.js is for daily retention tools.

`assets/chart.js` — dependency-free SVG line and bar charts. Colours come from `--c1..--c6` in
style.css so light and dark are defined once; series are labelled directly at their last point
(with collision nudging) rather than via a legend; every chart carries `<title>` and a
visually-hidden data table. Reuse for any future progress view.

Use `Cube.matches` for any typed sticker answer. Corner sticker codes are genuinely ambiguous in
the wild (UBL vs ULB name the same sticker), so it compares the own-face exactly and the other
faces as a set. Never grade corner answers with string equality.

Known gap: it is a **3x3 model only**. Stage 6 (wings) and 7 (centres) will need a 4x4 model —
same facelet approach, 96 stickers. Budget a session for it before lesson 6.

## Open threads

- `misorientedBuffer` (buffer piece home but flipped) needs no special *tracing* rule, but M2
  *execution* does need one. Cover in lesson 3.
- Find a trustworthy written commutator source before lesson 2 — see Gaps in RESOURCES.md.

## `m2-trainer.html` — execution drill (designed by grilling, 8 decisions)

Built because setups were the sticking point and the learner learns motor skills by repetition.
The design was interrogated rather than assumed; the decisions and *why* they went that way:

1. **Scrambles are rigged toward weak algorithms.** A random solve touches only ~7.4 of 22
   targets, so `I`/`Q`/`S` (~4% each) would stay weak for weeks. `Solve.pick()` generates 40
   candidates and keeps the one leaning hardest on weak/stale algorithms — measured 60/60 hit a
   weak target versus ~30% by chance.
2. **The unit is the algorithm (22), not the setup (16)** — and resolving the pair rule is part
   of the drill, because that is what actually ruins solves.
3. **Memo entry optional at layer 1 only.** Gating haptic reps behind a two-minute trace throttles
   the repetition the tool exists for. Layers 2–4 require it.
4. **Weakness signal = peek is a miss, time is speed, plus a fumble key.** Without the fumble key
   a confidently-wrong rep registers as success — the same flattery bug the Speffz trainer had.
   **The same bug reappeared in layer 1** and the learner caught it: the moves are on screen the
   whole time there, so those reps were being logged as clean and promoting algorithms to "solid".
   Layer 1 reps are now `spoiled` — a distinct counter that is *exposure, not evidence*:
   counts as a rep, keeps the algorithm fresh for staleness, logs **no time**, and can never make
   one solid. `solid` means 3+ clean unaided reps with peeks no more frequent than successes.
   Verified: 50 spoiled reps leave an algorithm unsolid; 3 clean ones make it solid.
   Kept `spoiled` separate from `peeks` rather than folding them together — a peek means "I did
   not know it", exposure means "I was never asked". Collapsing them would misreport both.
5. **Layer 2 shows the evolving cube as a checksum.** The app is blind to the learner's hands; the
   on-screen state is what makes a fumble *observable* rather than guessed.
6. **No SRS.** Motor skill, not fact recall — weighting by weakness and staleness gives spacing's
   benefit without due dates fighting a solve-shaped session.
7. **`assets/solve.js` is shared with lesson 3** rather than duplicating the solve trainer.
8. **Counting shown at layer 1, hidden from layer 2.** In a real solve nobody tells you you are on
   shot 4, and the pair rule depends on knowing.

Learner's own addition: a **step-through debugger**, because "it's tough to say what's wrong from
the final state alone". `Solve.debug()` names the first unproductive shot and what it should have
been. Available on a failed memo, a failed solve, and from the round summary.

Keys: space advances, P peeks, **F retries a fumbled shot**, **B steps back one shot**.

Fumble and back are two different repairs and both were needed:
- **F** (fumble) does *not* advance — it stays on the shot, reveals the moves, and records the
  fumble only when you finally advance. Advancing after F logs no time, so a botched rep never
  counts as a fast one.
- **B** (back) rewinds a completed shot and hands over `M2.invertSeq(alg)` so the *cube* can be
  rewound too — the app is blind to your hands, so a rewind that only moves the app would
  desynchronise them. Also reachable from the round summary.
- `Solve.store.record()` returns a token and `rollback(token)` reverses it exactly (count, peeks,
  fumbles, times array, previous best). Without that, stepping back would leave a phantom rep in
  the stats and skew the weighting that picks scrambles.
