The contract between the AI and the renderer is
core/schema/analysis.schema.json
(schemaVersion: 2). The AI emits a JSON object that validates against it; the
tested shell renders it. The AI never writes HTML.
The core idea: a lesson is an ordered list of teaching blocks. There is no fixed page layout — structure emerges from which blocks the AI picks and in what order. A 3-block lesson and a 10-block lesson are both valid.
| field | required | notes |
|---|---|---|
title |
✅ | lesson title, shown at the top |
mode |
✅ | code | thinking | text |
source |
✅ | what was analyzed, e.g. "src/lib/epr" |
generatedAt |
✅ | ISO timestamp |
audience |
beginner (default) | dev — the initial audience toggle state |
|
skin |
aurora (default) | storybook | blueprint | terminal |
The AI's teaching plan. It documents why the lesson is shaped the way it is and is useful for debugging; it does not dominate the page.
| field | notes |
|---|---|
approach |
one-line description of the teaching approach |
analogy |
the real-world metaphor, if one is used |
why |
why this approach fits this subject |
blocks is an ordered array. Every block has a type and may carry a title and
emoji. The remaining fields depend on the type — the renderer reads only the
fields relevant to that type. Common convention: start with a hook, end with a
quiz and/or recap.
| type | purpose | key fields |
|---|---|---|
hook |
opening line that grabs attention | headline, body, kicker? |
analogy |
map a real-world thing to the code | realWorld, mapping[{from,to,note?}] |
concept |
explain one idea, teacher voice | body, technical?, code?, lang? |
code |
show + explain a snippet | code, explain, lang? |
steps |
a narrated step-by-step walk | steps[{title,body,code?,lang?}] |
flow |
conceptual process diagram | nodes[{id,label,kind?,note?}], edges[{from,to,label?}], rootId? |
architecture |
real file map grouped into layers | groups[{label,items[{id,file,role?}]}], edges[{from,to,label?}] |
compare |
contrast two things | left{label,body,code?,lang?}, right{…} |
aha |
spotlight the key insight | insight |
quiz |
check understanding | questions[{q,choices,answer,explain}] |
recap |
the takeaways | points[] |
glossary |
define terms | terms[{term,plain,technical?}] |
Anywhere a technical field exists, it is optional — when absent the renderer
shows the plain/body text instead. This lets the audience toggle (Beginner ↔
Technical) swap depth, and lets the AI skip detail it doesn't have. Writing
technical everywhere is never required.
{
"schemaVersion": 2,
"meta": {
"title": "What this function does",
"mode": "code",
"source": "src/util/slugify.ts",
"generatedAt": "2026-07-03T00:00:00Z"
},
"blocks": [
{ "type": "hook", "headline": "Turning titles into URLs", "body": "This one function makes any string safe for a web address." },
{ "type": "code", "title": "The whole thing", "lang": "ts", "code": "export const slugify = (s) => s.toLowerCase().replace(/\\s+/g, '-');", "explain": "Lowercase everything, then swap runs of spaces for hyphens." },
{ "type": "recap", "points": ["Lowercasing keeps URLs consistent.", "Hyphens replace spaces so the URL has no gaps."] }
]
}For a full worked example, see the committed samples in
examples/ alongside their .analysis.json sources.
core/build/inline.mjs validates the analysis against this schema before building
and prints the exact failing path on error. Run it directly:
node core/build/inline.mjs analysis.json -o output.html
{ "schemaVersion": 2, // required — must be 2 "meta": { ... }, // required — title, mode, source, … "strategy": { ... }, // optional — the AI's teaching plan "blocks": [ { "type": "…", … } ] // required — ordered teaching blocks (≥ 1) }