Courses
A course teaches a subject in chapters. Each chapter is a sequence of steps, and each step is a short piece of theory checked by one or more multiple-choice questions. A chapter ends with a final review of all its questions. Every question the learner answers becomes a card in their own deck, scheduled like any other (srs.md). At the end of the course the learner has a deck of every card they studied.
A course is not a new kind of thing in the pod. It is a library release with an outline, and the learner's copy is an ordinary deck. A course published anywhere else, in its creator's pod say, is the same: a learner adds it from a link to its release, and plays it as one from the library, its outline read from that address. The pure parts are in domain/course.ts; the use cases are in useCases.ts.
The model
- A course is a library release. It lives at
decks/<name>/vN.ttllike any deck, and is also typedschema:Course. - Questions are cards. Each question is a card (card format 5 or 6):
sm:frontis the question andsm:backthe right option, so ordinary flip-and-grade study works on it later. - Wrong options are subjects of their own. Each is an
sm:Distractor(also aschema:Answer, distractor format 1) in the document that holds its card, which names it withsm:distractor. It has text (sm:distractorText) and, optionally, a note on why it is wrong (sm:distractorNote). - The outline stays in the release. Chapters (
sm:Chapter, also aschema:Syllabus) and steps (sm:Step, also aschema:LearningResource) are subjects of the release, each part of its parent (schema:isPartOf) at its place (schema:position, 0 first). They are never copied into a pod: the learner's deck reads them from the release itsprov:wasDerivedFromnames. - A step has its theory (
sm:theory) and the cards that check it (sm:checkedBy, one or more). Theory in Markdown may be in chunks, read one at a time: each top-level thematic break (---) ends one and begins the next (markdown.md). No term or shape says so: it is how the app shows the Markdown, and an app that does not chunk shows the breaks as rules. - A chapter has a title and description, and may name cards asked
only in its final review (
sm:reviewQuestion), questions that combine its steps. - Text is plain unless its subject says otherwise. A step, a
chapter and a card may each state
sm:textFormat sm:markdown(vocabulary 1.15): then the step's theory, the chapter's description (never its title) or the card's texts and its distractors' are Markdown (vocab.md).
<> a solid-memo:Deck , dcat:Dataset , schema:Course ;
solid-memo:studyDirection solid-memo:frontToBack ; … .
<#ch-why-solid> a solid-memo:Chapter , schema:Syllabus ;
solid-memo:formatVersion 1 ;
schema:isPartOf <> ;
schema:position 0 ;
dcterms:title "Why Solid"@en ;
dcterms:description "Why Solid exists: the data-silo problem, …"@en ;
solid-memo:reviewQuestion <#q-why-solid-r01> , … . # asked only in the final review
<#ch-why-solid-2> a solid-memo:Step , schema:LearningResource ;
solid-memo:formatVersion 1 ;
schema:isPartOf <#ch-why-solid> ;
schema:position 1 ;
solid-memo:textFormat solid-memo:markdown ;
solid-memo:theory """## Separation creates choice
When one company holds both your data and the app, …"""@en ;
solid-memo:checkedBy <#q-why-solid-2a> , <#q-why-solid-2b> .
<#q-why-solid-2a> a solid-memo:Card ;
solid-memo:formatVersion 5 ;
solid-memo:front "In the argument for Solid, why would separating data storage from apps create more competition?"@en ;
solid-memo:back "Storage and apps can each be chosen and replaced separately"@en ;
solid-memo:backNote "Once data does not belong to the app, users can switch apps …"@en ;
solid-memo:distractor <#q-why-solid-2a-d1> , <#q-why-solid-2a-d2> , <#q-why-solid-2a-d3> .
<#q-why-solid-2a-d1> a solid-memo:Distractor , schema:Answer ;
solid-memo:formatVersion 1 ;
solid-memo:distractorText "Users can install more apps on the same device"@en ;
solid-memo:distractorNote "How many apps fit on a device has nothing to do with it; …"@en .
decks/solid-fundamentals/v1.ttl,
the first course, written in Markdown, is the complete example. The
terms are in
vocab.md, the shapes in shapes.md, and
the rules the shapes cannot state in
deck-library.md. Those rules are the
domain's
(courseRules.ts):
npm run library:check runs them, and the Studio can too.
Order
- Chapters are ordered by position, and so are a chapter's steps. A tie, which the library check refuses, is broken by fragment id.
- RDF keeps no order among the values of one predicate. So a step's
questions and a chapter's review questions are ordered by fragment id.
A release orders them by how it names them (
q-why-solid-2a,q-why-solid-2b). - The final review has no subject of its own. It asks every card the
chapter's steps check, plus its review questions, each once, shuffled
(
finalReviewQueue). - The options of a question are the card's back and each of its
distractors, shuffled (
choicesOf). The text is shown in the reader's language. - A retired chapter or step (
owl:deprecated true) is left out of the outline (courseOutlineFromRecords), with every step of a retired chapter. - A retired distractor (
owl:deprecated true) is never offered (choicesOfleaves it out). It is kept wherever a card's distractors are read (distractorsOf), markedretired, since a learner's answers may name it and the Studio may restore it. In any order the release lists them, a card's distractors are read by id. An upgrade then retires it in the copies that have it.
The learner's state
The learner's state is kept in documents the pod already has, with no new type-index entries (data-model.md):
| Where | What |
|---|---|
catalog.ttl | The course's deck: prov:wasDerivedFrom <release>, and one sm:completedChapter <release#ch-…> per chapter completed. |
decks/<deckId>.ttl | The cards answered so far, each with its sm:Distractor subjects, under the release's fragment ids; each card names them with schema:suggestedAnswer too. |
reviews/<deckId>.ttl | An ordinary front→back review state per card answered, naming its card (sm:reviewOf). |
history/<YYYY-MM>.ttl | An sm:Answer per graded answer, with sm:answerMode sm:multipleChoice and, when the answer was wrong, sm:chosenDistractor <…#d1>. |
- A card joins the deck when its question is answered. Starting a course creates an empty deck. Answering a question for the first time writes the card, its distractors and its first review state. Study can therefore never show a card the learner has not reached, and needs no rule to hide them. An older app sees a plain deck.
- Progress is derived, not stored (
courseProgress):- A card is answered when it has a front→back review state.
- A step is done when every card it is checked by is answered. The learner resumes at the first step that is not done.
- A chapter is open once every chapter before it is completed, and locked until then. It is done once completed, whatever comes before it.
- Only a chapter's completion is written: the final review passed.
- A completion counts by the chapter's fragment id, so it holds in
whichever release the deck follows: an upgrade from
v1.ttltov2.ttlkeepssm:completedChapter <v1.ttl#ch-…>, and a chapterv2.ttlkeeps under that id stays done.
The learner's flow
- Start. A library deck whose release is a course is marked as one
(
LibraryDeck.isCourse, fromschema:Coursein the index). Starting it (startCourse) copies the release's metadata into an empty deck (importDeckwith no cards). An instance that already has a copy of any release of the course gets that copy back: a course is started once. A course added from a link is started the same way; its own release says it is a course, so the deck list offers to continue it as it does a library course (useCopies). Until every chapter is completed, the course is offered above the deck list, with its chapters done and a button to go on where the learner left off; a course deck's menu leads to its page too. - A step. A step comes in two phases. First the learner reads the
theory, with no question shown. Theory in chunks is read one chunk
at a time, starting at the first: "Part 2 of 3" says where the
learner is, Continue goes to the next chunk and Back, from the
second on, to the one before. Below the last chunk (all the theory,
when it is one), a button goes on to the
step's question(s); it is described by a hint that the theory is not
shown while answering. The step's questions are then asked one at a
time, with the theory not rendered at all: the answer checks what
was understood, not what can be read off the screen. After each
answer the app says whether it was right. A wrong choice shows its
distractor's note. The right answer is shown as the card's back,
with its
sm:backNote. The first answer to a question adds the card to the deck ("Added to your deck"). After the step's last question, Next goes to the next step's theory, or, after the chapter's last step, to the final review. There is no way back to the theory while answering; every step starts on its theory, at its first chunk, the one the chapter resumes at included, so leaving and coming back shows it again. Moving to another chunk puts the focus on "Part 2 of 3", so a screen reader reads the chunk from its start. When the reader's language is switched to one with fewer chunks (which a release checked by the library never has), a chunk past the last shows the last. - The final review. Once every step is done, the chapter's final
review comes next, whether the learner gets there with Next after
the last step or from the course's page. It opens on a word before
it: that every step of the chapter is done, and what the review is.
A button starts it, and a link goes back to the course's chapters;
the heading takes the focus. The review asks all the chapter's
questions, shuffled and again without the theory, each taking the
focus as it comes up. A question answered wrongly comes back until
it is answered right. Then the chapter is completed
(
completeChapter), which opens the next; while that is saved, and when it fails, with a way to try again, the review says so. A final review reached some other way (a bookmark, a typed link, Back) for a chapter with steps not done goes to the chapter instead, at the first of them, so the word before the review is never wrong. - Back to the chapters. Once the chapter is completed, and the course read afresh, the learner is taken back to the course's page, to go on or stop there. A learner who has left the review while it was saved stays where they went, and nothing is cheered. The page cheers the chapter just completed: a word that it is done, which takes the focus, so a screen reader reads it and Tab goes on to Continue, now to the next chapter. The chapter's Done badge pops with a burst of sparkles. When that was the course's last chapter left, the word says the course is complete and three confetti shooters along the bottom of the page pop one after the other, their confetti shooting up and fluttering slowly back down, all over in under five seconds. The cheer is kept in memory, not in the URL, and only until the learner goes elsewhere: a reload or a later visit does not show it again. The motion is decoration only, hidden from screen readers, and none of it plays when the reader asks for reduced motion.
- Afterward. A finished course says so on its page, with a link back to the decks. The cards are studied like any deck's, by flip-and-grade (srs.md). A chapter can be retaken at any time, as practice: a chapter whose steps are all done opens at its first step, and each step shows its theory again. No single earlier step can be picked to reread its theory: the step a chapter opens at is derived from the progress, not chosen.
How each answer is graded, and when it is graded at all, is in
srs.md. The screens and
their routes are in routing.md. The chapter player is
ChapterPlayerScreen and ChapterPlayerContainer, and the review between
a chapter's theory and its questions is ChapterReviewScreen and
ChapterReviewContainer, all in
packages/ui/src/ui/.
The course for newcomers
The library's index may name one course as the one to offer someone
who has no decks yet: sm:newcomerCourse on its catalog, pointing at
the course's series (deck-library.md). The
app marks that deck LibraryDeck.forNewcomers, but only when its
series is in the index and its current release is a course; anything
else offers nothing.
- Where it shows. On the deck list of an instance that has no decks
(empty groups do not count), right under the Decks heading, a card
offers the course: its title, what it is, its card count, "Start the
course" and a link to its library page (
NewcomerCourseContainer,NewcomerCourseOffer). It is a labeled landmark, not a section with a heading, and takes no focus, but comes next after the heading the screen focuses, in reading and Tab order. While the library loads, when it cannot be read, or when it names no course, there is no card. Creating an instance starts reading the library at once, so the card usually comes with the list rather than pushing it down. - Starting it is
startCourse, as from the course's library page: the deck list is read afresh, and the course opens. Its deck is then on the list, so the card is gone; it does not come back while the instance has a deck. Once the course is started it opens, even when the instance's decks could not be read afresh: the course screen reads them itself. - Which course is the library's choice, not the app's: the app
names no course.
NEWCOMER_COURSEin deckLibrary.ts names the deck, andnpm run librarywrites it into the index; set it to another course, or toundefinedfor none, and runnpm run libraryagain. The app needs no change, but with none named the journeysnewcomer-course.journey.ts, which expects an offer, andcourse-chapter.journey.ts, which works through the offered course's first chapter (its theory in chunks), are to be retired or moved to another way in too (testing.md).
Use cases and ports
| Use case | Does |
|---|---|
startCourse(instanceUrl, course) | The instance's deck of the course: the copy it has (isCopyOf), else a new, empty copy of the current release. |
importReleaseFromUrl(instanceUrl, release) | A course added from a link is started so too (readReleaseFromLink reads and checks it first, deck-library.md). |
getCourse(deck) | The course as the learner has it: the deck as its entry is now, the release, its outline, its cards by id, the cards answered and the progress. Writes nothing. A deck that is gone throws deckGone. |
answerCourseQuestion(instanceUrl, deck, card, choice, now, times?) | Grades one answer as courseAnswerEffect says, logging how long choosing took (times.recallMillis). A card introduced is written first (applyCardChanges), then graded, so a review state never exists without its card. Returns the effect and the card's state. |
completeChapter(deck, chapterUrl) | Adds sm:completedChapter to the deck's entry. |
setCompletedChapters(deck, edit) | The Studio's change of the chapters completed: one marked not done (in any release, by its fragment id), or the course restarted, none completed. Answers and review states stay. |
Two ports gained methods (ports.ts):
DeckLibrary.fetchCourseOutline(releaseUrl)reads the chapters and steps of a release (toCourseOutlinein libraryMapper.ts), each with its shape and brought up to its latest format. The adapter (solidDeckLibrary.ts) reads each release document once, for both its cards and its outline. It takes any address:getCoursereads the release the learner's deck names (prov:wasDerivedFrom), wherever it is, so a course added from a link plays as the library's do. Nothing in the player knows where a course came from.DeckRepository.completeChapter(deck, chapterUrl)(solidDeckRepository.ts) adds the one triple with a PATCH made only ifcatalog.ttlis as it was read (If-Match). On a 412 it reads the document and tries again, three attempts in all, as an edit of the deck groups does. A chapter completed already writes nothing. The write is not shape-checked:sm:completedChapterbelongs to no shape. The one other write of it isDeckRepository.addDeck(deck), which writes the entry of a guest's deck added to an instance with the chapters the guest completed, If-Match and tried again on a 412 in the same way.DeckRepository.setCompletedChapters(deck, edit)removes the triples the edit drops (editCompletedChaptersin course.ts) the same way. It is the Studio's edit of the entry, so it passes the shape check.
The deck's distractors are written and removed with its cards
(withDistractors and distractorsOf in
deckMapper.ts). An edit
that states no distractors, as the card editor's does while its wrong
options are left as they were, keeps the card's. Removing a card
removes the distractors it names. The card editor and the Studio edit
them (studio.md).
The tests that hold this against a real server are in courses.integration.test.ts.
Writing a course
A course is written as any library deck is (deck-library.md), with these additions. The Studio writes a course in a draft in the pod the same way, and suggests its ids so (studio.md).
- Type the release
schema:Coursetoo, and setsm:studyDirection sm:frontToBack. - Add the chapters and steps, each with
sm:formatVersion 1,schema:isPartOfand aschema:positionof its own among its siblings. A chapter's title and a step's theory are language-tagged text, one per language, one of them English. - Write every question as a card (format 5, or 6 for one with a front
label) with text on its back. Ask it from one place only: one step's
sm:checkedBy, or one chapter'ssm:reviewQuestion. - Give each card at least two distractors (the first course gives
three), each with
sm:formatVersion 1, text in every language its back has and a note on why it is wrong. A back in no language (code, a number) is untagged orzxx, and so are its distractors' texts. Write wrong options that are plausible and of the same form as the right one, never "all of the above". - Name the subjects so their order reads from the ids: chapters
ch-<topic>, stepsch-<topic>-<n>, a step's questionsq-<topic>-<n><letter>(q-why-solid-2a,q-why-solid-2b), review questionsq-<topic>-r<nn>(q-why-solid-r01) and distractors<question>-d<n>(q-why-solid-2a-d1). - To write theory, a chapter's description or a question in Markdown
(code, tables, lists), state
sm:textFormat sm:markdownon that step, chapter or card (deck-library.md). A question's options are then one paragraph each, its back included, and hold no links. - To have the learner read a step's theory in chunks, write it in
Markdown and put a thematic break,
---with a blank line before and after it, between each two chunks (without the blank line before it,---underlines the line above as a heading, which the check refuses). Every language of the theory has as many chunks, none of them empty: no break first, last, or right after another (deck-library.md). Break at a change of topic, where a learner could pause, and keep a chunk to about a screenful; a short theory needs no breaks. - Keep the attribution and the review rounds as every authored deck does (deck-library.md).
npm run format:turtle, thennpm run library, which runs the course checks (deck-library.md) and the Markdown checks (deck-library.md).
A new version follows the library's rule. Nothing published is
removed: a chapter, step, card or distractor that should go is retired
(owl:deprecated true). Ids are never reused, since learners' decks
keep them:
- A chapter keeps its id only while it teaches what it did. One that grows into far more gets a new id, and the old one is retired; otherwise a learner who completed it would count as done with steps they never saw, and the course would not lead them there.
- A step's id carries nothing over: its progress comes from its cards.
- A card asked again keeps its id, and with it the learners' review history, even when its text is rewritten.
- A distractor keeps its id only while it is the same wrong answer,
since a learner's history names the option they chose by its id
(
sm:chosenDistractor). A wrong answer that changed is retired, and the new one gets a new id (<question>-d4, …). - The order still reads from the ids: in a step that asks a kept card beside new ones, name the new ones so the ids sort in the order the step asks them. The Studio's id assistant names a new question so, after the last or before the one chosen (studio.md).
Each learner's deck follows the release it was copied or
last upgraded to, and its outline with it. An upgrade is offered on
the course's page, as on the deck's, when the newer release changes
something the deck holds or describes, the outline (a chapter, a step
or its theory, their order, or which questions they ask:
sameOutline), or a question the learner has not reached yet. An upgrade of a course's deck adds no cards: the
learner reaches new questions through the course. It still changes,
retires and restores the cards the deck holds, their distractors
included
(migrations.md). A
release that changes only the outline, or questions not reached yet,
writes nothing to the deck but its release: the learner then reads the
newer outline and meets the newer questions, with the chapters
completed and the questions answered kept.
Why schema.org
- Others can read it.
schema:Course,schema:Syllabus,schema:LearningResourceandschema:Answerare what search engines and learning tools already understand. Typing the release, chapters, steps and distractors with them tells such a tool what it is looking at without knowing Solid Memo. - They are extra types, not the shape's class. A shape is picked by
a Solid Memo class (
CLASS_NAMESPACESin packages/vocab/tooling/shapes.ts), so each subject also has ansm:class:sm:Chapteris a subclass ofschema:Syllabus,sm:Stepofschema:LearningResource, andsm:Distractorofschema:Answer. A course needs nosm:Course: the release is already ansm:Deck, and a secondsm:class would give it two shapes. - Properties are reused where they mean the same.
schema:isPartOfandschema:positionplace chapters and steps.sm:positionis not used: its meaning is the arranged deck list.sm:theoryandsm:distractorTextare subproperties ofschema:text, andsm:distractorandsm:distractorNotepoint atschema:suggestedAnswerandschema:answerExplanationwithrdfs:seeAlso. They are terms of their own because their values follow Solid Memo's rules for text (vocab.md). A card in a pod names its distractors withschema:suggestedAnsweras well (since vocabulary 1.16); the frozen releases do not. - No
rdf:List. Order is a position on each subject, which a version can change one triple at a time, and which SHACL can check.