feat(tutor): re-derivable FSRS schedule + per-learner difficulty fit#4
Merged
Conversation
Add fsrsderive.mjs: reconstruct the FSRS scheduling state purely from the witnessed graded attempt log and audit the cached itemState hint against it. - deriveItemStates(attempts): replays the recorded scored attempts (grade + timestamp) through the same pure gradeAttempt math, reproducing itemState bit-for-bit from session.attempts alone. - deriveScheduleReceipt(session): MATCH / DRIFT / NO_FSRS_LOG verdict plus a hash-chained ledger; the log-derived state is authoritative, so a stale or tampered cache is caught as DRIFT with a per-field diff, never trusted. - optimizeParameters(attempts): advisory per-learner initial-difficulty prior fitted from the learner's own accuracy (heuristic, not full FSRS weight fit); never moves the audit verdict or the mastery gate. Wired into CLI (tutor derive-schedule [--optimize], non-zero exit on DRIFT) and MCP (learn_tutor_derive_schedule). New doctor invariant tutor.schedule_rederivable_from_log proves the audit can FAIL. INTEGRITY: pure, no Date.now(), never grades, never appends attempts, never feeds the mastery gate. 14 new tests; full suite 298 pass; doctor MATCH. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this ships
Makes the opt-in FSRS schedule a re-derivable function of the recorded scored attempts, and adds a lightweight per-learner initial-difficulty fit. New module
src/tutor/fsrsderive.mjs.The v1.6.0 FSRS work stores per-item state (
session.itemState) as a mutable cached hint. This PR closes the accountability gap: it proves that hint is exactly what replaying the witnessed graded log produces, or reports the DRIFT.Shipped (working + tested)
deriveItemStates(attempts)replays the FSRS-graded attempts (each carriesgrade+timestamp, written bytutor.recordAttemptWithGrade) through the same puregradeAttempt()math the live scheduler uses. It reconstructssession.itemStatebit-for-bit fromsession.attemptsalone (proven by a parity test against the live state). Deterministic, noDate.now().deriveScheduleReceipt(session)audits the cached hint against a clean un-primed replay and emits aMATCH/DRIFT/NO_FSRS_LOGverdict plus a hash-chained ledger over the graded attempts. The log-derived state is authoritative, so a stale or tampered cache is flagged asDRIFTwith a per-field diff, never silently trusted.optimizeParameters(attempts)fits one initial-difficulty prior per objective from that learner's own accuracy (all-correct history => easier start, all-wrong => harder). Honestly scoped: a documented heuristic prior, not a full FSRS weight optimization. Advisory only: it never moves the audit verdict or the mastery gate.learn tutor derive-schedule <id> [--optimize], writestutor/<id>.derive-schedule.json, non-zero exit on DRIFT.learn_tutor_derive_schedule(optimizeflag).tutor.schedule_rederivable_from_logproves the audit can FAIL (clean session -> MATCH, tampered cache -> DRIFT). A schedule audit that cannot detect a tampered cache is not an audit.Specced / not in this PR (honest scope)
Integrity posture
fsrsderive.mjsis pure and I/O-free: noDate.now(), never grades, never appends tosession.attempts, never feeds the mastery gate.itemStatestays a hint; the witnessed hash-chained log stays the truth. The feature RIDES ON the existing receipt/ledger machinery (reusesaccountability/ledger.mjs) rather than replacing it.Test evidence
New:
tests/learn-fsrs-derive.test.mjs(10) +tests/learn-fsrs-derive-cli.test.mjs(4) = 14 new tests. Full suite:Key assertions: replay reproduces live
itemStateexactly; derivation is deterministic across repeats; intervals lengthen with successful recalls and shorten on lapse/fail; a monotone success streak yields monotonically longer intervals; a tampered cache is caught as DRIFT (module, CLI non-zero exit, and MCP); legacy correct/incorrect-only attempts are ignored by the replay.Limitations
grade+timestamp) participate; plain Leitner correct/incorrect attempts are intentionally excluded from replay (they never wroteitemState).🤖 Generated with Claude Code