# V9 Wonder & Recall → V10 Rail channel integration

**Audit date:** 2026-07-17

**Scope:** RA.6 source-first integration note for FSRS state, due-review
selection, active-recall answer write-back, and the review's ≤5s first-truth
recommendation.

## Integration decision

The Rail should reuse V9's real FSRS transition and mastery functions, but it
must add a player-scoped persistence and command boundary before exposing a
review micro-act:

```text
gated V9 lesson artifact + player FSRS trace snapshot
  → filter by persisted nextReviewAt
  → rank due concepts within the Rail micro-act budget
  → render the artifact's grounded retrieval prompt from cache
  → collect and ground a score in [0, 1]
  → applyRetrievalCheckpoint (real FSRS-v4)
  → atomically persist card + nextReviewAt + mastery + answer receipt
  → refresh Atlas mastery and the fading-star projection
```

The existing V9 packages are algorithm and view-model libraries. They do not
provide a durable per-learner review repository, a due-card query, a review
request/answer transport, an answer scorer, or a progressive/latency-enforced
consumer endpoint. Those are real integration seams, not reasons to replace the
shipped scheduler with a new one.

## Audited public API map

| Layer                   | Public entry points                                                                                                                                        | Input                                                                                    | Output / Rail role                                                                                                                                                                                                                  |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Mnemosyne FSRS core     | [`fsrsReview`, `fsrsRetrievability`, `FSRS_DEFAULT_PARAMETERS`](../libs/mnemosyne/core/src/index.ts)                                                       | FSRS card fields, a four-level `ReviewGrade`, parameters, and caller-supplied epoch time | The authoritative FSRS-v4 transition. It updates difficulty, stability, state, reps, lapses, interval, and exact `nextReviewAt`. Defaults use the published 17 weights, 0.9 requested retention, and a 36,500-day maximum interval. |
| Mnemosyne queue helpers | [`prioritizeReviews`, `calculateReviewLoad`](../libs/mnemosyne/core/src/memory-science.ts)                                                                 | Candidate card summaries plus an explicit time budget, or cards with next-due instants   | `prioritizeReviews` ranks urgency and caps count; it does **not** filter for due time. Its source default is 15 seconds per card, not 40. `calculateReviewLoad` forecasts counts but is not a repository query.                     |
| V9 FSRS binding         | [`ConceptTraceCard`, `newConceptCard`, `reviewConcept`, `conceptRetrievability`, `atForgettingFrontier`](../libs/v9/mnemosyne-glue/src/knowledge-trace.ts) | A concept trace and caller-supplied clock                                                | Binds a V9 concept to the shared FSRS engine and projects retrievability/mastery. `ConceptTraceCard` is a value object; it has no learner id, concept id, revision, or `nextReviewAt`.                                              |
| V9 answer transition    | [`retrievalScoreToGrade`, `applyRetrievalCheckpoint`, `buildAtlasMasteryMap`](../libs/v9/mnemosyne-glue/src/mastery-feedback.ts)                           | Concept id, current trace, numeric recall score in `[0,1]`, and answer instant           | Maps `0.95/0.75/0.5` thresholds to `easy/good/hard/again`, executes the real FSRS review, and returns the updated trace, mastery, interval, and next-review ISO instant. Invalid scores throw.                                      |
| Lesson contract         | [`V9LessonArtifactSchema`, `V9MnemosyneScheduleSchema`](../libs/contracts/src/v9/lesson.ts)                                                                | Forged lesson data                                                                       | Carries one gated retrieval prompt with a validated `claimRef`, plus checkpoint ref, interval, and next-review instant. It is immutable lesson content/schedule metadata, not mutable player FSRS state.                            |
| Prometheus forge        | [`forgeLesson`](../libs/v9/prometheus/src/pipeline.ts)                                                                                                     | Wonder, Atlas, writer/gates, optional input trace, and a numeric retrieval score         | Calls `applyRetrievalCheckpoint` during Stage 6 and bakes its schedule into the artifact. It awaits the complete forge and does not expose a progressive first-answer result. It is not the runtime answer command.                 |
| Artifact cache          | [`LessonCache`, `InMemoryLessonCache`, `compileLesson`](../libs/v9/prometheus/src/compile.ts)                                                              | Profile-class content cache key and immutable artifacts                                  | Reuses gated lesson content. The shipped implementation is process-local, and the cache lookup occurs at Stage 8 after earlier forge work, so it is not yet a cache-first consumer service.                                         |
| Consumer projections    | [`buildLessonPlayer`](../libs/v9/experience/src/lesson-player.ts), [`planLessonDelivery`](../libs/v9/chiron/src/lesson-delivery.ts)                        | Publishable V9 lesson artifact                                                           | Expose the artifact's retrieval prompt and refuse ungated lessons. Neither accepts an answer or updates scheduler state.                                                                                                            |
| Separate SM-2 path      | [`buildRetrievalCheckpoint`](../libs/v9/lesson-explorables/src/retrieval-checkpoint.ts)                                                                    | Lesson id, SM-2 ease/interval/repetition state, 0–5 grade, clock                         | Produces a real SM-2 checkpoint for the separate `lesson-explorables` assembly/gate path. It is not the FSRS player trace the Wonder & Recall Rail should update.                                                                   |

## Where FSRS state lives today

The complete live V9 trace shape is `ConceptTraceCard`:

```ts
interface ConceptTraceCard {
  difficulty: number;
  stability: number;
  state: 'New' | 'Learning' | 'Review' | 'Relearning';
  reps: number;
  lapses: number;
  lastReviewAt: number | null;
}
```

Today this state lives only in caller-owned values. Prometheus optionally
accepts one as `ForgeLessonInput.retrievalCard`; tests construct it in memory;
`applyRetrievalCheckpoint` returns a replacement value. No audited V9 or
Mnemosyne module durably associates that value with a learner and concept. The
`InMemoryLessonCache` stores immutable lesson artifacts, not player traces.

The Rail integration therefore needs a durable, player-scoped record at least
equivalent to:

```ts
interface WonderRecallTraceRecordV1 {
  learnerId: string;
  conceptId: V9ConceptId;
  lessonArtifactId: string;
  checkpointRef: string;
  card: ConceptTraceCard;
  nextReviewAt: string;
  mastery: MasteryLevel;
  revision: number;
  updatedAt: string;
}
```

`lessonArtifactId` and `checkpointRef` bind the mutable trace to the immutable,
gated prompt. `revision` is required for compare-and-swap answer commits. The
exact `nextReviewAt` returned by FSRS must be persisted alongside the reduced V9
trace because `ConceptTraceCard` itself omits it.

### Exact due-time rule

The due query must use `nextReviewAt <= now` before it calls
`prioritizeReviews`. The queue helper ranks every card it receives and has no
due-time field, so passing the entire learner deck would surface future cards.

`atForgettingFrontier` remains useful for the gentle fading-star projection,
especially for cards in `Review`, but it cannot be the only runtime due rule.
For an `again` transition, the shared FSRS state machine may return a same-day
interval and an exact `nextReviewAt`; the reduced `ConceptTraceCard` plus a
fresh retrievability calculation does not preserve that learning-step deadline.
The transition result's explicit instant is authoritative.

After due filtering, `prioritizeReviews` can rank mapped stable card ids within
an explicit Rail budget. The implementation defaults to **15 seconds per card**.
No audited V9/Mnemosyne source implements the ambient specification's 40-second
figure. The later adapter task should pass its own measured estimate and cap one
micro-act to ≤30 seconds rather than relying on either prose value.

## How a review is requested

There is no current `ReviewRequest` API. The available pieces establish the
following honest request path:

1. Read the learner's trace records whose persisted `nextReviewAt` is due.
2. Rank only those records with `prioritizeReviews` and a caller-supplied
   duration estimate.
3. Load each bound `V9LessonArtifact` from durable/cache storage.
4. Require `isV9LessonPublishable(artifact)` before delivery.
5. Resolve `artifact.assessment.retrievalCheck`; its contract-validated
   `claimRef` must index `artifact.groundTruth.claims`.
6. Return a player-scoped request id, concept/artifact/checkpoint bindings,
   expected trace revision, prompt, and expiry. Do not include the referenced
   truth in the pre-answer tile payload.

A cold-cache-safe request needs enough immutable context to replay exactly the
same card. A generic "latest review" deep link would allow the prompt and trace
revision to drift between the glance and the micro-act.

## How an answer is applied

There is no current answer transport or free-text scoring API. The only live V9
scheduler input is a numeric recall score in `[0,1]`. The adapter must not
silently turn arbitrary prose into a plausible score.

An answer command should:

1. validate learner, request id, concept id, artifact id, checkpoint ref,
   expected revision, answer instant, and one-use/idempotency key;
2. evaluate the response through an explicit grounded scorer bound to the
   artifact claim, or through a separately product-approved explicit self-grade
   action—never a hidden heuristic;
3. reject non-finite or out-of-range scores before the scheduler call;
4. call `applyRetrievalCheckpoint(conceptId, current.card, score, answeredAt)`;
5. atomically compare-and-swap the prior revision to the returned card,
   `nextReviewAtIso`, and mastery; and
6. retain an idempotent answer receipt so a retry cannot increment `reps` or
   `lapses` twice.

The updated trace set can then flow through `buildAtlasMasteryMap` for future
wonder scoping. The immutable lesson artifact must not be rewritten with
player-specific state.

Concurrent/stale answers must fail closed. Recomputing from a stale card and
blindly overwriting would lose a review transition; replaying the same answer
would advance the FSRS state twice.

## FSRS and SM-2 representation boundary

V9 contains two real spaced-repetition paths:

- the live concept mastery loop uses FSRS through `@oshun/v9-mnemosyne-glue`;
  and
- `@oshun/v9-lesson-explorables` creates an SM-2 checkpoint for its separate
  assembled `V9Lesson` release-gate path.

The current Prometheus `V9LessonArtifact` forge uses the **FSRS** checkpoint
path, despite older feature prose describing the artifact schedule as SM-2. The
Rail must follow executable source: update `ConceptTraceCard` via
`applyRetrievalCheckpoint`. It must not cast the SM-2
`{ easeFactor, interval, repetitions }` record into FSRS fields, and it must not
treat a forged artifact's schedule as a shared mutable player record.

Prometheus currently asks for `retrievalScore` while forging the lesson, before
the runtime learner sees its retrieval prompt. That is a content-generation
schedule input, not evidence that a user answer endpoint exists. Rail answers
belong in the new atomic trace store and should not rerun `forgeLesson`.

## The ≤5s first-truth SLO

The original recommendation in
[`V9_PRODUCT_REVIEW_2026-07-07.md` §4.1](../V9/V9_PRODUCT_REVIEW_2026-07-07.md)
is for the **ask-a-wonder front door**: render a grounded, pinned first answer
within five seconds, then progressively assemble the richer lesson. It is not a
shipped scheduler guarantee and is not currently measured in code.

Three source facts matter:

1. `forgeLesson` returns only after resolve, ground, plan, write, explorable,
   schedule, gates, compile, and cache; it has no partial grounded-answer event.
2. `InMemoryLessonCache` is neither durable nor consulted before those earlier
   forge stages.
3. `buildLessonPlayer`/`planLessonDelivery` synchronously project an already
   available gated artifact, but no consumer endpoint measures activation to
   first truth.

For Wonder & Recall, the ambient specification deliberately adopts the same
number for a narrower cached path: **Rail activation → first grounded,
reviewable prompt rendered**. The implementation should measure
`firstCardRenderedAt - activatedAt < 5_000 ms`, including trace/artifact cache
read and render. It should not include answer scoring or write-back, which occur
after first truth, and it must never invoke a novel lesson forge on this
critical path.

The prompt is truthful only when it comes from a publishable artifact and its
validated claim binding. A fast invented question or unverified stale prompt
does not satisfy the SLO. If the trace or artifact is unavailable, the channel
should expose an honest unavailable/empty state rather than minting content
inside the five-second budget.

## Required Rail-owned ports

The subsequent adapter can keep scheduler math in V9 and make infrastructure
ownership explicit with narrow ports:

```ts
interface WonderRecallTraceStore {
  readDue(
    learnerId: string,
    at: string,
    limit: number
  ): Promise<WonderRecallTraceRecordV1[]>;
  commitAnswer(
    command: AtomicWonderRecallAnswer
  ): Promise<CommittedWonderRecallAnswer>;
}

interface GatedLessonArtifactReader {
  readPublishable(artifactId: string): Promise<V9LessonArtifact | null>;
}

interface GroundedRecallScorer {
  score(input: BoundRecallAnswer): Promise<number>;
}
```

The store owns isolation, revisions, and idempotency. The artifact reader owns
durable/cache lookup and publishability enforcement. The scorer owns how a human
response becomes the numeric scheduler input. None of these ports may replace
`applyRetrievalCheckpoint`; they prepare and persist its real result.

## Verification seams for the adapter tasks

The next checkboxes should prove these source-derived invariants:

- a known FSRS card/score/clock round-trip matches the pinned Mnemosyne outputs,
  persists the returned exact due instant, and changes that instant after the
  answer;
- stale and replayed answer commands do not advance the card;
- future cards are filtered before urgency ranking, while due cards respect the
  Rail's ≤30-second micro-act cap;
- prompt delivery refuses missing, ungated, mismatched, or changed artifacts and
  never leaks the bound truth before answer;
- the player tile uses retrievability only as non-punitive fading and absence
  never mutates or deletes trace state; and
- desktop/mobile browser automation measures cached activation-to-card render
  under 5,000 ms, with the scorer/write path outside that measurement.
