A course teaches a subject in chapters. A chapter is a sequence of steps, each a short piece of theory that one or more multiple-choice questions check, and 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).
A course is a library release with an outline, and
the learner's copy is an ordinary deck, typed schema:Course like its
release. A course published elsewhere, such as in its creator's pod, is
added from a link to its release and
plays like one from the library. The pure parts are in
domain/course.ts, and the use cases
in courses.ts.
- A course is a library release at
decks/<name>/vN.ttl. - Questions are cards (card format 5, or 6 with a front label):
sm:frontis the question andsm:backthe right option. - Wrong options are subjects of their own. Each is an
sm:Distractor(and 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's wrong (sm:distractorNote). - The outline stays in the release. Chapters (
sm:Chapterandschema:Syllabus) and steps (sm:Stepandschema:LearningResource) are subjects of the release, each part of its parent (schema:isPartOf) at its place (schema:position, 0 first). They're 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). Markdown theory may be in chunks, split at top-level thematic breaks (---, markdown.md). No term or shape marks the chunks; an app that doesn't 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 can't state in
deck-library.md. Those rules are the
domain's
(courseRules.ts):
npm run library:check runs them, and so can the Studio.
- Chapters are ordered by position, and so are a chapter's steps. A tie, which the library check refuses, is broken by fragment id.
- A step's questions and a chapter's review questions are ordered by fragment id (RDF keeps no order among one predicate's values), as Writing a course names them.
- 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). Their text is in the reader's language, or first in the course's language (the deck's card language) when the learner chose one. On the course's screens, its title, description, chapters' titles and descriptions and steps' theory follow it too; the app's own text stays in the app's language. The learner picks it in the course's preferences, linked from its page, from every language the course states (i18n.md). - A retired chapter or step (
owl:deprecated true) is left out of the outline (courseOutlineFromRecords), and so is every step of a retired chapter. The steps are put under their chapters in one pass, so a release with thousands of chapters and steps takes no longer per step than a small one. - A question that names no card of the release is left out of the
outline (
outlineOfCards), and so is a step left with no question. A chapter left with no step and no review question goes too, and the next chapter unlocks as if it weren't there. A chapter left with only review questions opens at its final review. The Studio refuses such a question before it publishes, but a release from another server may have one. The player, the progress and the statistics read the same outline, so they count the same questions. - A retired distractor (
owl:deprecated true) is never offered (choicesOfleaves it out), and an upgrade retires it in the copies that have it. It's kept, markedretired, wherever a card's distractors are read (distractorsOf): a learner's answers may name it and the Studio may restore it. A card's distractors are read by id, in any order the release lists them.
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: typed schema:Course too, 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 (since vocabulary 1.16; the frozen releases don't). |
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, and the first answer to a question writes the card, its distractors and its first review state. A card the deck already has at that id stays as it is and only gets the state. Study never shows a card the learner hasn't reached, and 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 that checks it is answered. The learner resumes at the first step that isn't done.
- A chapter is open once every chapter before it is completed, and locked until then. It's 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, in whichever
release the deck follows: after an upgrade from
v1.ttltov2.ttl,sm:completedChapter <v1.ttl#ch-…>marks the chapterv2.ttlkeeps under that id done.
- 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), whose catalog entry says it's a course (Deck.course). A course is started once: an instance with a copy of any of its releases gets that copy back.- The deck list and the Studio tell a course by its deck
(
useCopies), even while the library's index loads or the course's host can't be reached. Only a copy made before decks said so is told by the index, or by its own release when added from a link (data-model.md). - Until every chapter is completed, the course is offered above the deck list, headed "Your courses", 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 line says these are the courses added and not finished yet, and that they run alongside the other decks: one need not be finished before adding more from the deck library.
- While every deck of the instance is a course, a line under the courses says the deck library has ready-made decks and courses to study alongside, with a secondary Deck library button. It goes once a deck that isn't a course is on the list, or with the strip once every course is finished. It depends only on the instance's decks.
- Each course in the strip has its deck's title, and is read only as
far as the strip needs (
getCourseSummary), once the deck list's arrangement is in. The library's index is read beside the arrangement, not after it.
- The deck list and the Studio tell a course by its deck
(
- A step comes in two phases.
- The theory first, with no question shown, one chunk at a time from the first. "Part 2 of 3" says where the learner is, and takes the focus on a move to another chunk. Continue goes to the next chunk, and Back, from the second on, to the one before. Below the last chunk, a button goes on to the step's question(s), described by a hint that the theory isn't shown while answering.
- Then the questions, one at a time, with the theory not rendered and
no way back to it. After each answer the app says whether it was
right: a wrong choice shows its distractor's note, and the right
answer shows 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"), unless the deck already has a card at that id, which stays as it is and gets no such word. 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. - Every step, the one a chapter resumes at included, starts on its theory at its first chunk. The chunks are counted in the language the app shows the theory in, the course's first. On a switch to a language with fewer chunks (which a release the library checked never has), a chunk past the last shows the last. If a course read afresh has fewer steps or questions than the learner has reached, the player and the course preview go on from the last step's theory or the step's last question.
- The final review comes once every step is done, by Next or from
the course's page. It opens on a word that every step of the chapter
is done, and what the review is, with a button to start it, a link
back to the course's chapters, and the focus on the heading. It asks
the chapter's questions (Order) without the theory, each
taking the focus as it comes up. A question answered wrongly comes
back until it's answered right. Then the chapter is completed
(
completeChapter), which opens the next; the review says so while that's saved, and when saving fails, with a way to try again. If a course read afresh no longer has a queued question, the review passes over it, and completes the chapter once none is left. A final review reached some other way (a bookmark, a typed link, Back) for a chapter with steps not done goes to the chapter's first such step instead. - Back to the chapters. Once the chapter is completed and the
course read afresh, the learner is back on the course's page. A
learner who left the review while it was saved stays where they went,
and nothing is cheered.
- The page cheers the newly completed chapter with a word that it's done, and that the learner can go on or stop, and meanwhile add decks from the deck library and study them too. The word takes the focus, and Tab goes on to Continue, now to the next chapter, then to Deck library.
- The chapter's Done badge pops with a burst of sparkles. When that was the 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 shoots up and flutters slowly back down, all in under five seconds.
- The cheer lives in memory, not in the URL, and only until the learner goes elsewhere: a reload or a later visit doesn't show it again. The motion is decoration only, hidden from screen readers, and doesn't play when the reader asks for reduced motion.
- Afterward. A finished course says so on its page, and that what to learn next is in the deck library, with a link back to the decks and a secondary Deck library button after it. Before then, the page has that button after Continue. The Studio's trial has neither. 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, each showing its theory again. The learner can't pick an earlier step to reread: the step a chapter opens at comes from the progress.
How each answer is graded, and when, is in
srs.md, and the screens
and their routes in routing.md. The chapter player is
ChapterPlayerScreen and ChapterPlayerContainer, and the review
between a chapter's theory and its questions ChapterReviewScreen and
ChapterReviewContainer, all in
packages/ui/src/ui/.
The library's index may name one course 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 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 don't count), a card offers the course right under the
Decks heading (
NewcomerCourseContainer,NewcomerCourseOffer).- It shows the course's title, its card count, "Start the course" and a link to its library page. It says what it is: a course for newcomers, where each answered question becomes a card and nothing needs finishing before other decks are added from the deck library.
- It's a labeled landmark, not a section with a heading, and takes no focus; it comes right after the heading the screen focuses, in reading and Tab order.
- While the library loads, when it can't be read, or when it names no course, there's no card. Creating an instance starts reading the library at once, so the card usually comes with the list.
- Starting it is
startCourse, as from the course's library page: the deck list is read afresh, and the course opens. The card then goes, and doesn't come back while the instance has a deck. The course opens even when the instance's decks couldn't be read afresh: the course screen reads them. - Which course is the library's choice: the app names no course. A
contributor names another course, or
undefinedfor none, inNEWCOMER_COURSE(deckLibrary.ts) and runsnpm run library, which writes it into the index. The app needs no change. With none named, two journeys (testing.md) are retired or moved to another way in:newcomer-course.journey.ts, which expects an offer, andcourse-chapter.journey.ts, which works through the offered course's first chapter (its theory in chunks) in Swedish. A course named instead states Swedish for its title, description, every chapter's title and its first step's theory, or the journey's course language changes with it.
| 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) |
Starts a course added from a link the same way (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's gone throws deckGone. |
getLibraryCourse(deck) |
For a library course's preview, before it's added: the release's outline, asking only its cards (outlineOfCards), and its cards by id. Writes nothing. |
getCourseSummary(deck) |
For the deck list's strip: the outline of the release the deck names and the cards answered. The deck is taken as given, not read again, and the release's cards aren't mapped. Writes nothing. |
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, only when the deck has no card at its id (addCardIfAbsent), then graded, so a review state never exists without its card. A card already there is graded as it stands: its text stays, and so does the release it names, or its lack of one, and the effect is review, since nothing joined the deck. 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 have methods for courses (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 its cards and outline. It takes any address:getCoursereads the release the deck names (prov:wasDerivedFrom) wherever it is, and nothing in the player knows where a course came from.DeckRepository.completeChapter(deck, chapterUrl)(solidDeckRepository.ts) adds the one triple tocatalog.ttlwith an If-Match PATCH, retried on a 412 (data-model.md); a chapter completed already writes nothing. The write isn't 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, the same way.DeckRepository.setCompletedChapters(deck, edit)writes the chapters the edit keeps (editCompletedChaptersin course.ts) the same way, all in one go, in time that grows with them. It's the Studio's edit of the entry, so it passes the shape check. The entry is read with at mostMAX_COMPLETED_CHAPTERSof them, asdata-model.mdsays.
A deck's distractors are written and removed with its cards
(withDistractors and distractorsOf in
deckMapper.ts). An edit
that states no distractors keeps the card's; the card editor states none
while its wrong options are unchanged. The card editor and the Studio
edit them (studio.md).
courses.integration.test.ts tests this against a real server.
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 ids that follow step 5 (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 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's wrong. A back in no language (code, a number) is untagged orzxx, and so is its distractors' text. 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). - For theory, a chapter's description or a question in Markdown (code,
tables, lists), state
sm:textFormat sm:markdownon the 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 with a thematic break between each two chunks:
---with a blank line before and after it. Without the one before,---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).
- Run
npm run format:turtle, thennpm run library, which runs the course checks (deck-library.md) and the Markdown checks.
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 (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 (guards against a learner counting as done with steps they never saw).
- 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's the same wrong answer (a
learner's history names the chosen one,
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.
Each learner's deck follows the release it was copied or last upgraded
to, and that release's outline. The course's page offers an upgrade, as
the deck's page does, when the newer release changes something the deck
holds or describes, the outline, or a question the learner hasn't
reached. The outline is the chapters, steps, theory, their order and the
questions they ask (sameOutline). The upgrade adds no cards: the
learner reaches new questions through the course. It still changes,
retires and restores the cards the deck holds, with their distractors
(migrations.md). A release
that changes only the outline, or questions not reached yet, writes only
the deck's release. The learner then reads the newer outline and meets
the newer questions, and the chapters completed and the questions
answered are kept.
- Others can read it.
schema:Course,schema:Syllabus,schema:LearningResourceandschema:Answertype the release, chapters, steps and distractors for search engines and learning tools that don't know Solid Memo. - They're 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 has ansm:class too:sm:Chapteris a subclass ofschema:Syllabus,sm:Stepofschema:LearningResource, andsm:Distractorofschema:Answer. A course has 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, notsm:position, which means the arranged deck list.sm:theoryandsm:distractorTextare subproperties ofschema:text, andsm:distractorandsm:distractorNotepoint atschema:suggestedAnswerandschema:answerExplanationwithrdfs:seeAlso; their values follow Solid Memo's rules for text (vocab.md). - 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.