@lernapps/app-templates 0.0.0-stage → 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.
- package/LICENSE +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-By2GGTJC.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-BmRrD7UY.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-BkEl-m-F.mjs +254 -0
- package/dist/content-cYBPYg_J.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-BiMI-pCL.mjs +89 -0
- package/dist/engine-BqF2Vffn.mjs +136 -0
- package/dist/explainer-e2e.mjs +1297 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2131 -0
- package/dist/explainer.mjs +1246 -0
- package/dist/html-mffkHGHh.mjs +89 -0
- package/dist/image-CspJVfyQ.mjs +21 -0
- package/dist/interactive-e2e.mjs +140 -0
- package/dist/interactive-plugin.mjs +242 -0
- package/dist/interactive.mjs +75 -0
- package/dist/load-C-0hUaSC.mjs +16947 -0
- package/dist/needs-BzEahJ6X.mjs +97 -0
- package/dist/pages-9RaaomoO.mjs +81 -0
- package/dist/questions-e2e.mjs +1 -0
- package/dist/questions-engine.mjs +3 -0
- package/dist/questions.mjs +6 -0
- package/dist/quiz-BKc8JYtC.mjs +70 -0
- package/dist/quiz-e2e.mjs +174 -0
- package/dist/quiz-plugin.mjs +266 -0
- package/dist/quiz.mjs +246 -0
- package/dist/render-BJey0bwl.mjs +540 -0
- package/dist/render-BL6fV_NI.mjs +507 -0
- package/dist/render-DpRO_RKm.mjs +146 -0
- package/dist/result-_eQcamoz.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-COaZso7u.mjs +1072 -0
- package/dist/validate-CxtdeQ5S.mjs +146 -0
- package/dist/vue-DO1m6q1R.mjs +40 -0
- package/dist/vue-code-BkJjW_I_.mjs +320 -0
- package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
- package/docs/arc42/02-constraints.arc42.md +228 -0
- package/docs/arc42/03-system-context.arc42.md +118 -0
- package/docs/arc42/04-solution-strategy.arc42.md +47 -0
- package/docs/arc42/05-building-blocks.arc42.md +307 -0
- package/docs/arc42/06-runtime-view.arc42.md +98 -0
- package/docs/arc42/07-deployment-view.arc42.md +160 -0
- package/docs/arc42/08-concepts.arc42.md +123 -0
- package/docs/arc42/09-decisions.arc42.md +246 -0
- package/docs/arc42/10-quality-requirements.arc42.md +155 -0
- package/docs/arc42/11-risks.arc42.md +113 -0
- package/docs/arc42/12-glossary.arc42.md +162 -0
- package/docs/arc42/architecture-evidence.md +395 -0
- package/explainer/.arc42ignore +3 -0
- package/explainer/README.md +54 -0
- package/explainer/_gitignore +5 -0
- package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
- package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
- package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
- package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
- package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
- package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
- package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
- package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
- package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
- package/explainer/e2e/explainer.e2e.ts +3 -0
- package/explainer/index.html +11 -0
- package/explainer/package.json +18 -0
- package/explainer/playwright.config.ts +1 -0
- package/explainer/src/app.yaml +20 -0
- package/explainer/src/components/ProzentFeld.test.ts +30 -0
- package/explainer/src/components/ProzentFeld.ts +121 -0
- package/explainer/src/components/ProzentFeld.vue +84 -0
- package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
- package/explainer/src/main.ts +4 -0
- package/explainer/src/prozent/chapter.yaml +18 -0
- package/explainer/src/prozent/grundwert.yaml +115 -0
- package/explainer/src/prozent/prozentsatz.yaml +99 -0
- package/explainer/src/prozent/prozentwert.yaml +119 -0
- package/explainer/tsconfig.json +1 -0
- package/explainer/vite.config.ts +4 -0
- package/interactive/.arc42ignore +5 -0
- package/interactive/README.md +28 -0
- package/interactive/_gitignore +4 -0
- package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
- package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
- package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
- package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
- package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
- package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
- package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
- package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
- package/interactive/e2e/interactive.e2e.ts +3 -0
- package/interactive/index.html +11 -0
- package/interactive/package.json +18 -0
- package/interactive/playwright.config.ts +1 -0
- package/interactive/src/app.yaml +28 -0
- package/interactive/src/core/index.ts +101 -0
- package/interactive/src/data/lessons.json +4 -0
- package/interactive/src/env.d.ts +6 -0
- package/interactive/src/main.ts +6 -0
- package/interactive/src/messages/de.json +12 -0
- package/interactive/src/playthroughs/grundreihe.yaml +33 -0
- package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
- package/interactive/src/ui/App.vue +85 -0
- package/interactive/tsconfig.json +1 -0
- package/interactive/vite.config.ts +4 -0
- package/package.json +144 -4
- package/quiz/README.md +31 -0
- package/quiz/_gitignore +5 -0
- package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
- package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
- package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
- package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
- package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
- package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
- package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
- package/quiz/e2e/quiz.e2e.ts +3 -0
- package/quiz/index.html +11 -0
- package/quiz/package.json +18 -0
- package/quiz/playwright.config.ts +1 -0
- package/quiz/src/images/berge-ordnen.jpg +0 -0
- package/quiz/src/images/everest-hoehe.jpg +0 -0
- package/quiz/src/images/galapagos.jpg +0 -0
- package/quiz/src/images/grand-canyon.jpg +0 -0
- package/quiz/src/images/totes-meer.jpg +0 -0
- package/quiz/src/images/uluru.jpg +0 -0
- package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
- package/quiz/src/main.ts +4 -0
- package/quiz/src/quiz.json +312 -0
- package/quiz/tsconfig.json +1 -0
- package/quiz/vite.config.ts +4 -0
- package/runtime/explainer/assessment-client.ts +478 -0
- package/runtime/explainer/assessment-page.ts +446 -0
- package/runtime/explainer/assessment.ts +393 -0
- package/runtime/explainer/check-page.ts +520 -0
- package/runtime/explainer/components/exercise.ts +409 -0
- package/runtime/explainer/components/props.ts +230 -0
- package/runtime/explainer/components/read.ts +428 -0
- package/runtime/explainer/content.ts +277 -0
- package/runtime/explainer/e2e.ts +1474 -0
- package/runtime/explainer/exercises.ts +324 -0
- package/runtime/explainer/explainer.css +1283 -0
- package/runtime/explainer/explainer.ts +838 -0
- package/runtime/explainer/explainer.v1.schema.json +1165 -0
- package/runtime/explainer/expression.ts +407 -0
- package/runtime/explainer/fraction.ts +107 -0
- package/runtime/explainer/generated/comparator.ts +31 -0
- package/runtime/explainer/generated/question.ts +165 -0
- package/runtime/explainer/generated/soundness.ts +280 -0
- package/runtime/explainer/generated/texts.ts +257 -0
- package/runtime/explainer/generated/types.ts +103 -0
- package/runtime/explainer/generated/values.ts +136 -0
- package/runtime/explainer/islands.ts +281 -0
- package/runtime/explainer/load.ts +457 -0
- package/runtime/explainer/messages/de.json +200 -0
- package/runtime/explainer/messages/en.json +200 -0
- package/runtime/explainer/messages.ts +13 -0
- package/runtime/explainer/needs.ts +122 -0
- package/runtime/explainer/open-task.ts +235 -0
- package/runtime/explainer/outline.ts +299 -0
- package/runtime/explainer/plugin.ts +488 -0
- package/runtime/explainer/position.ts +62 -0
- package/runtime/explainer/prompt.ts +75 -0
- package/runtime/explainer/render.ts +686 -0
- package/runtime/explainer/result.ts +238 -0
- package/runtime/explainer/routes.ts +119 -0
- package/runtime/explainer/schema.ts +140 -0
- package/runtime/explainer/scope.ts +87 -0
- package/runtime/explainer/stuck.ts +57 -0
- package/runtime/explainer/tasks.ts +109 -0
- package/runtime/explainer/tutor.ts +469 -0
- package/runtime/explainer/tutorpage.ts +669 -0
- package/runtime/explainer/video.ts +123 -0
- package/runtime/interactive/contract.ts +121 -0
- package/runtime/interactive/e2e.ts +190 -0
- package/runtime/interactive/interactive.css +82 -0
- package/runtime/interactive/interactive.ts +115 -0
- package/runtime/interactive/interactive.v1.schema.json +146 -0
- package/runtime/interactive/load.ts +188 -0
- package/runtime/interactive/messages/de.json +14 -0
- package/runtime/interactive/messages/en.json +14 -0
- package/runtime/interactive/messages.ts +11 -0
- package/runtime/interactive/playthrough.v1.schema.json +143 -0
- package/runtime/interactive/plugin.ts +266 -0
- package/runtime/interactive/render.ts +126 -0
- package/runtime/quiz/e2e.ts +233 -0
- package/runtime/quiz/engine.ts +242 -0
- package/runtime/quiz/messages/de.json +23 -0
- package/runtime/quiz/messages/en.json +23 -0
- package/runtime/quiz/plugin.ts +150 -0
- package/runtime/quiz/quiz.css +154 -0
- package/runtime/quiz/quiz.ts +56 -0
- package/runtime/quiz/quiz.v1.schema.json +72 -0
- package/runtime/quiz/render.ts +173 -0
- package/runtime/quiz/validate.ts +108 -0
- package/runtime/shared/appname.ts +16 -0
- package/runtime/shared/blocks/block.ts +162 -0
- package/runtime/shared/blocks/blocks.css +190 -0
- package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
- package/runtime/shared/blocks/code.ts +26 -0
- package/runtime/shared/blocks/figures.ts +49 -0
- package/runtime/shared/blocks/image.ts +25 -0
- package/runtime/shared/blocks/index.ts +13 -0
- package/runtime/shared/blocks/messages/de.json +4 -0
- package/runtime/shared/blocks/messages/en.json +4 -0
- package/runtime/shared/blocks/render.ts +134 -0
- package/runtime/shared/blocks/validate.ts +165 -0
- package/runtime/shared/content.ts +197 -0
- package/runtime/shared/examples.ts +20 -0
- package/runtime/shared/html.ts +10 -0
- package/runtime/shared/languages.ts +94 -0
- package/runtime/shared/page.css +101 -0
- package/runtime/shared/pages.ts +114 -0
- package/runtime/shared/questions/e2e.ts +115 -0
- package/runtime/shared/questions/engine.ts +209 -0
- package/runtime/shared/questions/index.ts +39 -0
- package/runtime/shared/questions/messages/de.json +26 -0
- package/runtime/shared/questions/messages/en.json +26 -0
- package/runtime/shared/questions/question.ts +430 -0
- package/runtime/shared/questions/question.v1.schema.json +342 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +247 -0
- package/runtime/shared/questions/validate.ts +518 -0
- package/runtime/shared/schema.ts +44 -0
- package/runtime/shared/text/check.ts +68 -0
- package/runtime/shared/text/formula.ts +69 -0
- package/runtime/shared/text/markdown.ts +185 -0
- package/runtime/shared/text/text.v1.schema.json +34 -0
- package/runtime/shared/vue-code.ts +395 -0
- package/runtime/shared/vue.ts +38 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The question of a task (a question of the shared questions), the answer in the shape of the learner's, the feedback
|
|
2
|
+
// on a wrong answer, and what a learner sees of a task.
|
|
3
|
+
import { ExpressionError, type Rec, same, type Value } from "../expression.ts";
|
|
4
|
+
import { Fraction } from "../fraction.ts";
|
|
5
|
+
import { type Answer, isCorrect, parseNumber, type Question } from "../../shared/questions/question.ts";
|
|
6
|
+
import { inLanguage } from "../../shared/languages.ts";
|
|
7
|
+
import { type Generated, type Task, type Templates } from "./types.ts";
|
|
8
|
+
import { kindOf } from "./comparator.ts";
|
|
9
|
+
import { fill, formatNumber, label, plainText, taskText, templatesIn } from "./texts.ts";
|
|
10
|
+
|
|
11
|
+
const listOf = (value: Value, what: string): readonly Value[] => {
|
|
12
|
+
if (!Array.isArray(value)) throw new ExpressionError(`the answer of ${what} is a list`);
|
|
13
|
+
return value as readonly Value[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The task as the question of the comparator's kind, in `lang`. */
|
|
17
|
+
export function questionFor(spec: Generated, task: Task, lang: string, templates: Templates): Question {
|
|
18
|
+
const kind = kindOf(spec.answer.compare);
|
|
19
|
+
const base = {
|
|
20
|
+
id: `${spec.id}-${task.seed}`,
|
|
21
|
+
text: plainText(taskText(spec, task, lang, templates)),
|
|
22
|
+
explanation: "",
|
|
23
|
+
links: [],
|
|
24
|
+
};
|
|
25
|
+
switch (kind.kind) {
|
|
26
|
+
case "number": {
|
|
27
|
+
if (!(task.answer instanceof Fraction)) throw new ExpressionError("the answer of number is a number");
|
|
28
|
+
return {
|
|
29
|
+
...base,
|
|
30
|
+
type: "number",
|
|
31
|
+
answer: task.answer.toNumber(),
|
|
32
|
+
tolerance: kind.tolerance.toNumber(),
|
|
33
|
+
...(kind.unit === undefined ? {} : { unit: inLanguage(kind.unit, lang) }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
case "choice":
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
type: "single-choice",
|
|
40
|
+
options: (task.options ?? []).map((option) => ({
|
|
41
|
+
text: label(option, lang),
|
|
42
|
+
correct: same(task.answer, option),
|
|
43
|
+
info: "",
|
|
44
|
+
})),
|
|
45
|
+
};
|
|
46
|
+
case "order":
|
|
47
|
+
return { ...base, type: "ordering", items: listOf(task.answer, "order").map((item) => label(item, lang)) };
|
|
48
|
+
case "pairs":
|
|
49
|
+
return {
|
|
50
|
+
...base,
|
|
51
|
+
type: "matching",
|
|
52
|
+
pairs: pairValues(spec, task.answer).map(([left, right]) => ({
|
|
53
|
+
left: label(left, lang),
|
|
54
|
+
right: label(right, lang),
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The pairs of a pairs answer, as values: [left, right]. */
|
|
61
|
+
export function pairValues(spec: Generated, value: Value): [Value, Value][] {
|
|
62
|
+
const kind = kindOf(spec.answer.compare);
|
|
63
|
+
return listOf(value, "pairs").map((item) => {
|
|
64
|
+
if (kind.kind === "pairs" && kind.left !== undefined && kind.right !== undefined) {
|
|
65
|
+
const fields = item as Rec;
|
|
66
|
+
return [fields[kind.left] ?? "", fields[kind.right] ?? ""];
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(item) || item.length !== 2)
|
|
69
|
+
throw new ExpressionError("each pair is a list of two: [left, right]");
|
|
70
|
+
return [item[0] as Value, item[1] as Value];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The learner's answer that gives `value` (e.g. a mistake) through the controls of the task's question. */
|
|
75
|
+
function answerGiving(spec: Generated, task: Task, value: Value): Answer {
|
|
76
|
+
const kind = kindOf(spec.answer.compare);
|
|
77
|
+
const index = (list: readonly Value[], item: Value) => list.findIndex((other) => same(other, item));
|
|
78
|
+
switch (kind.kind) {
|
|
79
|
+
case "number": {
|
|
80
|
+
if (!(value instanceof Fraction)) throw new ExpressionError("a mistake of number is a number");
|
|
81
|
+
const places = Math.min(value.decimals(), Math.max(kind.decimals, 2));
|
|
82
|
+
return formatNumber(value.round(places), "de");
|
|
83
|
+
}
|
|
84
|
+
case "choice": {
|
|
85
|
+
const at = index(task.options ?? [], value);
|
|
86
|
+
if (at < 0) throw new ExpressionError(`${label(value, "de")} is not one of the choices`);
|
|
87
|
+
return at;
|
|
88
|
+
}
|
|
89
|
+
case "order": {
|
|
90
|
+
const order = listOf(value, "order");
|
|
91
|
+
return listOf(task.answer, "order").map((item) => index(order, item));
|
|
92
|
+
}
|
|
93
|
+
case "pairs": {
|
|
94
|
+
const right = pairValues(spec, task.answer);
|
|
95
|
+
const given = pairValues(spec, value);
|
|
96
|
+
return right.map(([left]) => {
|
|
97
|
+
const chosen = given.find(([other]) => same(other, left))?.[1];
|
|
98
|
+
return chosen === undefined ? -1 : right.findIndex(([, other]) => same(other, chosen));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The learner's answer that gives `value`, an example, through the controls of the task's question: as answerGiving,
|
|
106
|
+
* but a number as it is, with up to six decimals, so an example just outside the tolerance stays outside.
|
|
107
|
+
*/
|
|
108
|
+
export function exampleGiving(spec: Generated, task: Task, value: Value): Answer {
|
|
109
|
+
if (kindOf(spec.answer.compare).kind !== "number") return answerGiving(spec, task, value);
|
|
110
|
+
if (!(value instanceof Fraction)) throw new ExpressionError("an example of number is a number");
|
|
111
|
+
return formatNumber(value, "de");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The learner's answer that gives the mistake `index` of a task, through the controls of its question. */
|
|
115
|
+
export function answerFor(spec: Generated, task: Task, _question: Question, index: number): Answer {
|
|
116
|
+
return answerGiving(spec, task, task.mistakes[index] as Value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Whether two answers to `question` are the same, a number within `slack`. */
|
|
120
|
+
function sameAnswer(question: Question, a: Answer, b: Answer, slack: number): boolean {
|
|
121
|
+
if (question.type === "number") {
|
|
122
|
+
const left = typeof a === "string" ? parseNumber(a, question.unit) : undefined;
|
|
123
|
+
const right = typeof b === "string" ? parseNumber(b, question.unit) : undefined;
|
|
124
|
+
return left !== undefined && right !== undefined && Math.abs(left - right) <= slack + 1e-9;
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.join() === b.join();
|
|
127
|
+
return a === b;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The index of the first typical mistake the learner's wrong answer makes; undefined when it makes none. */
|
|
131
|
+
export function mistakeOf(spec: Generated, task: Task, question: Question, answer: Answer): number | undefined {
|
|
132
|
+
if (isCorrect(question, answer)) return undefined;
|
|
133
|
+
const kind = kindOf(spec.answer.compare);
|
|
134
|
+
// a mistake typed rounded to the answer's decimals still counts as that mistake
|
|
135
|
+
const slack =
|
|
136
|
+
kind.kind === "number" ? Math.max(kind.tolerance.toNumber(), 0.5 * 10 ** -Math.max(kind.decimals, 2)) : 0;
|
|
137
|
+
for (const [index] of task.mistakes.entries()) {
|
|
138
|
+
try {
|
|
139
|
+
if (sameAnswer(question, answer, answerFor(spec, task, question, index), slack)) return index;
|
|
140
|
+
} catch {
|
|
141
|
+
// a mistake the controls cannot give
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The message of the first mistake the learner's wrong answer makes, in `lang`; undefined when it makes none. */
|
|
148
|
+
export function diagnose(
|
|
149
|
+
spec: Generated,
|
|
150
|
+
task: Task,
|
|
151
|
+
question: Question,
|
|
152
|
+
answer: Answer,
|
|
153
|
+
lang: string,
|
|
154
|
+
templates: Templates,
|
|
155
|
+
): string | undefined {
|
|
156
|
+
const index = mistakeOf(spec, task, question, answer);
|
|
157
|
+
if (index === undefined) return undefined;
|
|
158
|
+
return fill(templatesIn(templates, lang).messages[index] ?? "", spec, task, lang, "text");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** What a learner sees of a task: its text and its question's options, items or pairs; "Neue Aufgabe" changes it. */
|
|
162
|
+
export function taskKey(spec: Generated, task: Task, lang: string, templates: Templates): string {
|
|
163
|
+
const { id: _id, ...question } = questionFor(spec, task, lang, templates);
|
|
164
|
+
return JSON.stringify(question);
|
|
165
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// The property test of a generated exercise at build time: the tasks of a fixed set of seeds, each checked for
|
|
2
|
+
// its formats, its answer, its mistakes and its options, and its examples run through the check.
|
|
3
|
+
import { evaluate, parse, same, type Value } from "../expression.ts";
|
|
4
|
+
import { Fraction } from "../fraction.ts";
|
|
5
|
+
import { type Answer, isCorrect, type Question, rightAnswer } from "../../shared/questions/question.ts";
|
|
6
|
+
import { verdict } from "../../shared/examples.ts";
|
|
7
|
+
import { type Data, type Generated, type Task, type Templates } from "./types.ts";
|
|
8
|
+
import { kindOf } from "./comparator.ts";
|
|
9
|
+
import { ATTEMPTS, compiled, generate, holds, StarvedError, values } from "./values.ts";
|
|
10
|
+
import { answerLabel, fill, hintOf, label, stepsOf, taskText, templatesIn } from "./texts.ts";
|
|
11
|
+
import { answerFor, exampleGiving, pairValues, questionFor } from "./question.ts";
|
|
12
|
+
|
|
13
|
+
/** A problem of an exercise: where in it, in which language, and what. */
|
|
14
|
+
export interface GeneratedProblem {
|
|
15
|
+
path: (string | number)[];
|
|
16
|
+
lang?: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const message = (error: unknown) => (error instanceof Error ? error.message : String(error));
|
|
21
|
+
|
|
22
|
+
/** Whether the answer is in the comparator's format; a problem otherwise. */
|
|
23
|
+
function formatProblem(spec: Generated, task: Task, lang: string): string | undefined {
|
|
24
|
+
const kind = kindOf(spec.answer.compare);
|
|
25
|
+
const shown = () => label(task.answer, lang);
|
|
26
|
+
switch (kind.kind) {
|
|
27
|
+
case "number":
|
|
28
|
+
if (!(task.answer instanceof Fraction)) return `the answer is not a number, but compare is number`;
|
|
29
|
+
if (task.answer.decimals() > kind.decimals) {
|
|
30
|
+
return `the answer ${shown()} has more than ${kind.decimals} decimals; round it in the answer, or narrow given or where`;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
case "choice":
|
|
34
|
+
if (!(task.options ?? []).some((option) => same(option, task.answer)))
|
|
35
|
+
return `the answer ${shown()} is not one of the choices`;
|
|
36
|
+
return undefined;
|
|
37
|
+
case "order": {
|
|
38
|
+
if (!Array.isArray(task.answer) || task.answer.length < 2) return "the answer of order is a list of at least two";
|
|
39
|
+
const texts = (task.answer as readonly Value[]).map((item) => label(item, lang));
|
|
40
|
+
return new Set(texts).size === texts.length ? undefined : "the answer of order has an item twice";
|
|
41
|
+
}
|
|
42
|
+
case "pairs": {
|
|
43
|
+
const pairs = pairValues(spec, task.answer).map(([left, right]) => [label(left, lang), label(right, lang)]);
|
|
44
|
+
if (pairs.length < 2) return "the answer of pairs is a list of at least two";
|
|
45
|
+
const lefts = new Set(pairs.map(([left]) => left));
|
|
46
|
+
const rights = new Set(pairs.map(([, right]) => right));
|
|
47
|
+
return lefts.size === pairs.length && rights.size === pairs.length
|
|
48
|
+
? undefined
|
|
49
|
+
: "the answer of pairs has an entry twice";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The problems of one seed's task. */
|
|
55
|
+
function seedProblems(
|
|
56
|
+
spec: Generated,
|
|
57
|
+
seed: number,
|
|
58
|
+
data: Data,
|
|
59
|
+
templates: Templates,
|
|
60
|
+
languages: readonly string[],
|
|
61
|
+
): GeneratedProblem[] {
|
|
62
|
+
const problems: GeneratedProblem[] = [];
|
|
63
|
+
const kind = kindOf(spec.answer.compare);
|
|
64
|
+
// the answer, the mistakes and the options are defined
|
|
65
|
+
let task: Task;
|
|
66
|
+
try {
|
|
67
|
+
task = generate(spec, seed, data);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof StarvedError)
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
path: ["where"],
|
|
73
|
+
message: `for seed ${seed} ${message(error).replace(/ for seed \d+$/, "")}; loosen it or narrow given`,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
const code = compiled(spec);
|
|
77
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
78
|
+
const env = values(spec, seed, attempt, data);
|
|
79
|
+
try {
|
|
80
|
+
if (!holds(spec, env)) continue;
|
|
81
|
+
} catch (inner) {
|
|
82
|
+
return [{ path: ["where"], message: `for seed ${seed} where is not defined: ${message(inner)}` }];
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
evaluate(code.answer, env);
|
|
86
|
+
} catch (inner) {
|
|
87
|
+
return [
|
|
88
|
+
{ path: ["answer", "value"], message: `for seed ${seed} the answer is not defined: ${message(inner)}` },
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
for (const [index, mistake] of code.mistakes.entries()) {
|
|
92
|
+
try {
|
|
93
|
+
evaluate(mistake, env);
|
|
94
|
+
} catch (inner) {
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
path: ["mistakes", index, "value"],
|
|
98
|
+
message: `for seed ${seed} the mistake is not defined: ${message(inner)}`,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [
|
|
104
|
+
{ path: ["answer", "compare"], message: `for seed ${seed} the options are not defined: ${message(error)}` },
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
return [{ path: ["where"], message: `for seed ${seed} ${message(error)}` }];
|
|
108
|
+
}
|
|
109
|
+
const lang = languages[0] ?? "de";
|
|
110
|
+
// the answer is in its format and passes its own comparator
|
|
111
|
+
let format: string | undefined;
|
|
112
|
+
try {
|
|
113
|
+
format = formatProblem(spec, task, lang);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
format = message(error);
|
|
116
|
+
}
|
|
117
|
+
if (format !== undefined) return [{ path: ["answer", "value"], message: `for seed ${seed} ${format}` }];
|
|
118
|
+
let question: Question;
|
|
119
|
+
try {
|
|
120
|
+
question = questionFor(spec, task, lang, templates);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return [{ path: ["answer", "value"], message: `for seed ${seed} ${message(error)}` }];
|
|
123
|
+
}
|
|
124
|
+
if (!isCorrect(question, rightAnswer(question))) {
|
|
125
|
+
return [{ path: ["answer", "compare"], message: `for seed ${seed} the answer does not pass its own comparator` }];
|
|
126
|
+
}
|
|
127
|
+
// no mistake equals the answer, and the learner can give each
|
|
128
|
+
task.mistakes.forEach((mistake, index) => {
|
|
129
|
+
const path = ["mistakes", index, "value"];
|
|
130
|
+
let given: Answer;
|
|
131
|
+
try {
|
|
132
|
+
given = answerFor(spec, task, question, index);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
problems.push({ path, message: `for seed ${seed} ${message(error)}; the learner could not give this mistake` });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const equal =
|
|
138
|
+
kind.kind === "number" && mistake instanceof Fraction && task.answer instanceof Fraction
|
|
139
|
+
? mistake.sub(task.answer).abs().compare(kind.tolerance) <= 0
|
|
140
|
+
: isCorrect(question, given);
|
|
141
|
+
if (equal) {
|
|
142
|
+
problems.push({
|
|
143
|
+
path,
|
|
144
|
+
message: `for seed ${seed} the mistake equals the answer (${answerLabel(spec, task, lang)}); a mistake is a wrong answer, so change its value or remove it`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
if (problems.length > 0) return problems;
|
|
149
|
+
// the task is the same for the same seed, and no placeholder is left
|
|
150
|
+
const again = generate(spec, seed, data);
|
|
151
|
+
for (const each of languages) {
|
|
152
|
+
if (
|
|
153
|
+
taskText(spec, again, each, templates) !== taskText(spec, task, each, templates) ||
|
|
154
|
+
!same(again.answer, task.answer)
|
|
155
|
+
) {
|
|
156
|
+
return [{ path: ["given"], message: `for seed ${seed} the task is not the same each time` }];
|
|
157
|
+
}
|
|
158
|
+
const texts: [(string | number)[], string, string][] = [
|
|
159
|
+
[["task", "text"], "the task", taskText(spec, task, each, templates)],
|
|
160
|
+
...stepsOf(spec, task, each, templates).map((step, index): [(string | number)[], string, string] => [
|
|
161
|
+
["steps", index],
|
|
162
|
+
`step ${index + 1}`,
|
|
163
|
+
step,
|
|
164
|
+
]),
|
|
165
|
+
...(spec.hint === undefined
|
|
166
|
+
? []
|
|
167
|
+
: [[["hint"], "the hint", hintOf(spec, task, each, templates) ?? ""] as [(string | number)[], string, string]]),
|
|
168
|
+
...(spec.mistakes ?? []).map((_mistake, index): [(string | number)[], string, string] => [
|
|
169
|
+
["mistakes", index, "message"],
|
|
170
|
+
"the message",
|
|
171
|
+
fill(templatesIn(templates, each).messages[index] ?? "", spec, task, each, "text"),
|
|
172
|
+
]),
|
|
173
|
+
];
|
|
174
|
+
for (const [path, what, text] of texts) {
|
|
175
|
+
const left = /\{\{?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}\}?/.exec(text);
|
|
176
|
+
if (left === null) continue;
|
|
177
|
+
const source = path.reduce<unknown>((at, part) => (at as Record<string, unknown> | undefined)?.[part], spec);
|
|
178
|
+
const keyed = typeof source === "object" && source !== null;
|
|
179
|
+
const root = left[1]?.split(".")[0] ?? "";
|
|
180
|
+
problems.push({
|
|
181
|
+
path: keyed ? [...path, each] : path,
|
|
182
|
+
...(keyed ? { lang: each } : {}),
|
|
183
|
+
message:
|
|
184
|
+
root in spec.given || root === "answer"
|
|
185
|
+
? `for seed ${seed} ${what} still has the placeholder ${left[0]}: it has no value there; name a field the records have`
|
|
186
|
+
: `for seed ${seed} ${what} still has the placeholder ${left[0]}; declare ${root} in given, or remove the braces`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (problems.length > 0) return problems;
|
|
191
|
+
return problems;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The property test of an exercise over the seeds 1 to `seeds`: `where` holds in at least one draw of ten; for each
|
|
196
|
+
* seed the answer is defined, in its format and passes its comparator, no mistake equals it, the task is the same each
|
|
197
|
+
* time and has no placeholder left The problems of the first seed
|
|
198
|
+
* that has any; none when the exercise is sound.
|
|
199
|
+
*/
|
|
200
|
+
export function soundness(
|
|
201
|
+
spec: Generated,
|
|
202
|
+
data: Data,
|
|
203
|
+
templates: Templates,
|
|
204
|
+
languages: readonly string[],
|
|
205
|
+
seeds = 200,
|
|
206
|
+
): GeneratedProblem[] {
|
|
207
|
+
if ((spec.where ?? []).length > 0) {
|
|
208
|
+
let passes = 0;
|
|
209
|
+
for (let seed = 1; seed <= seeds; seed++) {
|
|
210
|
+
try {
|
|
211
|
+
if (holds(spec, values(spec, seed, 0, data))) passes++;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return [{ path: ["where"], message: `for seed ${seed} where is not defined: ${message(error)}` }];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (passes * 10 < seeds) {
|
|
217
|
+
return [
|
|
218
|
+
{
|
|
219
|
+
path: ["where"],
|
|
220
|
+
message: `where holds in ${passes} of ${seeds} draws; it must hold in at least 1 of 10, so loosen it or narrow given`,
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (let seed = 1; seed <= seeds; seed++) {
|
|
226
|
+
const problems = seedProblems(spec, seed, data, templates, languages);
|
|
227
|
+
if (problems.length > 0) return problems;
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Whether the check of every seed's task, 1 to `seeds`, judges each example as it is listed: an accepted one passes,
|
|
234
|
+
* a rejected one fails. An example is an expression of given and of answer, the task's right answer (unless given
|
|
235
|
+
* declares its own answer), evaluated per seed. One problem per example, at the first seed that judges it otherwise.
|
|
236
|
+
*/
|
|
237
|
+
export function exampleProblems(
|
|
238
|
+
spec: Generated,
|
|
239
|
+
data: Data,
|
|
240
|
+
templates: Templates,
|
|
241
|
+
languages: readonly string[],
|
|
242
|
+
seeds = 200,
|
|
243
|
+
): GeneratedProblem[] {
|
|
244
|
+
const examples = (["accepted", "rejected"] as const).flatMap((kind) =>
|
|
245
|
+
(spec.examples?.[kind] ?? []).map((source, index) => ({ kind, index, expression: parse(source) })),
|
|
246
|
+
);
|
|
247
|
+
if (examples.length === 0) return [];
|
|
248
|
+
const lang = languages[0] ?? "de";
|
|
249
|
+
const problems = new Map<string, GeneratedProblem>();
|
|
250
|
+
for (let seed = 1; seed <= seeds && problems.size < examples.length; seed++) {
|
|
251
|
+
const task = generate(spec, seed, data);
|
|
252
|
+
const question = questionFor(spec, task, lang, templates);
|
|
253
|
+
const env = { answer: task.answer, ...task.values };
|
|
254
|
+
for (const { kind, index, expression } of examples) {
|
|
255
|
+
const key = `${kind}#${index}`;
|
|
256
|
+
if (problems.has(key)) continue;
|
|
257
|
+
const path = ["examples", kind, index];
|
|
258
|
+
let given: Answer;
|
|
259
|
+
let shown: string;
|
|
260
|
+
try {
|
|
261
|
+
const value = evaluate(expression, env);
|
|
262
|
+
shown = label(value, lang);
|
|
263
|
+
given = exampleGiving(spec, task, value);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
problems.set(key, {
|
|
266
|
+
path,
|
|
267
|
+
message: `for seed ${seed} the learner could not give this answer: ${message(error)}`,
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const right = isCorrect(question, given);
|
|
272
|
+
if (right === (kind === "accepted")) continue;
|
|
273
|
+
problems.set(key, {
|
|
274
|
+
path,
|
|
275
|
+
message: `for seed ${seed} (answer ${answerLabel(spec, task, lang)}) ${verdict(`"${shown}"`, right, "the check", "change the exercise (e.g. another comparator, a tolerance) so that its check")}`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return [...problems.values()];
|
|
280
|
+
}
|