Architecture

July 1, 2026 · View on GitHub

learn is two engines sharing one accountability spine. This page maps the organs, the verbs between them, and where the integrity line actually lives in the code, not just in prose.

The one shape underneath both engines

Every capability in learn, credential logistics and the teach-you loop alike, is built from the same three primitives:

witness   ──  content-addressed hashing of what actually happened (sha256hex, observe)
ledger    ──  a hash-chained, tamper-evident append log (Ledger.append / .verify)
gate      ──  default-deny decision over a step (decide -> allow | deny | needs-human)

Nothing in learn records a fact without a witness, nothing accumulates state outside a ledger, and nothing graded proceeds without passing the gate. This is the same shape the rest of Project Telos uses: perceive, recover an invariant, check it, emit a verdict you can re-derive rather than take on trust. Here the "artifact" is a study session or a course run, and the invariant is "did the operator's own work satisfy the threshold," never a guess the tool supplies for them.

The credential engine

workflow (declarative steps)


gate.decide(step)  ──►  deny (undeclared kind, nothing actuated)
      │                 needs-human (assess, credentials, payment, CAPTCHA, submit-without-auth)

actuate (FakeDriver | NativeDriver)


witness (before/after snapshot digest)  ──►  ledger.append


receipt (json + markdown + html: logistics vs. human assessment vs. aid visualizations)
  • workflow/schema.mjs: the declarative step schema. STEP_KINDS is the engine's global allowlist; a step outside it is denied before it ever reaches a driver.
  • accountability/gate.mjs: decide(step, {sealedKinds, allowIrreversible}). assess always returns needs-human, unconditionally. A submit (or anything cost/irreversible-flagged) only proceeds automatically when the operator has explicitly authorized "witnessed-auto" for the run; otherwise it halts too. assess is never gated by that flag; it is not a submission mode concern, it always halts.
  • runtime/runner.mjs: run/resume. Walks workflow steps, asks the gate first, actuates only on allow, snapshots before/after every actuated step, and witnesses the digest into the ledger. A deny or needs-human decision stops the loop immediately; nothing after that point is actuated.
  • actuation/: FakeDriver (deterministic, offline, used by every test and by doctor) and NativeDriver (real browser control via native-control, used only for the operator-run smoke in docs/smoke.md).
  • adapters/: a Source-style seam (getAdapter(name)) so each course platform's "how do I read the completion certificate" logic stays isolated from the runner. Shipped: generic (config-driven) and an LMS pack (Coursera, Udemy, LinkedIn Learning, edX, Credly, Microsoft Learn, NonprofitReady, and a generic self-paced fallback). No adapter contains grading logic; they only read what already happened.
  • receipt/receipt.mjs: folds the ledger into three structurally separate channels (humanAssessments, manualSubmissions / witnessedAutoSubmissions, aidVisualizations), so a render can never be filed as if it were graded human work.

The tutor / learning loop

tutor.newSession(topic, objectives)


tutor.recordAttempt   ──  the operator's OWN answer to a practice prompt, scored true/false

      ├──► schedule.due            (spaced repetition: what's due for review, most-overdue first)
      ├──► misconception.misconceptions  (ranked aggregation of the operator's own wrong attempts)
      ├──► retrieval.clozePrompts / interleave  (blanked recall prompts from the operator's OWN
      │                                          assist-extracted claims; deterministic mixed order)
      ├──► predict.recordPrediction / scorePrediction  (predict-then-observe, pending until scored)
      ├──► explain.explanationThesis / gradeExplanation  (self-explanation via crucible MATCH/DRIFT)
      └──► map.normalizeObjectives / learningPath / readiness  (prerequisite gating)


tutor.mastery(session)  ──  ready iff EVERY objective has >= minAttempts and >= threshold accuracy,
      │                     computed ONLY from session.attempts (never a render, never a pending
      │                     prediction, never a visualization)

study.studyPlan / studyReceipt  ──  composes all of the above into one plan and one witnessed,
                                     hash-chained study record
  • tutor/tutor.mjs: the session shape and the mastery-gate itself. recordAttempt is the only way an attempt enters the log, and it always coerces correct to a boolean (!!correct), so a null/pending value can never be silently read as true.
  • tutor/schedule.mjs: an SM-2-lite / Leitner ladder over session.attempts. Pure function of the practice log and an explicit now; no wall clock, no hidden state.
  • tutor/misconception.mjs: aggregates wrong attempts (correct === false) per objective, ranked by count. It surfaces nothing the operator did not already see; it never fabricates a "correct answer" field.
  • tutor/retrieval.mjs: clozePrompts blanks a salient span out of a claim the operator's own assist() already extracted, carrying a source so the operator can check themselves afterward. interleave is a seeded (mulberry32), deterministic shuffle, never Math.random.
  • tutor/predict.mjs: recordPrediction stores the operator's prediction as a pending attempt (correct: null); scorePrediction requires an explicit index and throws rather than silently no-op-ing on an already-scored or invalid attempt.
  • tutor/explain.mjs: wraps the operator's own explanation into a crucible thesis (via assist + toCrucibleThesis) and buckets the returned MATCH/DRIFT/UNVERIFIABLE verdicts into grounded/shaky/unverifiable. It supplies no answer key; the falsification field stays for crucible or the operator to fill in.
  • tutor/map.mjs: normalizes objectives (plain string or {id, text, requires}), computes a topological learningPath, and gates readiness on whether every prerequisite is itself mastered (reading mastery()'s own ready flag, adding no new judgment about correctness).
  • tutor/study.mjs: the orchestrator. Composes due, misconceptions, interleave, readiness, and mastery into one studyPlan, and studyReceipt hash-chains the practice log underneath it into a witnessed, tamper-evident record.
  • tutor/tutorstore.mjs: session persistence (load/save by id), so a study session survives across CLI invocations without a database dependency.

The visualization bridge (aid, never graded)

concept  ──►  toTelosSceneSpec (pure, no I/O)  ──►  telosRender (spawns LEARN_TELOS_CMD)  ──►
      render result, always tagged provenance:"aid"


toAidLedgerEntry  ──  kind:"aid-visualization", structurally distinct from every graded-channel kind


recordVisualization(session, ...)  ──  attached to the study log, NEVER read by mastery()

interop/telos.mjs delegates rendering to an external telos engine process over LEARN_TELOS_CMD. It never imports telos internals and never chooses the render profile; it asks over a process boundary and is fail-closed: no command configured returns UNVERIFIABLE immediately, never a guess dressed as a pass. Every render carries provenance: "aid" all the way into the ledger and the receipt, so it can never be misread as a graded answer or a human assessment.

Where the integrity line actually lives in code

The rule ("no learning-aid capability may produce, hint, or auto-fill an answer to a graded assessment; mastery() is a function of the operator's own practice only") is enforced at these concrete points, each with a falsifiable test:

  1. accountability/gate.mjs: decide returns needs-human for assess unconditionally; no flag, mode, or caller argument can move it to allow.
  2. runtime/runner.mjs: a needs-human or deny decision halts the loop before any actuation for that step; doctor.mjs asserts this halts with zero driver actions.
  3. tutor/tutor.mjs: mastery() reads only session.attempts; recording a visualization or a pending prediction changes session.visualizations or the attempt count, never the accuracy of an already-scored objective.
  4. tutor/predict.mjs: a pending prediction is correct: null, coerced to false by recordAttempt's own boolean cast; it is never silently counted as correct.
  5. receipt/receipt.mjs: aid visualizations are filed under aidVisualizations, structurally separate from humanAssessments, manualSubmissions, and witnessedAutoSubmissions.
  6. doctor.mjs: re-runs checks 1 to 5 at runtime and reports MATCH/DEGRADED, so a future change that weakens any of them fails loudly instead of silently.

Peer composition

learn composes with gather (source receipts feed the assist pillar's citations), crucible (measured claim evaluation powers self-explanation grading), and the telos engine (renders learning aids over a process boundary). None of these are imported as internals; each is a clean seam that defaults to absent (fail-closed) rather than a hard dependency, so learn stands alone with zero external packages while still composing with the rest of Project Telos when those tools are present.

See also: How it works for the study loop walked step by step, and Enterprise Readiness for the operational contract.