Files
OpenSquawk/shared/learn/variantSignature.ts
itsrubberduck 2c5c2d5239 feat(classroom): show the full curriculum and raise the mastery bar
The overview rendered four anonymous module tiles and half a page of empty
space, which undersold 63 existing lessons. It now stacks four module
sections that list every lesson as a chip with status and variant dots, and
the header states the real scope: 4 modules, 63 lessons, 262 variations.

Mastery moves from a flat two-variation counter to per-module thresholds
(3/4/4/5) backed by persisted variant signatures — a hash over each roll's
expected answers. That triples the practice runs to full mastery without
authoring a single new lesson, and fixes a dedup hole where the same
variation counted twice after a page reload.

Existing progress is preserved: `done` is sticky, so raising the bar never
revokes a check mark someone already earned.

Also fixes `yarn test`, which passed quoted globs Node 20 does not expand —
it exited 0 without running a single test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 20:08:55 +02:00

29 lines
810 B
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Lesson, Scenario } from './types'
/**
* Identifies a rolled scenario by what the pilot actually has to say.
*
* Two rolls that demand the same answers are the same exercise, no matter how
* the rest of the scenario differs, so mastery must not count them twice.
*/
export function lessonVariantSignature(lesson: Pick<Lesson, 'fields'>, scenario: Scenario): string {
const parts = lesson.fields.map(field => {
try {
return String(field.expected(scenario) ?? '')
} catch {
return ''
}
})
return fnv1a(`${parts.length}${parts.join('')}`)
}
function fnv1a(value: string): string {
let hash = 0x811c9dc5
for (let i = 0; i < value.length; i++) {
hash ^= value.charCodeAt(i)
hash = Math.imul(hash, 0x01000193)
}
return (hash >>> 0).toString(36)
}