mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-11 20:15:32 +08:00
docs(classroom): design for raising classroom production value
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
142
docs/plans/2026-07-30-classroom-production-value-design.md
Normal file
142
docs/plans/2026-07-30-classroom-production-value-design.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# Classroom Production Value — Design
|
||||||
|
|
||||||
|
Date: 2026-07-30
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The Classroom overview at `/classroom` renders four module cards and roughly
|
||||||
|
half a page of empty space below them. The cards expose no lesson titles, so the
|
||||||
|
curriculum looks like four items with a handful of exercises each. The product
|
||||||
|
sells for 30 €; the page undersells what is already built.
|
||||||
|
|
||||||
|
The curriculum actually holds **63 lessons across 4 modules**
|
||||||
|
(`shared/data/learnModules.ts`). Every lesson carries a `generate()` function
|
||||||
|
that rolls a fresh scenario from `randInt`/`sample`, so the number of distinct
|
||||||
|
practice runs is effectively unbounded. Mastery currently requires only two
|
||||||
|
clean runs (`CLASSROOM_VARIANTS_FOR_MASTERY = 2`), which caps the whole
|
||||||
|
curriculum at 126 runs.
|
||||||
|
|
||||||
|
Two levers follow, neither of which requires authoring new lesson content:
|
||||||
|
|
||||||
|
1. Require more rolled variants per lesson, and track which ones were passed.
|
||||||
|
2. Show the 63 lessons on the overview instead of four anonymous cards.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Presentation first. No new lessons are authored in this work.
|
||||||
|
- Variant thresholds are staggered per module, not one global constant.
|
||||||
|
- Passed variants are stored as signatures, not just a counter.
|
||||||
|
- Existing progress is preserved. A lesson already marked `done` keeps its
|
||||||
|
check mark; the raised threshold shows up as a new goal beside it, never as a
|
||||||
|
regression.
|
||||||
|
|
||||||
|
## Part 1 — Variant mastery
|
||||||
|
|
||||||
|
### Variant signature
|
||||||
|
|
||||||
|
A lesson defines `fields[].expected(scenario)` (`shared/learn/types.ts:145`).
|
||||||
|
The signature of a roll is a short hash over exactly those expected answer
|
||||||
|
values, joined in field order. Two rolls that demand the same spoken answer are
|
||||||
|
the same exercise regardless of unrelated scenario differences, so this needs no
|
||||||
|
per-lesson configuration.
|
||||||
|
|
||||||
|
Encoding: FNV-1a over the joined string, base36, ~8 characters. At five variants
|
||||||
|
across 63 lessons that is roughly 3 KB per profile — irrelevant next to the
|
||||||
|
existing `Mixed` progress blob in `server/models/LearnProfile.ts`.
|
||||||
|
|
||||||
|
### Data model
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// shared/learn/config.ts
|
||||||
|
export interface LessonProgress {
|
||||||
|
best: number
|
||||||
|
done: boolean
|
||||||
|
assessmentVersion?: number
|
||||||
|
successfulVariants?: number // retained; stays the displayed count
|
||||||
|
variantSignatures?: string[] // new
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CLASSROOM_VARIANTS_BY_MODULE: Record<string, number> = {
|
||||||
|
'normalize': 3, // Foundations, 7 lessons
|
||||||
|
'arc': 4, // Mandatory Readbacks, 22 lessons
|
||||||
|
'decision-tree': 4, // Pilot Calls & Abnormal Situations, 17 lessons
|
||||||
|
'full-flight': 5, // Guided Flight Sequence, 17 lessons
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CLASSROOM_VARIANTS_DEFAULT = 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Short drills need less repetition than full scenarios, which is why the
|
||||||
|
threshold rises with module complexity. Total runs to full mastery:
|
||||||
|
7×3 + 22×4 + 17×4 + 17×5 = **262**, up from 126.
|
||||||
|
|
||||||
|
`CLASSROOM_VARIANTS_FOR_MASTERY` is replaced by a lookup that falls back to
|
||||||
|
`CLASSROOM_VARIANTS_DEFAULT` for unknown module ids.
|
||||||
|
|
||||||
|
### Assessment change
|
||||||
|
|
||||||
|
`updateMasteryProgress` (`shared/learn/assessment.ts:43`) takes the current
|
||||||
|
roll's signature and the module threshold instead of the
|
||||||
|
`variantAlreadyCounted` boolean:
|
||||||
|
|
||||||
|
- A pass counts when the model answer was not revealed **and** the signature is
|
||||||
|
absent from `variantSignatures`.
|
||||||
|
- On count, the signature is appended and `successfulVariants` is set to
|
||||||
|
`max(previous, variantSignatures.length)`.
|
||||||
|
- `done` becomes `successfulVariants >= threshold`.
|
||||||
|
|
||||||
|
This also fixes a live defect: `variantCounted` is a session ref reset on every
|
||||||
|
roll (`app/pages/classroom.vue:3873`), so today the same variant can be counted
|
||||||
|
again after a page reload. Signatures persist, so dedup now survives reloads.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
`CLASSROOM_ASSESSMENT_VERSION` stays at 2 — bumping it would wipe paying
|
||||||
|
customers' progress. Legacy entries have `successfulVariants` between 0 and 2
|
||||||
|
and no signatures. They are read as-is: the counter is authoritative until
|
||||||
|
signatures overtake it, and `isCurrentMastery` keeps returning true for anything
|
||||||
|
already `done`. A previously mastered lesson therefore keeps its check mark and
|
||||||
|
additionally shows "2 of 4 variations" as the new goal.
|
||||||
|
|
||||||
|
## Part 2 — Overview shows the curriculum
|
||||||
|
|
||||||
|
Four cards side by side cannot carry 22 lessons underneath. The overview becomes
|
||||||
|
four stacked module sections:
|
||||||
|
|
||||||
|
- **Section header** — artwork as a narrow band on the left, title and subtitle,
|
||||||
|
progress bar, "Continue" button on the right.
|
||||||
|
- **Lesson grid** — every lesson of the module as a compact chip: title, status
|
||||||
|
icon, variant dots (●●●○○ against the module threshold), best score.
|
||||||
|
- Clicking a chip opens that lesson directly.
|
||||||
|
|
||||||
|
That puts 63 concrete entries on the page. A buyer reads "ATIS", "METAR",
|
||||||
|
"Go-Around", "TCAS RA", "Mayday Vector" instead of four unlabelled tiles, and
|
||||||
|
the empty lower half is gone.
|
||||||
|
|
||||||
|
The existing lesson search stays as-is; it now duplicates a browsing path rather
|
||||||
|
than being the only way to discover a lesson title.
|
||||||
|
|
||||||
|
## Part 3 — Scope figures in the header
|
||||||
|
|
||||||
|
Below the headline, one line of figures derived from the data, not from
|
||||||
|
marketing: **4 modules · 63 lessons · 262 practice variations**, plus overall
|
||||||
|
progress. All three numbers are computed from `learnModules` and
|
||||||
|
`CLASSROOM_VARIANTS_BY_MODULE` so they cannot drift from reality.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- `tests/shared/classroomCurriculum.test.ts` covers the counter today. Extend
|
||||||
|
with: identical signature does not count twice; distinct signatures accumulate;
|
||||||
|
a legacy entry without signatures keeps `done`; module thresholds resolve, and
|
||||||
|
unknown ids fall back to the default.
|
||||||
|
- Add a signature-stability test: the same scenario hashed twice is equal, and
|
||||||
|
two rolls with different expected answers differ.
|
||||||
|
- `yarn typecheck` must pass; `LessonProgress` gains an optional field, so no
|
||||||
|
call site breaks.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- New lessons or modules.
|
||||||
|
- Streaks, activity history, or time-based statistics — `LessonProgress` has no
|
||||||
|
timestamps, and adding them is a separate piece of work.
|
||||||
|
- XP, which is already marked legacy in `shared/learn/config.ts`.
|
||||||
Reference in New Issue
Block a user