Specifications / Application frameworks
Workbench Lessons
STOP — NOT YET IN FORCE. This document was rewritten on 2026-10-31 for the
.lean-only direction of PROOF-SOURCE while thefeat/type-disciplinesPR is still open. Any agent working on that PR ignores this rewrite until the PR has merged. Do not delete, rename, rewrite or stop maintaining any code, test, lesson or screen because of this text before then; the previous revision in git history is the contract for that PR. After the merge, plan 0044 is the order of work.
Status: rewritten for .lean; delivery under plan 0044 phase 3, then plan 0043. Lessons are a mode of the proof workbench: the same application, the same store, the same checking services. They add one extension format, two framework components (Image and Video) and three regions.
Aim [LESSONS-AIM]
Learn mode teaches people to write Lean proofs in the workbench, from the first questions — what is a proof? what is a solution? what is a solver? — up to reading a research problem, the Navier–Stokes problem, in Lean's terms. A lesson is never only something to read or watch: each asks the learner to state a claim, prove it, or decide whether a proof holds, in Lean. On Submit the same services that fill a badge in the workbench (WORKBENCH-CHECK, WORKBENCH-VERDICT) judge the answer. No lesson stores an answer key the checker does not confirm.
The extension format [LESSONS-EXTENSION]
A lesson is two files. lessons/<slug>.lean is an ordinary Lean file holding every statement, solution and shown proof the lesson uses, as named declarations; it checks under osprey prove check like any other file and contains nothing a lesson-reader needs to understand. lessons/<slug>.json is the extension: prose, media, prompts, hints and challenges, each challenge pointing at declarations in the .lean file by name. The extension never contains proof text, a formula in any notation other than Lean's, or anything a .lean file could hold instead. The deleted sentence-step and formula notations of [PROOF-SOURCE-DELETED] may not appear in any lesson.
Mode [LESSONS-MODE]
Examples and Play walkthrough open the lesson catalogue, held as Learning in the Layout slice. The lesson surface replaces the editor and goal columns with:
| Region | Shows | Reads |
|---|---|---|
catalogue |
every lesson in stage order: title, summary, minutes, a tick when every challenge is right; prerequisites are shown as advice, never enforced | Shelf, Achieved |
lesson |
the open lesson as one scrolling page: its blocks in order and a slot per challenge | Shelf |
challenge:<lesson>/<id> |
one challenge's answer, button and feedback, keyed so that typing re-sends only that challenge | ChallengeState |
Open in the workbench on a challenge opens a new .lean document containing the lesson's preamble, the challenge's statement and the learner's answer, so a lesson leads into ordinary work. Switching modes preserves the document, the open lesson and the answers.
Files [LESSONS-FILES]
| Path | Holds |
|---|---|
apps/workbench/lessons/catalogue.json |
the ordered list of lesson slugs and the stage names |
apps/workbench/lessons/<slug>.lean |
the lesson's declarations |
apps/workbench/lessons/<slug>.json |
the extension |
apps/workbench/lessons/media/<slug>/ |
images, videos, posters and captions |
At start the application reads catalogue.json with ReadTexts into host-held slice lessons:catalogue, then every lesson's two files into lessons:texts; each read stores { files: [{ path, text, error }] }. A lesson that fails to read is listed with its fault and cannot be opened. Hosts contain no lesson judgement or proof logic. Media paths are relative to apps/workbench/lessons/, must start with media/<slug>/, and may not contain .., a backslash or a leading /.
The lesson format [LESSONS-FORMAT]
{
"format": 2,
"slug": "and-swap",
"title": "Swapping a conjunction",
"summary": "Take the pair apart, then build it the other way round.",
"stage": 2,
"minutes": 8,
"after": ["what-is-a-proof"],
"blocks": [
{ "kind": "prose", "text": "..." },
{ "kind": "lean", "decl": "and_swap" },
{ "kind": "lean", "source": "p ∧ q → q ∧ p" },
{ "kind": "math", "math": { "kind": "op", "symbol": "=", "left": ..., "right": ... }, "spoken": "..." },
{ "kind": "image", "source": "media/and-swap/pair.jpg", "alt": "...", "width": 1920, "height": 1080 },
{ "kind": "video", "source": "media/and-swap/film.mp4", "poster": "media/and-swap/poster.jpg", "captions": "media/and-swap/film.en.vtt", "alt": "...", "seconds": 40 },
{ "kind": "challenge", "challenge": { ... } }
]
}
| Field | Rule |
|---|---|
format |
2. 1 and anything else is refused with "this lesson needs a newer workbench". |
slug |
lowercase letters, digits and hyphens; equal to both file names and unique in the catalogue |
title, summary |
non-empty |
stage |
1 first steps, 2 proving, 3 research |
minutes |
positive |
after |
slugs this lesson builds on; each must be in the catalogue |
blocks |
non-empty; at least one block is a challenge |
Blocks:
kind |
Fields | Drawn as |
|---|---|---|
prose |
text: paragraphs separated by a blank line; no markup |
body text at reading measure |
lean |
decl: a declaration name in the lesson's .lean; or source: a Lean term, elaborated in the lesson's .lean context |
the statement or term typeset through [WORKBENCH-MATH], with the Lean source shown beneath on request; refused if Lean rejects it |
math |
math: a ui::Math tree; spoken: what assistive technology reads |
display mathematics beyond what a .lean file states (physics, integrals) |
image |
source, alt (non-empty), width, height |
the Image component |
video |
source, poster, captions (WebVTT), alt, seconds |
the Video component; never autoplays |
challenge |
challenge: one of [LESSONS-CHALLENGES] |
the challenge:<lesson>/<id> region |
Maths::decode is the inverse of Maths::encode and refuses an unknown kind. A lesson never contains TeX, HTML or MathML.
Challenges [LESSONS-CHALLENGES]
Every challenge has an id (unique within its lesson), a prompt, zero or more hints, and one kind. Three kinds, all judged by Lean and the kernel.
Prove [LESSONS-PROVE]
{ "kind": "prove", "id": "swap", "prompt": "Prove that the order of a conjunction does not matter.",
"statement": "and_swap", "given": ["intro h"], "solution": "and_swap_solution",
"hints": ["Take the conjunction apart first: cases h with hp hq.", "Then build the new one: constructor."] }
statement names a declaration in the lesson's .lean whose body is by sorry; the learner writes tactic lines into a marked editor under the locked given lines. On Submit the workbench builds a .lean document from the lesson's preamble, the statement's signature, by, the given lines and the answer, and checks it through [WORKBENCH-CHECK]. The answer is right exactly when the verdict is Checked. solution names a declaration in the .lean with the same signature and a complete proof; Show a solution copies its tactic lines into the answer.
State [LESSONS-STATE]
{ "kind": "state", "id": "rain", "prompt": "Write: if it rains, the ground is wet.",
"variables": ["rain", "wet"], "expected": "rain_wet",
"hints": ["\"If … then …\" is an arrow."] }
The learner writes a Lean proposition over the variables (each a Prop in the lesson's .lean). On Submit the workbench elaborates it in the lesson context; a Lean error is shown as one, and a name outside variables is refused with the variables listed. Otherwise the kernel decides whether the answer and the declaration expected are the same proposition by definition (Reduce::defeq): ¬rain and rain → False are both accepted where either is expected; wet ∧ rain is not accepted for rain ∧ wet, and the feedback says so and points at the prove challenge that shows it.
A state challenge shows a keypad of its variables, the connectives and parentheses with plain names (Implies for →), spelled from the shared notation table. A key inserts at the attempt's bounded UTF-8 byte caret, advances the caret and clears stale marking without submitting; carets are independent per challenge; insertion alone never produces a verdict. A keypad insertion advances a per-attempt focusEpoch; a retained editor returns focus only for an epoch greater than every epoch it has observed, and editor identity includes the lesson as well as the challenge.
Judge [LESSONS-JUDGE]
{ "kind": "judge", "id": "wrong-order", "prompt": "Does this proof check?", "proof": "and_swap_wrong" }
proof names a declaration in the lesson's .lean. The learner sees it typeset with its tactic lines and chooses It checks or It fails. On Submit the workbench checks that declaration then and there; the learner is right when the choice matches. The file holds no answer. Feedback shows what Lean and the kernel said, with the refused line underlined.
Submitting and feedback [LESSONS-FEEDBACK]
Submit is enabled when the answer is non-empty (and, for judge, a choice is made). It issues one check with a revision ticket; a completion for an older revision is dropped. The challenge then shows exactly one of:
| Result | When | Shown |
|---|---|---|
Correct message |
prove: Checked; state: same proposition; judge: choice matches |
a "Right" badge and the explanation |
Incorrect line message |
prove: Rejected; state: different proposition; judge: choice does not match |
a "Not yet" badge; the refused line underlined with Lean's or the kernel's message |
Unfinished reason |
prove: Unchecked (a sorry, an unsolved goal); state: Lean cannot elaborate the text; no toolchain |
"Not finished yet:" and the reason; does not count as an attempt |
Every Incorrect adds one attempt. Hint n appears after n wrong attempts or on Show a hint. A shown solution marks the challenge seen, not right, until the learner submits it themselves. Editing after Correct clears the result.
Every shipped lesson is checked [LESSONS-INTEGRITY]
The suite refuses the build when any of these fails for any lesson in apps/workbench/lessons/:
- the
.jsonreads with no fault and its slug,afterlist and challenge ids follow [LESSONS-FORMAT]; every referenceddeclexists in the.lean; - the
.leanchecks underosprey prove checkwith everystatementdeclarationUnchecked(its body issorry), everysolutionandexpecteddeclarationChecked, and every judgeproofCheckedorRejected, neverUnchecked; - every
leanblock withsourceelaborates; - every media path follows [LESSONS-FILES] and the file exists.
Progress [LESSONS-PROGRESS]
The Tutor slice holds, per challenge, the answer text, the choice, the attempts, the hints shown and the last result, and the set of challenges answered right. Progress is written through host-held slice lessons:progress with StoreWrite to lessons/progress.json after each newly right answer and read with StoreRead at start; it holds challenge identifiers and nothing else. A missing file means no progress; a failed write reports that progress was not saved.
Framework additions [LESSONS-FRAMEWORK]
@osprey/native gains Image (source, alt, width, height) and Video (source, poster, captions, alt, seconds; poster and play control first, captions on, never autoplays, length printed on the poster). Both encode as protocol nodes "image" and "video"; neither carries a binding. Maths::decode sits beside Maths::encode.
The curriculum [LESSONS-CURRICULUM]
Seven lessons in three stages; each has at least two challenges and at least one prove challenge except where noted. Every lesson teaches Lean's own words (intro, cases … with, constructor, exact, left, right, absurd) with a one-line English explanation beside each.
| Stage | Slug | Teaches | Media |
|---|---|---|---|
| 1 | what-is-a-proof |
A claim, the steps, the rule behind each step, why a checked proof leaves no room for doubt | film |
| 1 | what-is-a-solution |
A proposition is a problem and its proof is the solution | image |
| 1 | what-is-a-solver |
Searching versus checking; why the checker's verdict can be trusted; an honest "not finished" | film |
| 2 | and-swap |
The first full proof: intro, cases, constructor, exact |
image |
| 2 | either-way |
Proof by cases on ∨ |
none |
| 2 | not-and-contradiction |
¬p as p → False, and absurd |
none |
| 3 | navier-stokes |
The equations, what a solution is, classical results, and the OpenAI solution with its Lean formalization | film, image |
The Navier–Stokes lesson teaches the solution published by OpenAI on 8 September 2026: smooth forcing can produce a finite-time singularity from smooth fluid at rest while energy stays finite, establishing statements C and D of the Millennium formulation. It teaches the equations (math blocks with u, p, ν), what a smooth solution supplies, what was proved before (Leray 1934; global smooth solutions in two dimensions; local in three; small-data results with their hypotheses) and the published result with its hypotheses. Its challenges use Props named smooth and blowup: prove smooth ∨ blowup → ¬blowup → smooth; state that both cannot hold; judge a "proof" of smooth from nothing, which the checker refuses. Credit the published solution. Osprey checks selected supporting exports from openai/math and does not check the complete theorem; keep the three levels distinct.
Films and images [LESSONS-MEDIA]
Films come from the Nimblesite TikTok content factory (factory film): 1920×1080 H.264 with AAC, fast start, at most 2 MB and 60 seconds, narrated with English WebVTT captions, with a JPEG poster. Sources live in that repository under projects/osprey/videos/lesson-<slug>/; only published files are copied into media/<slug>/. Images are JPEG at true size, at most 400 KB. A film's narration says only what its lesson says.
Store and messages [LESSONS-STORE]
| Slice | Held | Contents |
|---|---|---|
lessons:catalogue, lessons:texts |
host | bounded file-read results |
lessons:progress |
host | saved completed challenge identifiers |
Shelf |
session | catalogue summaries and faults, the open lesson's slug and both sources, the focused challenge, loading or save errors |
Tutor |
session | per-challenge attempt state with its caret and focus epoch, and the completed set |
Achieved |
derived | completed challenge keys |
The action union carries ToLearn, ToWork, Open slug, Answer id, FormulaPlace id, Key id text, Pick id, Submit id, Hint id, Reveal id and Carry id. Judging is a check request plus a pure comparison of its completion; file and progress operations are bounded host requests.
Verification [LESSONS-VERIFICATION]
In apps/workbench/tests/, each a fixture through the real start/dispatch where it concerns the application:
- the reader accepts every block and challenge kind and refuses each broken form: unknown
kind, missing field, bad slug, unsafe media path, unknowndecl,format1; Maths::decode (Maths::encode t) = tover a corpus of trees, inpackages/native/tests/;- prove: a
Checkedcompletion isCorrect; aRejectedcompletion at line 4 isIncorrectat line 4 with its message; asorryisUnfinished; a stale completion is dropped; - state: an exact answer, an answer equal by definition (
¬pforp → False), a different but equivalent answer (Incorrect), a stray name and an unparsable formula; - keypad: caret insertion at start, middle, end and clamped; multi-byte offsets; independent attempts; marking invalidation without submission; focus high-water and lesson-qualified identity reset;
- judge: both choices against a
Checkedand aRejecteddeclaration; - attempts, hints and the solution reveal follow [LESSONS-FEEDBACK]; editing after
Correctclears it; - a keystroke in one answer re-sends only that
challenge:<lesson>/<id>region; - progress round-trips through
StoreWriteandStoreRead; - every shipped lesson passes [LESSONS-INTEGRITY] with the toolchain present;
- the
ImageandVideoencodings, inpackages/native/tests/; - no lesson file contains the deleted formats (the grep gate of [WORKBENCH-VERIFICATION]).
Each host's smoke test opens what-is-a-proof, plays nothing, answers its first challenge and sees the "Right" badge.