A V9 lesson is not supposed to dead-end. Once Prometheus has forged a gated
V9LessonArtifact and the learner has played it, three subsystems decide what
happens after the last beat — and they are the difference between a one-shot
explainer video and a guide that carries a curious ape across days. Theia
turns the answer into the next, better question and offers the thread forward
(Olbers' paradox → the age of the universe → the cosmic microwave background →
the Big Bang). The experience layer renders the learner's progress as a
literal map of reality — concepts they have lit up shown as brightness,
everything still dark shown as an inviting next star — so progress reads as a
star map, not a progress bar. And the Mnemosyne glue schedules spaced
retrieval at the moment recall is about to fail, projects each concept's memory
strength onto a MasteryLevel, and feeds that mastery back into the very
wonder-scoping that picks the next lesson. The product optimizes for durable
understanding weeks later, not session length.
The reason all three are buildable rather than aspirational is the decision that
runs through every V9 subsystem: compose the real engines, reinvent nothing.
Theia walks the same AtlasStore graph that wonder-resolution uses
(@oshun/v9-atlas); the star-map reads the same six-rung MasteryLevel ladder
(@mnemosyne/core) that the rest of the platform's learning science uses; and
the mastery loop delegates every schedule to the shipped FSRS-v4, SM-2, and IRT
engines in libs/mnemosyne/core. The V9 packages on this page are binding
glue, not new psychometrics — the header of
libs/v9/mnemosyne-glue/src/index.ts says so plainly: "Reuses the real
Mnemosyne FSRS/IRT engines — no new psychometrics." This page is the
feature-side companion for the back half of the pipeline; the hub for the set is
../V9_features.md.
What ships, honestly#
The logic and data for all three subsystems are real and unit-tested with
known-value assertions, not shape checks. Theia's thread continuation, emotional
arc, and surprise-me (libs/v9/theia/src/theia.ts, tested in theia.spec.ts);
the personal Atlas star-map (libs/v9/experience/src/mastery-map.ts, tested in
experience.spec.ts); the FSRS knowledge-trace, the active-recall checkpoint,
and the flow-channel tuner (libs/v9/mnemosyne-glue/src/*, tested in
mnemosyne-glue.spec.ts); and the per-learner IRT difficulty + SM-2 retrieval
checkpoint (libs/v9/lesson-explorables/src/*, tested in their *.spec.ts) all
exist and assert specific numbers — FSRS initial stabilities pinned to 0.4072
and 15.4722, the Rasch calibration identity asserted to six decimals, the SM-2
1 → 6 → 16 interval ladder. A genuine constant-stub would fail these.
Two honest qualifications. First, there is no rendered V9 star-map screen in
the repo. buildPersonalAtlasMap and buildWonderFrontDoor are view-models —
the tested data a screen would bind to — but a grep of apps/ finds no V9
surface importing @oshun/v9-experience or @oshun/v9-theia (the
StarMapCanvas.tsx that exists belongs to Nyx mobile, a different domain).
Treat the star map as a shipped, tested data model whose UI is not yet in-repo.
Second, the emotional arc (orchestrateEmotionalArc) is a deterministic
keyword-and-position labeler over a lesson's beats, not an affect model — real
and predictable, but it classifies text it is given, it does not sense
emotion. Where the monolith promises verifiable credentials, that promise is
backed: libs/v9/cross-cutting/src/lms-interop.ts ships a real
toOpenBadgeCredential emitting an Open Badges 3.0 verifiable credential — but
it lives in the cross-cutting interop module, covered by
./governance-and-boundaries.md, not in the
mastery glue.
Threads — Theia, the wonder director#
libs/v9/theia/src/theia.ts is the "wonder director." It does three jobs over
the real Atlas graph and the learner's mastery state, and it owns no graph store
of its own — it reads AtlasStore (@oshun/v9-atlas) and the
Map<V9ConceptId, MasteryLevel> the mastery loop produces.
Thread continuation — the next, better question#
nextWonders(atlas, from, max = 3) walks the concept edges leaving a node and
returns the questions it opens, in a fixed priority order of edge type:
const THREAD_EDGE_PRIORITY = ['enables', 'bridges', 'specializes', 'related'];
These are four of the nine edge types in the V9ConceptEdgeTypeSchema enum
(libs/contracts/src/v9/concept-graph.ts:
prerequisite, related, part_of, generalizes, specializes, enables, conflicts, complements, bridges).
The order is the editorial claim that an enables edge ("you can now
understand…") is a better thread than a merely related one, and that the
cross-axis bridges edge — V9's own addition, joining e.g. cosmology to
deep-time — is worth surfacing above a same-axis specialization. The function
dedups by target and slices to max, and it throws on an unknown source
concept rather than returning an empty thread
(if (!atlas.hasNode(from)) throw). The theia.spec.ts case proves the
ordering: with both an enables edge to "Age of the universe" and a related
edge to "Cosmic microwave background", the first returned wonder is the
enables target. These same concept ids are what the forge bakes into the
artifact's thread.nextWonders (libs/contracts/src/v9/lesson.ts), which the
lesson player surfaces as the end-of-lesson thread.
The emotional arc#
orchestrateEmotionalArc(beats) tags each narrative beat with one of four
emotions — awe | curiosity | understanding | anticipation — to shape the
science↔human arc the product analysis asks for (awe → curiosity →
understanding → the next question). It is a transparent rule, not a model: the
first beat (or one containing "hook") is awe; a beat mentioning "why it
matters" / "connect" is understanding; a beat about "the next question" — or
the final beat — is anticipation; everything else is curiosity. The test
feeds it a three-beat lesson and asserts awe → understanding → anticipation.
Because it is deterministic, the same lesson always arcs the same way, which is
what lets the rendered player rely on it; because it is keyword-driven, it is
honest about being a labeler over beats Prometheus already wrote.
"Surprise me" — the frontier of what you almost know#
surpriseMe(atlas, masteryMap, max = 1) answers a different question: not
"where next from here?" but "what unexplored idea is the learner most ready
for?" For every node not yet mastered, it computes a readiness = the
fraction of that concept's transitive prerequisites the learner has already
mastered, keeps those in (0, 1], and ranks by readiness descending. "Mastered"
here is ≥ intermediate on the MASTERY_LEVELS ladder
(MASTERED_FLOOR = indexOf('intermediate')), and the prerequisite set comes
from the real Mnemosyne closure via atlas.prerequisitesOf — so a "surprise" is
never a random concept, it is the highest-readiness star just past the learner's
frontier. The test mastering "Light" surfaces "Olbers paradox" at readiness
1.0.
Note one honest duplication: the experience layer's front door has its own frontier suggester,
suggestFrontierinlibs/v9/experience/src/wonder-front-door.ts, which proposes concepts adjacent to the current teaching set. It is not the same function assurpriseMe(adjacency vs. readiness-ranking); the two serve different entry points (a just-resolved wonder vs. an open "surprise me" button) and are not yet unified.
The personal Atlas — progress as a star map#
buildPersonalAtlasMap(atlas, masteryMap) in
libs/v9/experience/src/mastery-map.ts is the "what you know / what's next"
view-model. It maps every Atlas node to a StarMapNode with three signals that
make "star map, not progress bar" concrete:
brightness(0–1) — mastery rendered as light.brightnessOf(level) = (indexOf(level) + 1) / MASTERY_LEVELS.length, sonovice≈0.17,intermediate=0.5, andmaster=1.0; an unreviewed concept is0(dark). The six rungs arenovice, beginner, intermediate, advanced, expert, master(libs/mnemosyne/core/src/types.ts).explored—truefor any concept with any mastery entry, evennovice. Note the deliberate asymmetry: a concept you have merely touched is "explored" (and faintly bright), but it does not count toward another concept's readiness until it reaches≥ intermediate.frontier— a still-dark concept whose every prerequisite is already mastered: the inviting next star to light.!explored && prereqs.length > 0 && prereqs.every(isMastered).
The map also rolls up totalCount, exploredCount, and frontierCount. The
experience.spec.ts worked example builds a two-node Atlas — "Cosmology basics"
→ (prerequisite) → "Age of the universe" — masters only the basics, and asserts:
basics is explored with brightness === 1; "Age of the universe" is
unexplored with brightness === 0 but frontier === true, because its only
prerequisite is now mastered. That is the whole product metaphor in one
assertion: mastering one star lights the next as an invitation.
The mastery loop — remembering forever#
The mastery loop is where "remember it weeks later" is actually engineered. It has four moving parts, and a subtlety worth stating up front: V9 uses two different spaced-repetition engines for two different jobs.
| Engine | Where | Job |
|---|---|---|
| FSRS-v4 | libs/v9/mnemosyne-glue/src/knowledge-trace.ts |
The live mastery loop: stability, the forgetting frontier, and the MasteryLevel fed back to Atlas |
| SM-2 | libs/v9/lesson-explorables/src/retrieval-checkpoint.ts |
The durable schedule.checkpointRef the forge bakes into the artifact (the G7 gate reads it) |
Both are real and tested; they are simply not the same algorithm. The artifact's
schedule block (checkpointRef, intervalDays, nextReviewAtIso in
libs/contracts/src/v9/lesson.ts) is minted by the SM-2 path at forge time; the
re-review-over-days loop below runs on FSRS. If you read only one of the two
files you will think V9 "has spaced repetition" and miss that it has two seams.
1. The knowledge trace (FSRS forgetting frontier)#
A taught concept becomes an FSRS card — ConceptTraceCard =
{ difficulty, stability, state, reps, lapses, lastReviewAt }.
reviewConcept(card, grade, now) delegates straight to fsrsReview
(libs/mnemosyne/core/src/memory-science.ts) using the published FSRS-v4
default weights (FSRS_DEFAULT_PARAMETERS.w, a 17-element vector with
requestRetention: 0.9). conceptRetrievability applies the FSRS forgetting
curve R = (1 + elapsedDays / (9·stability))^-1, and
atForgettingFrontier(card, now, requestRetention = 0.9) is the scheduling
decision: a New card is always due, and a reviewed card is due once its recall
probability has decayed to or below the requested retention — recall scheduled
for the moment it is hardest-but-possible, which is the testing effect the
product leans on. The trace test pins the engine's honesty: stabilities are
strictly monotonic in grade (again < hard < good < easy) and equal 0.4072
and 15.4722 for again/easy — exactly w[0] and w[3].
conceptMastery(card) then projects the FSRS state onto a MasteryLevel by
stability in days: New → novice; Learning/Relearning → beginner; in
Review, < 7d → beginner, < 30d → intermediate, < 180d → advanced,
< 365d → expert, else master. A concept retained for over a year is
master; one you saw once is novice. This is the single number every other
subsystem on this page reads.
2. The active-recall checkpoint#
applyRetrievalCheckpoint(conceptId, card, recallScore, now) in
mastery-feedback.ts is the loop's pump. It insists the checkpoint is active
recall — a question the learner answers — not a re-read: a [0,1] recall score
maps to an FSRS grade (retrievalScoreToGrade: ≥ 0.95 → easy,
≥ 0.75 → good, ≥ 0.5 → hard, else again; throws outside [0,1]),
drives a real FSRS review, and returns the updated card, the new MasteryLevel,
the next-review ISO timestamp, and the interval in days. buildAtlasMasteryMap
folds a learner's whole set of traces into the Map<V9ConceptId, MasteryLevel>
that resolveWonder({ learnerMastery }) consumes — and that is the feedback
edge: mastered concepts drop out of the prerequisite frontier, so the next
wonder is scoped to what the learner still needs.
3. Adaptive difficulty in the flow channel#
Keeping the next item in Csíkszentmihályi's flow channel — challenge matched
to skill, so neither boredom nor anxiety — is done with IRT, not heuristics.
flow-channel.ts defines a FlowBand of success probability
{ lower: 0.6, upper: 0.85 }: flowChannelFor(theta, difficultyLogit) computes
the success probability with the real irt2PL and classifies
> 0.85 → boredom, < 0.6 → anxiety, else flow.
tuneDifficultyToFlow(history) picks the next item's difficulty: it estimates
the learner's ability θ by reusing Mnemosyne's Newton-Raphson MLE
(estimateAbility, via libs/v9/lesson-explorables/src/adaptive-difficulty.ts
computeAdaptiveDifficulty), calibrates an item to a target success probability
(default 0.75, which must lie strictly inside the flow band or the function
throws), and then verifies the recommended item lands in flow. The
calibration is an exact identity, not an approximation: b* = θ − logit(p*),
and the adaptive-difficulty test asserts irt1PL(θ, b*) ≈ targetSuccess to six
decimal places. A stronger learner gets a harder next item; targeting 90%
success yields easier items than targeting 60% — both asserted.
4. The loop closes#
Put together, one lesson's checkpoint updates a trace, the trace projects to
mastery, mastery rewrites the Atlas map, and the rewritten map both re-scopes
the next wonder (fewer prerequisite gaps) and re-lights the star map (a new
frontier star). Difficulty tuning keeps the next item in flow. The same
MasteryLevel feeds Theia's surpriseMe so the "next star" is always the
readiest unexplored one.
How it reaches the screen — the lesson player#
The consumer-facing binding for the back half is
libs/v9/experience/src/lesson-player.ts. buildLessonPlayer(artifact)
projects a gated V9LessonArtifact into player state and refuses an ungated
lesson — if (!isV9LessonPublishable(artifact)) throw new UngatedLessonError,
proven by the test that flips G2 to failing and expects the throw. Two of its
fields belong to this page directly: retrievalPrompt (from
assessment.retrievalCheck.prompt) is the active-recall question the checkpoint
scores, and nextWonders (from thread.nextWonders) is the thread Theia
authored. So the same continuation concepts nextWonders() computes at forge
time are the ones the learner taps at the end of the lesson; the same
retrievalCheck the gates verified is the question that later pumps the mastery
loop.
Edge cases, failure modes, and configuration#
- Fail-loud, never fabricate.
nextWonders/surpriseMe/prerequisitesOfthrow on an unknown concept id;retrievalScoreToGradethrows outside[0,1];tuneDifficultyToFlowthrows when the target success sits outside the flow band;computeAdaptiveDifficultyrejects a target success≤ 0or≥ 1. None of these returns a plausible-but-invented value. - Cold start. A brand-new concept card is
New,novice, and always at the forgetting frontier (atForgettingFrontierreturnstrueforNew). An empty response history is a warm start:computeAdaptiveDifficulty([], { priorTheta: 0 })returns θ = 0 and an adaptive difficulty ofσ(−logit(0.8)) ≈ 0.2. - The "explored but not mastering" gap. Because
exploredtriggers on any level butisMasteredrequires≥ intermediate, a faintly-litnoviceconcept will not unlock its dependents as frontier stars. This is intended — touching a concept is not the same as being able to build on it — but it is a real behavior to know when reading the map. - Two clocks, supplied not invented. Every scheduler takes
nowUnixMsfrom the caller (reviewConcept,applyRetrievalCheckpoint,buildRetrievalCheckpoint), so next-review dates are deterministic and testable against a fixed clock — the SM-2 test pinscheckpointRefto an exact string for a fixedNOW. - Tunable knobs. The flow band
{ 0.6, 0.85 }and target success0.75(flow) /0.8(calibration) are defaults you can override per call; the FSRSrequestRetention(0.9) governs how aggressively reviews are scheduled; theMASTERED_FLOORofintermediategoverns frontier/readiness, andresolveWonder'smasteryFlooroption (defaultintermediate) governs which prerequisites count as cleared.
How this connects to the rest of V9#
- Atlas & wonder resolution. The mastery map is the input to
resolveWonder's mastery-filtered prerequisite frontier, andprerequisitesOfdelegates to the MnemosyneKnowledgeGraph.getPrerequisitestransitive closure overprerequisiteedges only. See ./atlas-wonder-resolution.md. - Prometheus. The forge writes
thread.nextWonders,assessment.masterySignal, and the SM-2schedule.checkpointRefinto the artifact this page's runtime then re-reads. See ./prometheus-lesson-forge.md and the gate mechanics in ./subsystem-map-and-gates.md. - Chiron & Hephaestus. The emotional arc and thread sit on top of Chiron's teacher persona and Hephaestus's explorables; Chiron's cross-session memory opens on what the learner struggled with last time, reading the same mastery signal. See ./chiron-and-hephaestus.md.
- The braid & the commons. Theia is also the orchestrator of the science↔human braid, and the "surprise me" / thread objects become shareable in Agora. See ./braid-commons-and-films.md.
- Governance. The free-tier quota that decides whether the next lesson is
even served (
checkBillingAccess) and the Open Badges credential that attests mastery both live in the governance/cross-cutting surface. See ./governance-and-boundaries.md.
Related pages#
- ./overview.md — the product and the five nested primitives
- ./subsystem-map-and-gates.md — the ownership ledger and the seven-gate mechanics
- ./atlas-wonder-resolution.md — the concept graph and the mastery-filtered wonder frontier this loop feeds
- ./prometheus-lesson-forge.md — the forge that bakes the thread, mastery signal, and checkpoint into the artifact
- ./chiron-and-hephaestus.md — the teacher and the explorable workshop
- ./braid-commons-and-films.md — the science↔human braid, Agora, and explainer films
- ./governance-and-boundaries.md — entitlements, credentials, the anti-metric, and the non-goals
- Hub: ../V9_features.md · Architecture spec: ../V9_ARCHITECTURE.md · Product thesis: ../V9_PRODUCT_ANALYSIS.md