# Threads, the Personal Atlas, and the Mnemosyne Mastery Loop

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](../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](./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:

```ts
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, `suggestFrontier` in
> `libs/v9/experience/src/wonder-front-door.ts`, which proposes concepts
> _adjacent to the current teaching set_. It is not the same function as
> `surpriseMe` (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`, so
  `novice` ≈ `0.17`, `intermediate` = `0.5`, and `master` = `1.0`; an unreviewed
  concept is `0` (dark). The six rungs are
  `novice, beginner, intermediate, advanced, expert, master`
  (`libs/mnemosyne/core/src/types.ts`).
- **`explored`** — `true` for any concept with _any_ mastery entry, even
  `novice`. 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.

```mermaid
flowchart TD
  L[Lesson played] --> RC["Active-recall checkpoint<br/>applyRetrievalCheckpoint"]
  RC -->|"recall score → grade"| FSRS["FSRS review (fsrsReview)<br/>stability · forgetting frontier"]
  FSRS --> M["conceptMastery → MasteryLevel"]
  M --> MAP["buildAtlasMasteryMap<br/>Map&lt;conceptId, MasteryLevel&gt;"]
  MAP --> RW["resolveWonder(learnerMastery)<br/>frontier shrinks"]
  MAP --> SM["Theia surpriseMe<br/>readiest unexplored star"]
  MAP --> STAR["buildPersonalAtlasMap<br/>brightness · frontier"]
  RW --> NEXT[Next lesson scoped]
  SM --> NEXT
  HIST["Response history"] --> FLOW["tuneDifficultyToFlow (IRT)<br/>next item in flow band"]
  FLOW --> NEXT
  NEXT --> L
```

## 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`/`prerequisitesOf`
  throw on an unknown concept id; `retrievalScoreToGrade` throws outside
  `[0,1]`; `tuneDifficultyToFlow` throws when the target success sits outside
  the flow band; `computeAdaptiveDifficulty` rejects a target success `≤ 0` or
  `≥ 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 (`atForgettingFrontier` returns `true` for `New`). 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 `explored` triggers on any
  level but `isMastered` requires `≥ intermediate`, a faintly-lit `novice`
  concept 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 `nowUnixMs` from
  the caller (`reviewConcept`, `applyRetrievalCheckpoint`,
  `buildRetrievalCheckpoint`), so next-review dates are deterministic and
  testable against a fixed clock — the SM-2 test pins `checkpointRef` to an
  exact string for a fixed `NOW`.
- **Tunable knobs.** The flow band `{ 0.6, 0.85 }` and target success `0.75`
  (flow) / `0.8` (calibration) are defaults you can override per call; the FSRS
  `requestRetention` (`0.9`) governs how aggressively reviews are scheduled; the
  `MASTERED_FLOOR` of `intermediate` governs frontier/readiness, and
  `resolveWonder`'s `masteryFloor` option (default `intermediate`) 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, and
  `prerequisitesOf` delegates to the Mnemosyne `KnowledgeGraph.getPrerequisites`
  transitive closure over `prerequisite` edges only. See
  [./atlas-wonder-resolution.md](./atlas-wonder-resolution.md).
- **Prometheus.** The forge writes `thread.nextWonders`,
  `assessment.masterySignal`, and the SM-2 `schedule.checkpointRef` into the
  artifact this page's runtime then re-reads. See
  [./prometheus-lesson-forge.md](./prometheus-lesson-forge.md) and the gate
  mechanics in [./subsystem-map-and-gates.md](./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](./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](./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](./governance-and-boundaries.md).

## Related pages

- [./overview.md](./overview.md) — the product and the five nested primitives
- [./subsystem-map-and-gates.md](./subsystem-map-and-gates.md) — the ownership
  ledger and the seven-gate mechanics
- [./atlas-wonder-resolution.md](./atlas-wonder-resolution.md) — the concept
  graph and the mastery-filtered wonder frontier this loop feeds
- [./prometheus-lesson-forge.md](./prometheus-lesson-forge.md) — the forge that
  bakes the thread, mastery signal, and checkpoint into the artifact
- [./chiron-and-hephaestus.md](./chiron-and-hephaestus.md) — the teacher and the
  explorable workshop
- [./braid-commons-and-films.md](./braid-commons-and-films.md) — the
  science↔human braid, Agora, and explainer films
- [./governance-and-boundaries.md](./governance-and-boundaries.md) —
  entitlements, credentials, the anti-metric, and the non-goals
- Hub: [../V9_features.md](../V9_features.md) · Architecture spec:
  [../V9_ARCHITECTURE.md](../V9_ARCHITECTURE.md) · Product thesis:
  [../V9_PRODUCT_ANALYSIS.md](../V9_PRODUCT_ANALYSIS.md)
