@lernapps/app-templates 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +195 -2
  3. package/dist/appname-Bh23cUuz.mjs +12 -0
  4. package/dist/assessment-By2GGTJC.mjs +1696 -0
  5. package/dist/block-BK_ohsH1.mjs +82 -0
  6. package/dist/blocks-BmRrD7UY.mjs +149 -0
  7. package/dist/blocks.mjs +4 -0
  8. package/dist/check-BkEl-m-F.mjs +254 -0
  9. package/dist/content-cYBPYg_J.mjs +140 -0
  10. package/dist/contract-PONpxJtd.mjs +52 -0
  11. package/dist/e2e-BiMI-pCL.mjs +89 -0
  12. package/dist/engine-BqF2Vffn.mjs +136 -0
  13. package/dist/explainer-e2e.mjs +1297 -0
  14. package/dist/explainer-islands.mjs +197 -0
  15. package/dist/explainer-plugin.mjs +2131 -0
  16. package/dist/explainer.mjs +1246 -0
  17. package/dist/html-mffkHGHh.mjs +89 -0
  18. package/dist/image-CspJVfyQ.mjs +21 -0
  19. package/dist/interactive-e2e.mjs +140 -0
  20. package/dist/interactive-plugin.mjs +242 -0
  21. package/dist/interactive.mjs +75 -0
  22. package/dist/load-C-0hUaSC.mjs +16947 -0
  23. package/dist/needs-BzEahJ6X.mjs +97 -0
  24. package/dist/pages-9RaaomoO.mjs +81 -0
  25. package/dist/questions-e2e.mjs +1 -0
  26. package/dist/questions-engine.mjs +3 -0
  27. package/dist/questions.mjs +6 -0
  28. package/dist/quiz-BKc8JYtC.mjs +70 -0
  29. package/dist/quiz-e2e.mjs +174 -0
  30. package/dist/quiz-plugin.mjs +266 -0
  31. package/dist/quiz.mjs +246 -0
  32. package/dist/render-BJey0bwl.mjs +540 -0
  33. package/dist/render-BL6fV_NI.mjs +507 -0
  34. package/dist/render-DpRO_RKm.mjs +146 -0
  35. package/dist/result-_eQcamoz.mjs +180 -0
  36. package/dist/tasks-DADUYy52.mjs +74 -0
  37. package/dist/validate-COaZso7u.mjs +1072 -0
  38. package/dist/validate-CxtdeQ5S.mjs +146 -0
  39. package/dist/vue-DO1m6q1R.mjs +40 -0
  40. package/dist/vue-code-BkJjW_I_.mjs +320 -0
  41. package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
  42. package/docs/arc42/02-constraints.arc42.md +228 -0
  43. package/docs/arc42/03-system-context.arc42.md +118 -0
  44. package/docs/arc42/04-solution-strategy.arc42.md +47 -0
  45. package/docs/arc42/05-building-blocks.arc42.md +307 -0
  46. package/docs/arc42/06-runtime-view.arc42.md +98 -0
  47. package/docs/arc42/07-deployment-view.arc42.md +160 -0
  48. package/docs/arc42/08-concepts.arc42.md +123 -0
  49. package/docs/arc42/09-decisions.arc42.md +246 -0
  50. package/docs/arc42/10-quality-requirements.arc42.md +155 -0
  51. package/docs/arc42/11-risks.arc42.md +113 -0
  52. package/docs/arc42/12-glossary.arc42.md +162 -0
  53. package/docs/arc42/architecture-evidence.md +395 -0
  54. package/explainer/.arc42ignore +3 -0
  55. package/explainer/README.md +54 -0
  56. package/explainer/_gitignore +5 -0
  57. package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
  58. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
  59. package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
  60. package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
  61. package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
  62. package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
  63. package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
  64. package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
  65. package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
  66. package/explainer/e2e/explainer.e2e.ts +3 -0
  67. package/explainer/index.html +11 -0
  68. package/explainer/package.json +18 -0
  69. package/explainer/playwright.config.ts +1 -0
  70. package/explainer/src/app.yaml +20 -0
  71. package/explainer/src/components/ProzentFeld.test.ts +30 -0
  72. package/explainer/src/components/ProzentFeld.ts +121 -0
  73. package/explainer/src/components/ProzentFeld.vue +84 -0
  74. package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
  75. package/explainer/src/main.ts +4 -0
  76. package/explainer/src/prozent/chapter.yaml +18 -0
  77. package/explainer/src/prozent/grundwert.yaml +115 -0
  78. package/explainer/src/prozent/prozentsatz.yaml +99 -0
  79. package/explainer/src/prozent/prozentwert.yaml +119 -0
  80. package/explainer/tsconfig.json +1 -0
  81. package/explainer/vite.config.ts +4 -0
  82. package/interactive/.arc42ignore +5 -0
  83. package/interactive/README.md +28 -0
  84. package/interactive/_gitignore +4 -0
  85. package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
  86. package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
  87. package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
  88. package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
  89. package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
  90. package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
  91. package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
  92. package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
  93. package/interactive/e2e/interactive.e2e.ts +3 -0
  94. package/interactive/index.html +11 -0
  95. package/interactive/package.json +18 -0
  96. package/interactive/playwright.config.ts +1 -0
  97. package/interactive/src/app.yaml +28 -0
  98. package/interactive/src/core/index.ts +101 -0
  99. package/interactive/src/data/lessons.json +4 -0
  100. package/interactive/src/env.d.ts +6 -0
  101. package/interactive/src/main.ts +6 -0
  102. package/interactive/src/messages/de.json +12 -0
  103. package/interactive/src/playthroughs/grundreihe.yaml +33 -0
  104. package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
  105. package/interactive/src/ui/App.vue +85 -0
  106. package/interactive/tsconfig.json +1 -0
  107. package/interactive/vite.config.ts +4 -0
  108. package/package.json +142 -4
  109. package/quiz/README.md +31 -0
  110. package/quiz/_gitignore +5 -0
  111. package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
  112. package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
  113. package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
  114. package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
  115. package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
  116. package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
  117. package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
  118. package/quiz/e2e/quiz.e2e.ts +3 -0
  119. package/quiz/index.html +11 -0
  120. package/quiz/package.json +18 -0
  121. package/quiz/playwright.config.ts +1 -0
  122. package/quiz/src/images/berge-ordnen.jpg +0 -0
  123. package/quiz/src/images/everest-hoehe.jpg +0 -0
  124. package/quiz/src/images/galapagos.jpg +0 -0
  125. package/quiz/src/images/grand-canyon.jpg +0 -0
  126. package/quiz/src/images/totes-meer.jpg +0 -0
  127. package/quiz/src/images/uluru.jpg +0 -0
  128. package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
  129. package/quiz/src/main.ts +4 -0
  130. package/quiz/src/quiz.json +312 -0
  131. package/quiz/tsconfig.json +1 -0
  132. package/quiz/vite.config.ts +4 -0
  133. package/runtime/explainer/assessment-client.ts +478 -0
  134. package/runtime/explainer/assessment-page.ts +446 -0
  135. package/runtime/explainer/assessment.ts +393 -0
  136. package/runtime/explainer/check-page.ts +520 -0
  137. package/runtime/explainer/components/exercise.ts +409 -0
  138. package/runtime/explainer/components/props.ts +230 -0
  139. package/runtime/explainer/components/read.ts +428 -0
  140. package/runtime/explainer/content.ts +277 -0
  141. package/runtime/explainer/e2e.ts +1474 -0
  142. package/runtime/explainer/exercises.ts +324 -0
  143. package/runtime/explainer/explainer.css +1283 -0
  144. package/runtime/explainer/explainer.ts +838 -0
  145. package/runtime/explainer/explainer.v1.schema.json +1165 -0
  146. package/runtime/explainer/expression.ts +407 -0
  147. package/runtime/explainer/fraction.ts +107 -0
  148. package/runtime/explainer/generated/comparator.ts +31 -0
  149. package/runtime/explainer/generated/question.ts +165 -0
  150. package/runtime/explainer/generated/soundness.ts +280 -0
  151. package/runtime/explainer/generated/texts.ts +257 -0
  152. package/runtime/explainer/generated/types.ts +103 -0
  153. package/runtime/explainer/generated/values.ts +136 -0
  154. package/runtime/explainer/islands.ts +281 -0
  155. package/runtime/explainer/load.ts +457 -0
  156. package/runtime/explainer/messages/de.json +200 -0
  157. package/runtime/explainer/messages/en.json +200 -0
  158. package/runtime/explainer/messages.ts +13 -0
  159. package/runtime/explainer/needs.ts +122 -0
  160. package/runtime/explainer/open-task.ts +235 -0
  161. package/runtime/explainer/outline.ts +299 -0
  162. package/runtime/explainer/plugin.ts +488 -0
  163. package/runtime/explainer/position.ts +62 -0
  164. package/runtime/explainer/prompt.ts +75 -0
  165. package/runtime/explainer/render.ts +686 -0
  166. package/runtime/explainer/result.ts +238 -0
  167. package/runtime/explainer/routes.ts +119 -0
  168. package/runtime/explainer/schema.ts +140 -0
  169. package/runtime/explainer/scope.ts +87 -0
  170. package/runtime/explainer/stuck.ts +57 -0
  171. package/runtime/explainer/tasks.ts +109 -0
  172. package/runtime/explainer/tutor.ts +469 -0
  173. package/runtime/explainer/tutorpage.ts +669 -0
  174. package/runtime/explainer/video.ts +123 -0
  175. package/runtime/interactive/contract.ts +121 -0
  176. package/runtime/interactive/e2e.ts +190 -0
  177. package/runtime/interactive/interactive.css +82 -0
  178. package/runtime/interactive/interactive.ts +115 -0
  179. package/runtime/interactive/interactive.v1.schema.json +146 -0
  180. package/runtime/interactive/load.ts +188 -0
  181. package/runtime/interactive/messages/de.json +14 -0
  182. package/runtime/interactive/messages/en.json +14 -0
  183. package/runtime/interactive/messages.ts +11 -0
  184. package/runtime/interactive/playthrough.v1.schema.json +143 -0
  185. package/runtime/interactive/plugin.ts +266 -0
  186. package/runtime/interactive/render.ts +126 -0
  187. package/runtime/quiz/e2e.ts +233 -0
  188. package/runtime/quiz/engine.ts +242 -0
  189. package/runtime/quiz/messages/de.json +23 -0
  190. package/runtime/quiz/messages/en.json +23 -0
  191. package/runtime/quiz/plugin.ts +150 -0
  192. package/runtime/quiz/quiz.css +154 -0
  193. package/runtime/quiz/quiz.ts +56 -0
  194. package/runtime/quiz/quiz.v1.schema.json +72 -0
  195. package/runtime/quiz/render.ts +173 -0
  196. package/runtime/quiz/validate.ts +108 -0
  197. package/runtime/shared/appname.ts +16 -0
  198. package/runtime/shared/blocks/block.ts +162 -0
  199. package/runtime/shared/blocks/blocks.css +190 -0
  200. package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
  201. package/runtime/shared/blocks/code.ts +26 -0
  202. package/runtime/shared/blocks/figures.ts +49 -0
  203. package/runtime/shared/blocks/image.ts +25 -0
  204. package/runtime/shared/blocks/index.ts +13 -0
  205. package/runtime/shared/blocks/messages/de.json +4 -0
  206. package/runtime/shared/blocks/messages/en.json +4 -0
  207. package/runtime/shared/blocks/render.ts +134 -0
  208. package/runtime/shared/blocks/validate.ts +165 -0
  209. package/runtime/shared/content.ts +197 -0
  210. package/runtime/shared/examples.ts +20 -0
  211. package/runtime/shared/html.ts +10 -0
  212. package/runtime/shared/languages.ts +94 -0
  213. package/runtime/shared/page.css +101 -0
  214. package/runtime/shared/pages.ts +114 -0
  215. package/runtime/shared/questions/e2e.ts +115 -0
  216. package/runtime/shared/questions/engine.ts +209 -0
  217. package/runtime/shared/questions/index.ts +39 -0
  218. package/runtime/shared/questions/messages/de.json +26 -0
  219. package/runtime/shared/questions/messages/en.json +26 -0
  220. package/runtime/shared/questions/question.ts +430 -0
  221. package/runtime/shared/questions/question.v1.schema.json +342 -0
  222. package/runtime/shared/questions/questions.css +192 -0
  223. package/runtime/shared/questions/render.ts +247 -0
  224. package/runtime/shared/questions/validate.ts +518 -0
  225. package/runtime/shared/schema.ts +44 -0
  226. package/runtime/shared/text/check.ts +68 -0
  227. package/runtime/shared/text/formula.ts +69 -0
  228. package/runtime/shared/text/markdown.ts +185 -0
  229. package/runtime/shared/text/text.v1.schema.json +34 -0
  230. package/runtime/shared/vue-code.ts +395 -0
  231. package/runtime/shared/vue.ts +38 -0
@@ -0,0 +1,1165 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:lernapps:schema:explainer.v1",
4
+ "title": "Content of a lernapps explainer",
5
+ "description": "The content of an app built from the archetype explainer (runtime @lernapps/app-templates), as YAML in src/: the app (src/app.yaml), each chapter (src/<chapter>/chapter.yaml), each page (src/<chapter>/<page>.yaml) and data files (src/data/<name>.yaml). A page is a list of blocks from a closed set; structure comes only from section blocks. The build validates every file against this schema, then what a schema cannot say (order lists complete, depth, languages complete, formulas in the command list), and fails with one line per problem. Every text is for learners, in plain language. A text is a string while German is the app's only language, else a map of texts by language: { de: …, en: … }.",
6
+ "allOf": [
7
+ {
8
+ "if": {
9
+ "type": "object",
10
+ "required": ["chapters"],
11
+ "properties": {
12
+ "chapters": {}
13
+ }
14
+ },
15
+ "then": {
16
+ "$ref": "#/$defs/app"
17
+ }
18
+ },
19
+ {
20
+ "if": {
21
+ "type": "object",
22
+ "required": ["pages"],
23
+ "properties": {
24
+ "pages": {}
25
+ }
26
+ },
27
+ "then": {
28
+ "$ref": "#/$defs/chapter"
29
+ }
30
+ },
31
+ {
32
+ "if": {
33
+ "type": "object",
34
+ "required": ["blocks"],
35
+ "properties": {
36
+ "blocks": {}
37
+ }
38
+ },
39
+ "then": {
40
+ "$ref": "#/$defs/page"
41
+ }
42
+ },
43
+ {
44
+ "if": {
45
+ "type": "array"
46
+ },
47
+ "then": {
48
+ "$ref": "#/$defs/data"
49
+ }
50
+ }
51
+ ],
52
+ "$defs": {
53
+ "id": {
54
+ "description": "A stable name, part of the address: lower-case letters, digits and hyphens. Keep it once published, so that links to it keep working.",
55
+ "type": "string",
56
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
57
+ },
58
+ "text": {
59
+ "$ref": "urn:lernapps:schema:text.v1#/$defs/text"
60
+ },
61
+ "markdown": {
62
+ "$ref": "urn:lernapps:schema:text.v1#/$defs/markdown"
63
+ },
64
+ "latex": {
65
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/latex"
66
+ },
67
+ "source": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "required": ["title"],
71
+ "properties": {
72
+ "title": {
73
+ "description": "What the source is, e.g. the title of the article or book.",
74
+ "$ref": "#/$defs/text"
75
+ },
76
+ "url": {
77
+ "description": "Where to read it.",
78
+ "type": "string",
79
+ "pattern": "^https?://\\S+$"
80
+ }
81
+ },
82
+ "description": "Where the app's facts come from."
83
+ },
84
+ "credit": {
85
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/credit"
86
+ },
87
+ "image": {
88
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/image"
89
+ },
90
+ "app": {
91
+ "description": "src/app.yaml: the app, its languages and the order of its chapters.",
92
+ "type": "object",
93
+ "additionalProperties": false,
94
+ "required": ["title", "intro", "languages", "chapters"],
95
+ "properties": {
96
+ "title": {
97
+ "description": "The app's title: the start page's h1 and the title of every page.",
98
+ "$ref": "#/$defs/text"
99
+ },
100
+ "intro": {
101
+ "description": "What the app explains and for whom, under the title of the start page.",
102
+ "$ref": "#/$defs/markdown"
103
+ },
104
+ "languages": {
105
+ "description": "The languages of the app, German first. Every text is then complete in each of them; each language other than German gets its own page tree below /<language>/.",
106
+ "type": "array",
107
+ "minItems": 1,
108
+ "items": {
109
+ "type": "string",
110
+ "pattern": "^[a-z]{2}$"
111
+ }
112
+ },
113
+ "chapters": {
114
+ "description": "The order of the chapters: the id of each folder src/<chapter>/, each exactly once. An app with one chapter hides it in the navigation and keeps it in the address.",
115
+ "type": "array",
116
+ "minItems": 1,
117
+ "items": {
118
+ "$ref": "#/$defs/id"
119
+ }
120
+ },
121
+ "sources": {
122
+ "description": "Where the app's facts come from, listed on the start page.",
123
+ "type": "array",
124
+ "minItems": 1,
125
+ "items": {
126
+ "$ref": "#/$defs/source"
127
+ }
128
+ },
129
+ "tutor": {
130
+ "description": "Optional: what the app adds for a KI tutor, an assistant the learner brings. The build writes, per language tree, a short tutor page per content page (<chapter>/<page>/tutor/, its Markdown beside it as index.md), the one source of what a tutor gets: lean HTML, visible to everyone: who the tutor helps and how it guides, where the child is, the app's own rules; then the page's content as context and its exercises, a generated one once as its template with a table of each task number's inputs and outputs (the values, the solution, what each typical mistake gives), an exercise component with each task number's task and solution as far as they fit. The learner switches an assistant on once, on the start page or beside the title of a page, kept on the device; then every page gets a bar beside its title, \"Mit <name> lernen\" (way A: the assistant opens with one sentence that names the page's tutor page with the page's task numbers; nothing loaded before a click), and every exercise gets \"Hilfe\", \"Warum?\" after a wrong answer (way B: the tutor page and the task, the input and the app's message word for word, as a new chat, or copied on a page opened with via=tutor); a page opened with via=tutor gets \"Zurück zum Tutor\", which copies a result line with the address of the page's tutor page.",
131
+ "type": "object",
132
+ "additionalProperties": false,
133
+ "properties": {
134
+ "rules": {
135
+ "description": "The app's own rules for a tutor, after the guide on each tutor page: what matters in this subject, e.g. \"Rechenwege auf Papier, das Ergebnis in die App\". Restricted Markdown, in the app's languages.",
136
+ "$ref": "#/$defs/markdown"
137
+ }
138
+ }
139
+ },
140
+ "scope": {
141
+ "description": "The scope for the tutor, never shown to the learner: with it every page has its scope, and the build writes the scope beside each page, chapter and the app, linked from every tutor page. The tutor judges open answers by it, not by general knowledge.",
142
+ "type": "object",
143
+ "additionalProperties": false,
144
+ "required": ["out"],
145
+ "properties": {
146
+ "text": {
147
+ "description": "Optional: what the app is about as a whole, in your own words, and where its weight lies (what a test asks most). What belongs to the scope is what the pages' scopes say.",
148
+ "$ref": "#/$defs/markdown"
149
+ },
150
+ "out": {
151
+ "description": "What does not belong to it, one item each: what a learner or tutor might expect but the material does not teach.",
152
+ "type": "array",
153
+ "minItems": 1,
154
+ "items": {
155
+ "$ref": "#/$defs/text"
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ },
162
+ "chapter": {
163
+ "description": "src/<chapter>/chapter.yaml: a chapter and the order of its pages.",
164
+ "type": "object",
165
+ "additionalProperties": false,
166
+ "required": ["title", "pages"],
167
+ "properties": {
168
+ "title": {
169
+ "description": "The chapter's title.",
170
+ "$ref": "#/$defs/text"
171
+ },
172
+ "summary": {
173
+ "description": "What the chapter is about, on the chapter's page and in the contents.",
174
+ "$ref": "#/$defs/markdown"
175
+ },
176
+ "pages": {
177
+ "description": "The order of the pages: the id of each file src/<chapter>/<page>.yaml, each exactly once.",
178
+ "type": "array",
179
+ "minItems": 1,
180
+ "items": {
181
+ "$ref": "#/$defs/id"
182
+ }
183
+ },
184
+ "selfAssessment": {
185
+ "description": "Optional: the chapter's self-assessment, at <chapter>/einschaetzung/, offered on the chapter's page. The app's, at einschaetzung/ and offered on the start page, is every chapter's in the order of the chapters; in an app with one chapter it is the only one.",
186
+ "$ref": "#/$defs/selfAssessment"
187
+ },
188
+ "scope": {
189
+ "description": "Optional, for the tutor: what the chapter is about as a whole, in your own words; its pages' scopes follow it at <chapter>/scope/.",
190
+ "$ref": "#/$defs/markdown"
191
+ }
192
+ }
193
+ },
194
+ "page": {
195
+ "description": "src/<chapter>/<page>.yaml: a page, its address /<chapter>/<page>/. Its blocks in order; structure by section blocks (h2), with at most one level of sections inside them (h3). Every page has at least one exercise (one of the quiz's question types, or generated), e.g. in a last section Übung.",
196
+ "type": "object",
197
+ "additionalProperties": false,
198
+ "required": ["title", "blocks"],
199
+ "properties": {
200
+ "title": {
201
+ "description": "The page's title: its h1.",
202
+ "$ref": "#/$defs/text"
203
+ },
204
+ "description": {
205
+ "description": "One sentence on what the page explains, for search engines.",
206
+ "$ref": "#/$defs/text"
207
+ },
208
+ "blocks": {
209
+ "$ref": "#/$defs/blocks"
210
+ },
211
+ "scope": {
212
+ "description": "For the tutor, never shown to the learner: what this page teaches, in your own words from the material, as continuous prose with the exact content (facts, terms, reasons, examples). Required on every page when src/app.yaml has scope. The build writes it beside the page at scope/, with what belongs to the scope and what does not.",
213
+ "$ref": "#/$defs/markdown"
214
+ }
215
+ }
216
+ },
217
+ "dataValue": {
218
+ "description": "A value of a data record: a number, a truth value, a text (a string, or a map by language), a list of values, or a record of values.",
219
+ "anyOf": [
220
+ {
221
+ "type": "number"
222
+ },
223
+ {
224
+ "type": "boolean"
225
+ },
226
+ {
227
+ "$ref": "#/$defs/text"
228
+ },
229
+ {
230
+ "type": "array",
231
+ "items": {
232
+ "$ref": "#/$defs/dataValue"
233
+ }
234
+ },
235
+ {
236
+ "type": "object",
237
+ "minProperties": 1,
238
+ "additionalProperties": {
239
+ "$ref": "#/$defs/dataValue"
240
+ }
241
+ }
242
+ ]
243
+ },
244
+ "data": {
245
+ "description": "src/data/<name>.yaml: a list of records with the same fields, e.g. events with date, title and text. Blocks draw from it by its name (a timeline's data). A field may be a list or a record of values too (compounds: [{ formula, text }]), for a component that takes the file as a list prop; exercises and timelines use the flat fields. Only a timeline (data), a generated exercise (from: { data: <name> }) and a component prop ({ data: <name> }) read a data file; the check reports a file that none of them names (explainer-unused-data).",
246
+ "type": "array",
247
+ "minItems": 1,
248
+ "items": {
249
+ "type": "object",
250
+ "minProperties": 1,
251
+ "propertyNames": {
252
+ "pattern": "^[a-z][a-zA-Z0-9]*$"
253
+ },
254
+ "additionalProperties": {
255
+ "$ref": "#/$defs/dataValue"
256
+ }
257
+ }
258
+ },
259
+ "blocks": {
260
+ "type": "array",
261
+ "minItems": 1,
262
+ "items": {
263
+ "$ref": "#/$defs/block"
264
+ }
265
+ },
266
+ "block": {
267
+ "type": "object",
268
+ "required": ["type"],
269
+ "properties": {
270
+ "type": {
271
+ "description": "The kind of block.",
272
+ "enum": [
273
+ "text",
274
+ "formula",
275
+ "figure",
276
+ "labelled-figure",
277
+ "table",
278
+ "timeline",
279
+ "definition",
280
+ "quote",
281
+ "code",
282
+ "video",
283
+ "example",
284
+ "note",
285
+ "section",
286
+ "single-choice",
287
+ "multiple-choice",
288
+ "true-false",
289
+ "number",
290
+ "ordering",
291
+ "matching",
292
+ "generated",
293
+ "open-task",
294
+ "vue-component"
295
+ ]
296
+ }
297
+ },
298
+ "allOf": [
299
+ {
300
+ "if": {
301
+ "properties": {
302
+ "type": {
303
+ "const": "text"
304
+ }
305
+ },
306
+ "required": ["type"],
307
+ "type": "object"
308
+ },
309
+ "then": {
310
+ "$ref": "#/$defs/textBlock"
311
+ }
312
+ },
313
+ {
314
+ "if": {
315
+ "properties": {
316
+ "type": {
317
+ "const": "formula"
318
+ }
319
+ },
320
+ "required": ["type"],
321
+ "type": "object"
322
+ },
323
+ "then": {
324
+ "$ref": "#/$defs/formula"
325
+ }
326
+ },
327
+ {
328
+ "if": {
329
+ "properties": {
330
+ "type": {
331
+ "const": "figure"
332
+ }
333
+ },
334
+ "required": ["type"],
335
+ "type": "object"
336
+ },
337
+ "then": {
338
+ "$ref": "#/$defs/figure"
339
+ }
340
+ },
341
+ {
342
+ "if": {
343
+ "properties": {
344
+ "type": {
345
+ "const": "labelled-figure"
346
+ }
347
+ },
348
+ "required": ["type"],
349
+ "type": "object"
350
+ },
351
+ "then": {
352
+ "$ref": "#/$defs/labelledFigure"
353
+ }
354
+ },
355
+ {
356
+ "if": {
357
+ "properties": {
358
+ "type": {
359
+ "const": "table"
360
+ }
361
+ },
362
+ "required": ["type"],
363
+ "type": "object"
364
+ },
365
+ "then": {
366
+ "$ref": "#/$defs/table"
367
+ }
368
+ },
369
+ {
370
+ "if": {
371
+ "properties": {
372
+ "type": {
373
+ "const": "timeline"
374
+ }
375
+ },
376
+ "required": ["type"],
377
+ "type": "object"
378
+ },
379
+ "then": {
380
+ "$ref": "#/$defs/timeline"
381
+ }
382
+ },
383
+ {
384
+ "if": {
385
+ "properties": {
386
+ "type": {
387
+ "const": "definition"
388
+ }
389
+ },
390
+ "required": ["type"],
391
+ "type": "object"
392
+ },
393
+ "then": {
394
+ "$ref": "#/$defs/definition"
395
+ }
396
+ },
397
+ {
398
+ "if": {
399
+ "properties": {
400
+ "type": {
401
+ "const": "quote"
402
+ }
403
+ },
404
+ "required": ["type"],
405
+ "type": "object"
406
+ },
407
+ "then": {
408
+ "$ref": "#/$defs/quote"
409
+ }
410
+ },
411
+ {
412
+ "if": {
413
+ "properties": {
414
+ "type": {
415
+ "const": "code"
416
+ }
417
+ },
418
+ "required": ["type"],
419
+ "type": "object"
420
+ },
421
+ "then": {
422
+ "$ref": "#/$defs/code"
423
+ }
424
+ },
425
+ {
426
+ "if": {
427
+ "properties": {
428
+ "type": {
429
+ "const": "video"
430
+ }
431
+ },
432
+ "required": ["type"],
433
+ "type": "object"
434
+ },
435
+ "then": {
436
+ "$ref": "#/$defs/video"
437
+ }
438
+ },
439
+ {
440
+ "if": {
441
+ "properties": {
442
+ "type": {
443
+ "const": "example"
444
+ }
445
+ },
446
+ "required": ["type"],
447
+ "type": "object"
448
+ },
449
+ "then": {
450
+ "$ref": "#/$defs/box"
451
+ }
452
+ },
453
+ {
454
+ "if": {
455
+ "properties": {
456
+ "type": {
457
+ "const": "note"
458
+ }
459
+ },
460
+ "required": ["type"],
461
+ "type": "object"
462
+ },
463
+ "then": {
464
+ "$ref": "#/$defs/box"
465
+ }
466
+ },
467
+ {
468
+ "if": {
469
+ "properties": {
470
+ "type": {
471
+ "const": "section"
472
+ }
473
+ },
474
+ "required": ["type"],
475
+ "type": "object"
476
+ },
477
+ "then": {
478
+ "$ref": "#/$defs/section"
479
+ }
480
+ },
481
+ {
482
+ "if": {
483
+ "properties": {
484
+ "type": {
485
+ "const": "single-choice"
486
+ }
487
+ },
488
+ "required": ["type"],
489
+ "type": "object"
490
+ },
491
+ "then": {
492
+ "$ref": "urn:lernapps:schema:question.v1#/$defs/question"
493
+ }
494
+ },
495
+ {
496
+ "if": {
497
+ "properties": {
498
+ "type": {
499
+ "const": "multiple-choice"
500
+ }
501
+ },
502
+ "required": ["type"],
503
+ "type": "object"
504
+ },
505
+ "then": {
506
+ "$ref": "urn:lernapps:schema:question.v1#/$defs/question"
507
+ }
508
+ },
509
+ {
510
+ "if": {
511
+ "properties": {
512
+ "type": {
513
+ "const": "true-false"
514
+ }
515
+ },
516
+ "required": ["type"],
517
+ "type": "object"
518
+ },
519
+ "then": {
520
+ "$ref": "urn:lernapps:schema:question.v1#/$defs/question"
521
+ }
522
+ },
523
+ {
524
+ "if": {
525
+ "properties": {
526
+ "type": {
527
+ "const": "number"
528
+ }
529
+ },
530
+ "required": ["type"],
531
+ "type": "object"
532
+ },
533
+ "then": {
534
+ "$ref": "urn:lernapps:schema:question.v1#/$defs/question"
535
+ }
536
+ },
537
+ {
538
+ "if": {
539
+ "properties": {
540
+ "type": {
541
+ "const": "ordering"
542
+ }
543
+ },
544
+ "required": ["type"],
545
+ "type": "object"
546
+ },
547
+ "then": {
548
+ "$ref": "urn:lernapps:schema:question.v1#/$defs/question"
549
+ }
550
+ },
551
+ {
552
+ "if": {
553
+ "properties": {
554
+ "type": {
555
+ "const": "matching"
556
+ }
557
+ },
558
+ "required": ["type"],
559
+ "type": "object"
560
+ },
561
+ "then": {
562
+ "$ref": "urn:lernapps:schema:question.v1#/$defs/question"
563
+ }
564
+ },
565
+ {
566
+ "if": {
567
+ "properties": {
568
+ "type": {
569
+ "const": "generated"
570
+ }
571
+ },
572
+ "required": ["type"],
573
+ "type": "object"
574
+ },
575
+ "then": {
576
+ "$ref": "#/$defs/generated"
577
+ }
578
+ },
579
+ {
580
+ "if": {
581
+ "properties": {
582
+ "type": {
583
+ "const": "open-task"
584
+ }
585
+ },
586
+ "required": ["type"],
587
+ "type": "object"
588
+ },
589
+ "then": {
590
+ "$ref": "#/$defs/openTask"
591
+ }
592
+ },
593
+ {
594
+ "if": {
595
+ "properties": {
596
+ "type": {
597
+ "const": "vue-component"
598
+ }
599
+ },
600
+ "required": ["type"],
601
+ "type": "object"
602
+ },
603
+ "then": {
604
+ "$ref": "#/$defs/vueComponent"
605
+ }
606
+ }
607
+ ],
608
+ "unevaluatedProperties": false
609
+ },
610
+ "textBlock": {
611
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/textBlock"
612
+ },
613
+ "formula": {
614
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/formula"
615
+ },
616
+ "figure": {
617
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/figure"
618
+ },
619
+ "drawing": {
620
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/drawing"
621
+ },
622
+ "labelledFigure": {
623
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/labelledFigure"
624
+ },
625
+ "table": {
626
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/table"
627
+ },
628
+ "timeline": {
629
+ "description": "Dated events, from the earliest: listed here (events) or the records of a data file (data) with the fields date, title and optional label and text; one of the two.",
630
+ "properties": {
631
+ "events": {
632
+ "type": "array",
633
+ "minItems": 1,
634
+ "items": {
635
+ "$ref": "#/$defs/event"
636
+ }
637
+ },
638
+ "data": {
639
+ "description": "The name of a data file src/data/<name>.yaml.",
640
+ "$ref": "#/$defs/id"
641
+ }
642
+ },
643
+ "type": "object"
644
+ },
645
+ "event": {
646
+ "type": "object",
647
+ "additionalProperties": false,
648
+ "required": ["date", "title"],
649
+ "properties": {
650
+ "date": {
651
+ "description": "The date: a year (1789; -44 for 44 BC), a month (1789-07) or a day (1789-07-14).",
652
+ "anyOf": [
653
+ {
654
+ "type": "integer"
655
+ },
656
+ {
657
+ "type": "string",
658
+ "pattern": "^-?\\d{1,4}(-\\d{2}(-\\d{2})?)?$"
659
+ }
660
+ ]
661
+ },
662
+ "label": {
663
+ "description": "The date as the learner reads it, when it differs from the date itself, e.g. um 1500.",
664
+ "$ref": "#/$defs/text"
665
+ },
666
+ "title": {
667
+ "description": "What happened, in a few words.",
668
+ "$ref": "#/$defs/text"
669
+ },
670
+ "text": {
671
+ "description": "More on it.",
672
+ "$ref": "#/$defs/markdown"
673
+ }
674
+ }
675
+ },
676
+ "definition": {
677
+ "description": "A term and what it means, in place; collected into the app's glossary page, which every term links to.",
678
+ "required": ["term", "explanation"],
679
+ "properties": {
680
+ "term": {
681
+ "description": "The term.",
682
+ "$ref": "#/$defs/text"
683
+ },
684
+ "explanation": {
685
+ "description": "What it means.",
686
+ "$ref": "#/$defs/markdown"
687
+ }
688
+ },
689
+ "type": "object"
690
+ },
691
+ "quote": {
692
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/quote"
693
+ },
694
+ "code": {
695
+ "$ref": "urn:lernapps:schema:blocks.v1#/$defs/code"
696
+ },
697
+ "video": {
698
+ "description": "A video on YouTube, behind two clicks. Without JavaScript and before a click the page shows its title as a link to the video on youtube.com, its channel and a note that playing it sends data (among them the learner's IP address) to YouTube/Google; nothing loads from YouTube or Google. With JavaScript a button \"Video abspielen\" loads the player from youtube-nocookie.com; the choice \"Videos immer direkt laden\", kept on the device until the learner clears it, loads every player without that click from then on. No thumbnail: YouTube's would load before a click. llms.txt and the page's tutor file name the video as a link. An app with a video loads from another server after a click: its catalog entry says fitness.thirdParty: on-click.",
699
+ "required": ["youtube", "title", "channel"],
700
+ "properties": {
701
+ "youtube": {
702
+ "description": "The video's id on YouTube: the 11 characters after watch?v= in its address, e.g. rkLmLaFHodo for https://www.youtube.com/watch?v=rkLmLaFHodo.",
703
+ "type": "string",
704
+ "pattern": "^[A-Za-z0-9_-]{11}$"
705
+ },
706
+ "title": {
707
+ "description": "The video's title as YouTube shows it, in the video's language: the text of the link and the name of the player.",
708
+ "type": "string",
709
+ "minLength": 1,
710
+ "pattern": "\\S"
711
+ },
712
+ "channel": {
713
+ "description": "The channel that published it, as YouTube names it, e.g. Lehrerschmidt.",
714
+ "type": "string",
715
+ "minLength": 1,
716
+ "pattern": "\\S"
717
+ }
718
+ },
719
+ "type": "object"
720
+ },
721
+ "box": {
722
+ "description": "A box with a heading (the next level below its place) and blocks: example for a worked example, note for a hint or something to remember. A box holds no sections and no boxes.",
723
+ "required": ["title", "blocks"],
724
+ "properties": {
725
+ "title": {
726
+ "$ref": "#/$defs/text"
727
+ },
728
+ "blocks": {
729
+ "$ref": "#/$defs/blocks"
730
+ }
731
+ },
732
+ "type": "object"
733
+ },
734
+ "section": {
735
+ "description": "A section: a heading (h2, or h3 inside a section) and its blocks. Its address is #<id>, inside a section #<section>/<id>.",
736
+ "required": ["id", "title", "blocks"],
737
+ "properties": {
738
+ "id": {
739
+ "$ref": "#/$defs/id"
740
+ },
741
+ "title": {
742
+ "$ref": "#/$defs/text"
743
+ },
744
+ "blocks": {
745
+ "$ref": "#/$defs/blocks"
746
+ }
747
+ },
748
+ "type": "object"
749
+ },
750
+ "expression": {
751
+ "description": "An expression of the expression language, e.g. \"G * p / 100\": numbers (exact fractions), + - * /, parentheses, == != < <= > >=, and, or, not, strings in quotes, record fields (e.year), list index (xs[0]), lists ([1, 2]), and the functions round(x, n), decimals, map(xs, field), sortBy(xs, field), reverse. It may use the variables of given.",
752
+ "type": "string",
753
+ "minLength": 1,
754
+ "pattern": "\\S"
755
+ },
756
+ "literal": {
757
+ "description": "An item of a list: a number, a string, or a text by language ({ de: …, en: … }).",
758
+ "type": ["number", "string", "object"],
759
+ "minLength": 1,
760
+ "additionalProperties": {
761
+ "type": "string",
762
+ "minLength": 1
763
+ }
764
+ },
765
+ "list": {
766
+ "description": "A list of items, or the records of a data file: { data: <name> } for src/data/<name>.yaml.",
767
+ "type": ["array", "object"],
768
+ "minItems": 1,
769
+ "items": {
770
+ "$ref": "#/$defs/literal"
771
+ },
772
+ "required": ["data"],
773
+ "additionalProperties": false,
774
+ "properties": {
775
+ "data": {
776
+ "$ref": "#/$defs/id"
777
+ }
778
+ }
779
+ },
780
+ "arbitrary": {
781
+ "description": "How a value of given is drawn, exactly one of: int: [min, max] with a step (default 1); decimal: [min, max] with a step (e.g. 0.1); oneOf: a list to pick one from; pick: how many, from: a list or { data: <name> }, distinct: true for no repeats (pick: 1 gives one item, more give a list).",
782
+ "type": "object",
783
+ "additionalProperties": false,
784
+ "minProperties": 1,
785
+ "properties": {
786
+ "int": {
787
+ "type": "array",
788
+ "minItems": 2,
789
+ "maxItems": 2,
790
+ "items": {
791
+ "type": "integer"
792
+ }
793
+ },
794
+ "decimal": {
795
+ "type": "array",
796
+ "minItems": 2,
797
+ "maxItems": 2,
798
+ "items": {
799
+ "type": "number"
800
+ }
801
+ },
802
+ "step": {
803
+ "type": "number",
804
+ "exclusiveMinimum": 0
805
+ },
806
+ "oneOf": {
807
+ "type": "array",
808
+ "minItems": 1,
809
+ "items": {
810
+ "$ref": "#/$defs/literal"
811
+ }
812
+ },
813
+ "pick": {
814
+ "type": "integer",
815
+ "minimum": 1
816
+ },
817
+ "from": {
818
+ "$ref": "#/$defs/list"
819
+ },
820
+ "distinct": {
821
+ "type": "boolean"
822
+ }
823
+ }
824
+ },
825
+ "numberCompare": {
826
+ "description": "The learner types a number. format: integer (no decimals), decimal (decimals, default 2), money (2 decimals, unit €) or percent (unit %); decimals: how many the answer may have; tolerance: how far the learner's number may be off (default 0); unit: shown next to the input.",
827
+ "type": "object",
828
+ "additionalProperties": false,
829
+ "properties": {
830
+ "format": {
831
+ "enum": ["integer", "decimal", "money", "percent"]
832
+ },
833
+ "decimals": {
834
+ "type": "integer",
835
+ "minimum": 0,
836
+ "maximum": 6
837
+ },
838
+ "tolerance": {
839
+ "type": "number",
840
+ "minimum": 0
841
+ },
842
+ "unit": {
843
+ "$ref": "#/$defs/text"
844
+ }
845
+ }
846
+ },
847
+ "compare": {
848
+ "description": "How the learner answers and what counts as right, the quiz's answer kind of the same name: number (the quiz's number question; or { number: { format, decimals, tolerance, unit } }), { choice: <options> } (single choice; the options a list, or an expression giving one), order (the answer a list in the right order; the quiz's ordering), pairs (the answer a list of [left, right]; or { pairs: { left: <field>, right: <field> } } for a list of records; the quiz's matching).",
849
+ "type": ["string", "object"],
850
+ "pattern": "^(number|order|pairs)$",
851
+ "minProperties": 1,
852
+ "maxProperties": 1,
853
+ "additionalProperties": false,
854
+ "properties": {
855
+ "number": {
856
+ "$ref": "#/$defs/numberCompare"
857
+ },
858
+ "choice": {
859
+ "type": ["array", "string"],
860
+ "minItems": 2,
861
+ "minLength": 1,
862
+ "items": {
863
+ "$ref": "#/$defs/literal"
864
+ }
865
+ },
866
+ "pairs": {
867
+ "type": "object",
868
+ "additionalProperties": false,
869
+ "required": ["left", "right"],
870
+ "properties": {
871
+ "left": {
872
+ "type": "string",
873
+ "minLength": 1
874
+ },
875
+ "right": {
876
+ "type": "string",
877
+ "minLength": 1
878
+ }
879
+ }
880
+ }
881
+ }
882
+ },
883
+ "generated": {
884
+ "description": "An exercise drawn from a seed, property-based: given draws the values, where filters them, task is the text with placeholders ({{G}}, {{e.name}}; {{answer}} in the steps), answer is an expression with its comparator, mistakes are typical wrong answers each with a message saying what went wrong, hint and steps help. A placeholder is always two braces around a declared name or its field, nothing else inside, in prose and in formulas alike ($\\frac{{{W}}}{{{G}}}$); an index is no field, so {{opts[0].x}} is no placeholder and the build reports it: draw the record on its own (given: { e: { pick: 1, from: { data: name } } }) and write {{e.x}}; one brace is never a placeholder, so in a formula G and {G} are the letter. The build checks that every placeholder names a variable of given (or answer), that no text has double braces around anything but a name or its field, that no prose has a placeholder with one brace or spaces inside, and that every variable of given is used in a placeholder or in an expression of the answer or mistakes; then it checks it over 200 fixed seeds: where holds in at least 1 of 10 draws; the answer is defined, in its format and passes its comparator; no mistake equals it; the task is the same each time and keeps no placeholder. Its tasks are numbered: the build walks the seeds 1, 2, 3, … and numbers only the distinct tasks, Aufgabe Nr. 1 to N (N at most 67; fewer when the exercise has fewer distinct tasks, noted in the build output). The number is what the page shows, what a learner names to a tutor, and what the address carries: ?seed=<n>#<id> opens task number n (a number above N wraps around). Without JavaScript the page shows the tasks 1, 2 and 3 as worked tasks with their solution; with JavaScript a live task of a random number, Neue Aufgabe, which moves to the next number (after N back to 1) and writes it into the address, and the deep link: a wrong answer gets the message of the mistake it matches (else a line to look again, with the hint above) and may be tried again; the solution and the steps show after the right answer or Lösung zeigen.",
885
+ "required": ["id", "given", "task", "answer"],
886
+ "properties": {
887
+ "id": {
888
+ "$ref": "#/$defs/id"
889
+ },
890
+ "given": {
891
+ "description": "The variables and how each is drawn; names of letters, digits and _, e.g. G, p, events.",
892
+ "type": "object",
893
+ "minProperties": 1,
894
+ "propertyNames": {
895
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
896
+ },
897
+ "additionalProperties": {
898
+ "$ref": "#/$defs/arbitrary"
899
+ }
900
+ },
901
+ "where": {
902
+ "description": "Preconditions, each an expression that is true or false; a draw that fails one is drawn again.",
903
+ "type": "array",
904
+ "minItems": 1,
905
+ "items": {
906
+ "$ref": "#/$defs/expression"
907
+ }
908
+ },
909
+ "task": {
910
+ "type": "object",
911
+ "additionalProperties": false,
912
+ "required": ["text"],
913
+ "properties": {
914
+ "text": {
915
+ "description": "The task, restricted Markdown in one paragraph, with placeholders.",
916
+ "$ref": "#/$defs/markdown"
917
+ }
918
+ }
919
+ },
920
+ "answer": {
921
+ "type": "object",
922
+ "additionalProperties": false,
923
+ "required": ["value", "compare"],
924
+ "properties": {
925
+ "value": {
926
+ "$ref": "#/$defs/expression"
927
+ },
928
+ "compare": {
929
+ "$ref": "#/$defs/compare"
930
+ }
931
+ }
932
+ },
933
+ "mistakes": {
934
+ "description": "Typical mistakes: the wrong answer as an expression, and a message (plain text) that says what went wrong. A learner who gives that answer reads the message with the feedback, and may try again.",
935
+ "type": "array",
936
+ "minItems": 1,
937
+ "items": {
938
+ "type": "object",
939
+ "additionalProperties": false,
940
+ "required": ["value", "message"],
941
+ "properties": {
942
+ "value": {
943
+ "$ref": "#/$defs/expression"
944
+ },
945
+ "message": {
946
+ "$ref": "#/$defs/text"
947
+ }
948
+ }
949
+ }
950
+ },
951
+ "practice": {
952
+ "description": "Optional: the sentence above the live task, plain text, in the app's languages. Without it the page says \"Übe selbst, mit immer neuen Aufgaben:\"; write your own when the tasks change something other than tasks in general, e.g. \"Übe selbst, mit immer neuen Stoffen:\".",
953
+ "$ref": "#/$defs/text"
954
+ },
955
+ "hint": {
956
+ "description": "A hint before the answer, restricted Markdown with placeholders; with JavaScript it stays folded under the live task until the learner opens it.",
957
+ "$ref": "#/$defs/markdown"
958
+ },
959
+ "steps": {
960
+ "description": "The steps of the solution, restricted Markdown with placeholders and {{answer}}.",
961
+ "type": "array",
962
+ "minItems": 1,
963
+ "items": {
964
+ "$ref": "#/$defs/markdown"
965
+ }
966
+ },
967
+ "examples": {
968
+ "description": "Optional: test cases for the build, never shown to learners. Before you write the exercise, note which answers fulfil its text and which typical wrong answers there are; write the other correct answers the text allows into accepted (e.g. the answer rounded as the text permits: \"round(W / G * 100, 0)\"), the typical wrong ones into rejected. Each is an expression like answer.value, of the variables of given and of answer, the task's right answer, e.g. \"answer + 1\", \"reverse(answer)\". The build evaluates each for every seed it checks and runs it through the same check the page uses: an accepted one the check rejects, or a rejected one it accepts, fails the build; then the exercise checks something other than what its text asks, so change its comparator, its tolerance or its text. The mistakes already count as rejected.",
969
+ "type": "object",
970
+ "additionalProperties": false,
971
+ "minProperties": 1,
972
+ "properties": {
973
+ "accepted": {
974
+ "description": "Answers the text allows, each of which the check must accept for every seed.",
975
+ "type": "array",
976
+ "minItems": 1,
977
+ "items": {
978
+ "$ref": "#/$defs/expression"
979
+ }
980
+ },
981
+ "rejected": {
982
+ "description": "Typical wrong answers, each of which the check must reject for every seed.",
983
+ "type": "array",
984
+ "minItems": 1,
985
+ "items": {
986
+ "$ref": "#/$defs/expression"
987
+ }
988
+ }
989
+ }
990
+ }
991
+ },
992
+ "type": "object"
993
+ },
994
+ "openTask": {
995
+ "description": "An open task: the learner answers in their own words, typed or written on paper and photographed, then compares the answer with the points a good answer has and ticks those it has; the result is right (all), partly right (some) or wrong (none), the points it missed named P1, P2, …. The model answer opens only after the learner's own answer. Without JavaScript the page shows the task, the points and the model answer under Lösung. The answer stays on the device; the photo is never stored or sent. The tutor gets the points with their sources and the model answer.",
996
+ "type": "object",
997
+ "required": ["id", "task", "points", "solution"],
998
+ "properties": {
999
+ "id": {
1000
+ "$ref": "#/$defs/id"
1001
+ },
1002
+ "task": {
1003
+ "description": "The task, restricted Markdown, e.g. Erkläre, warum … Say how long the answer should be.",
1004
+ "$ref": "#/$defs/markdown"
1005
+ },
1006
+ "answer": {
1007
+ "description": "How the learner may answer: typed (text), on paper with a photo (photo), or both, the learner's choice (the default).",
1008
+ "type": "array",
1009
+ "minItems": 1,
1010
+ "uniqueItems": true,
1011
+ "items": {
1012
+ "enum": ["text", "photo"]
1013
+ }
1014
+ },
1015
+ "points": {
1016
+ "description": "What a good answer has, 2 to 5 points, each one checkable sentence the learner ticks when the answer has it, in the order of a good answer.",
1017
+ "type": "array",
1018
+ "minItems": 2,
1019
+ "maxItems": 5,
1020
+ "items": {
1021
+ "$ref": "#/$defs/openTaskPoint"
1022
+ }
1023
+ },
1024
+ "solution": {
1025
+ "description": "The model answer: a good answer in the words a learner of the class would use, with every point of points.",
1026
+ "$ref": "#/$defs/markdown"
1027
+ },
1028
+ "hint": {
1029
+ "description": "Optional: a hint that helps to start, shown before the learner compares.",
1030
+ "$ref": "#/$defs/markdown"
1031
+ }
1032
+ }
1033
+ },
1034
+ "openTaskPoint": {
1035
+ "description": "A point of a good answer: about the content, with the section of the app it comes from, or about the form of the answer (whole sentences, a thread, the words to use), without a source.",
1036
+ "type": "object",
1037
+ "required": ["text"],
1038
+ "properties": {
1039
+ "text": {
1040
+ "description": "The point as the learner checks it, one sentence.",
1041
+ "$ref": "#/$defs/text"
1042
+ },
1043
+ "kind": {
1044
+ "description": "content (the default) or form.",
1045
+ "enum": ["content", "form"]
1046
+ },
1047
+ "source": {
1048
+ "description": "Where the content of the point is, /<chapter>/<page>/#<section>: the build checks that the page and section exist, and the learner gets a link to look again.",
1049
+ "type": "string",
1050
+ "pattern": "^/[a-z0-9]+(-[a-z0-9]+)*/[a-z0-9]+(-[a-z0-9]+)*/#[a-z0-9]+(-[a-z0-9]+)*(/[a-z0-9]+(-[a-z0-9]+)*)*$"
1051
+ }
1052
+ },
1053
+ "if": {
1054
+ "properties": {
1055
+ "kind": {
1056
+ "const": "form"
1057
+ }
1058
+ },
1059
+ "required": ["kind"]
1060
+ },
1061
+ "then": {
1062
+ "properties": {
1063
+ "source": false
1064
+ }
1065
+ },
1066
+ "else": {
1067
+ "properties": {
1068
+ "source": true
1069
+ },
1070
+ "required": ["source"]
1071
+ },
1072
+ "unevaluatedProperties": false
1073
+ },
1074
+ "vueComponent": {
1075
+ "description": "A Vue single-file component of the app, src/components/<component>.vue, for what no built-in block or generated exercise can express. The build renders it into the page and checks it (what it may do, its test file, and for an exercise, one that emits answer, its logic module <component>.ts); in the browser it hydrates in place. An exercise component is only the input of its task: it emits answer with the learner's answer each time it changes (undefined while there is none) and draws no \"Prüfen\" of its own; the explainer draws \"Prüfen\" under it, checks the answer with check(task, answer) and gives the feedback with reason(task, answer, props, lang), the app's message for a wrong answer, and may set its prop checked: boolean. An exercise component declares the prop seed: number, which the explainer gives it and no block does: its task number. The build numbers its distinct tasks like a generated exercise's (seeds 1, 2, 3, …, tasks compared as the JSON of generate(seed, props), Aufgabe Nr. 1 to N, N at most 67); the page shows \"Aufgabe Nr. <n>\" and \"Neue Aufgabe\", the address ?seed=<n>#<id> opens task number n, and the component gets that task's seed and shows generate(seed, props). Its logic module exports generate(seed, props), check(task, answer), solution(task), wrong(task), reason(task, answer, props, lang), describe(task, props, lang) and about: describe gives { task, solution }, the task as a learner sees it and its solution, each restricted Markdown in the language lang (props come in that language); about = { task, answer, correct, mistakes } says what the learner does, the answer's format, what counts as right and the typical mistakes, each a text (a map by language when the app has more than one). The build checks describe for every task number, and writes the no-JS text of an exercise component from the task of number 1 with its solution folded. Without JavaScript the page shows the nojs text instead of the component.",
1076
+ "required": ["component"],
1077
+ "properties": {
1078
+ "id": {
1079
+ "description": "The block's anchor on the page, a lowercase id: the address ?seed=<n>#<id> opens the task <n> of an exercise component. Optional: an exercise component without one has its name in kebab case (AssignLabels: assign-labels, assign-labels-2 for a second one on the page). Give one when the default would clash with a section or another exercise.",
1080
+ "$ref": "#/$defs/id"
1081
+ },
1082
+ "component": {
1083
+ "description": "The component's name, in PascalCase: src/components/<component>.vue.",
1084
+ "type": "string",
1085
+ "pattern": "^[A-Z][A-Za-z0-9]*$"
1086
+ },
1087
+ "props": {
1088
+ "description": "The component's props, checked against its prop types (defineProps<…>()). A prop of type string is a learner text: a string, or a map by language. A prop that is a list may be given as { data: <name> }: the records of src/data/<name>.yaml, cut to the fields the component declares for an item; every record needs them.",
1089
+ "type": "object"
1090
+ },
1091
+ "nojs": {
1092
+ "description": "What the component shows and does, for readers without JavaScript, with its content: the labelled diagram described. Required for a component that is no exercise. Restricted Markdown, rendered in place; once the component runs, it replaces this text, or, when the component declares defineOptions({ nojs: \"caption\" }), keeps it folded under it as a text description that one click opens. An exercise component needs none: the build writes its no-JS text from describe, the task of number 1 with its solution folded, so the page without JavaScript shows the same task as the component with seed 1; a nojs text it has goes above that task, e.g. what the figure shows.",
1093
+ "$ref": "#/$defs/markdown"
1094
+ },
1095
+ "examples": {
1096
+ "description": "Required for an exercise component (one that emits answer), else not allowed: test cases for the build, never shown to learners. Its check is the component's own code, so before you write it, note which answers fulfil the task's text and which typical wrong answers there are: at least one answer the check must accept, other than the one solution(task) gives where the text allows another (e.g. a circuit with one lamp and a switch, when the text asks for one lamp), and at least one it must reject. Each answer is written as check(task, answer) of <component>.ts takes it. The build runs each through check, for the task of its seed, or of every seed the property test draws when it gives none, and fails when an accepted one fails or a rejected one passes.",
1097
+ "type": "object",
1098
+ "additionalProperties": false,
1099
+ "properties": {
1100
+ "accepted": {
1101
+ "description": "Answers the text allows, each of which check must accept.",
1102
+ "type": "array",
1103
+ "items": {
1104
+ "$ref": "#/$defs/componentExample"
1105
+ }
1106
+ },
1107
+ "rejected": {
1108
+ "description": "Typical wrong answers, each of which check must reject.",
1109
+ "type": "array",
1110
+ "items": {
1111
+ "$ref": "#/$defs/componentExample"
1112
+ }
1113
+ }
1114
+ }
1115
+ }
1116
+ },
1117
+ "type": "object"
1118
+ },
1119
+ "componentExample": {
1120
+ "description": "One answer of an exercise component's examples.",
1121
+ "type": "object",
1122
+ "additionalProperties": false,
1123
+ "required": ["answer"],
1124
+ "properties": {
1125
+ "answer": {
1126
+ "description": "The answer as check(task, answer) of the component's logic module takes it, e.g. a list or a record."
1127
+ },
1128
+ "seed": {
1129
+ "description": "Optional: the seed of the task to check it against, when the right answer depends on the task; without it, the answer is checked against the task of every seed.",
1130
+ "type": "integer",
1131
+ "minimum": 1
1132
+ }
1133
+ }
1134
+ },
1135
+ "selfAssessment": {
1136
+ "description": "Optional: a self-assessment, which a learner may do before learning, with JavaScript only. It puts exercises of the content pages, without the page around them, on pages of its own, a few on each, and checks them only all together at the end: no single exercise is checked. The result says for each content page whether the learner probably knows it already, should look at it again or should read it; a page with no exercise in it is not asked. The last result stays on the device, one per self-assessment, and a new start replaces it. The learner may take the result to a KI tutor: the self-assessment's tutor page (<assessment>/tutor/), with the result in its address, makes the tutor a guide through the chapters, their pages and new rounds. A chapter declares it in its chapter.yaml, with exercises of its own pages; a chapter without it is not asked. The app's (einschaetzung/) is every chapter's together. Opened by a KI tutor (via=tutor), a run keeps nothing on the device and ends with \"Zurück zum Tutor\", which copies the result for the chat.",
1137
+ "type": "object",
1138
+ "additionalProperties": false,
1139
+ "required": ["pages"],
1140
+ "properties": {
1141
+ "pages": {
1142
+ "description": "Its pages in order, each with the exercises it shows, best 2 to 5.",
1143
+ "type": "array",
1144
+ "minItems": 1,
1145
+ "items": {
1146
+ "type": "object",
1147
+ "additionalProperties": false,
1148
+ "required": ["exercises"],
1149
+ "properties": {
1150
+ "exercises": {
1151
+ "description": "The exercises of the page in order, each by its page and id: /<chapter>/<page>/#<exercise>. A generated exercise draws a new task for each run; an exercise of a question type shows as it is. Each exercise once in the self-assessment; an exercise component cannot be in it.",
1152
+ "type": "array",
1153
+ "minItems": 1,
1154
+ "items": {
1155
+ "type": "string",
1156
+ "pattern": "^/[a-z0-9]+(-[a-z0-9]+)*/[a-z0-9]+(-[a-z0-9]+)*/#[a-z0-9]+(-[a-z0-9]+)*$"
1157
+ }
1158
+ }
1159
+ }
1160
+ }
1161
+ }
1162
+ }
1163
+ }
1164
+ }
1165
+ }