@plasius/learning 0.6.0 → 0.8.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 (48) hide show
  1. package/README.md +41 -0
  2. package/dist/chunk-UNPCTBNK.js +43 -0
  3. package/dist/chunk-UNPCTBNK.js.map +1 -0
  4. package/dist/course-suggestions-CBs5swU6.d.ts +17 -0
  5. package/dist/course-suggestions-mK2MPB7e.d.cts +17 -0
  6. package/dist/courses/adventure-mission-planner.cjs +436 -0
  7. package/dist/courses/adventure-mission-planner.cjs.map +1 -0
  8. package/dist/courses/adventure-mission-planner.d.cts +6 -0
  9. package/dist/courses/adventure-mission-planner.d.ts +6 -0
  10. package/dist/courses/adventure-mission-planner.js +210 -0
  11. package/dist/courses/adventure-mission-planner.js.map +1 -0
  12. package/dist/courses/creature-care-dashboard.cjs +425 -0
  13. package/dist/courses/creature-care-dashboard.cjs.map +1 -0
  14. package/dist/courses/creature-care-dashboard.d.cts +6 -0
  15. package/dist/courses/creature-care-dashboard.d.ts +6 -0
  16. package/dist/courses/creature-care-dashboard.js +199 -0
  17. package/dist/courses/creature-care-dashboard.js.map +1 -0
  18. package/dist/courses/robot-mission-control.cjs +428 -0
  19. package/dist/courses/robot-mission-control.cjs.map +1 -0
  20. package/dist/courses/robot-mission-control.d.cts +6 -0
  21. package/dist/courses/robot-mission-control.d.ts +6 -0
  22. package/dist/courses/robot-mission-control.js +202 -0
  23. package/dist/courses/robot-mission-control.js.map +1 -0
  24. package/dist/courses/vibe-bug-detective.cjs +416 -0
  25. package/dist/courses/vibe-bug-detective.cjs.map +1 -0
  26. package/dist/courses/vibe-bug-detective.d.cts +8 -0
  27. package/dist/courses/vibe-bug-detective.d.ts +8 -0
  28. package/dist/courses/vibe-bug-detective.js +221 -0
  29. package/dist/courses/vibe-bug-detective.js.map +1 -0
  30. package/dist/courses/vibe-game-remix-lab.cjs +424 -0
  31. package/dist/courses/vibe-game-remix-lab.cjs.map +1 -0
  32. package/dist/courses/vibe-game-remix-lab.d.cts +8 -0
  33. package/dist/courses/vibe-game-remix-lab.d.ts +8 -0
  34. package/dist/courses/vibe-game-remix-lab.js +229 -0
  35. package/dist/courses/vibe-game-remix-lab.js.map +1 -0
  36. package/dist/courses/vibe-idea-studio.cjs +386 -0
  37. package/dist/courses/vibe-idea-studio.cjs.map +1 -0
  38. package/dist/courses/vibe-idea-studio.d.cts +8 -0
  39. package/dist/courses/vibe-idea-studio.d.ts +8 -0
  40. package/dist/courses/vibe-idea-studio.js +191 -0
  41. package/dist/courses/vibe-idea-studio.js.map +1 -0
  42. package/dist/index.cjs +41 -1
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +1 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +38 -0
  47. package/dist/index.js.map +1 -1
  48. package/package.json +32 -2
@@ -0,0 +1,428 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/courses/robot-mission-control.ts
21
+ var robot_mission_control_exports = {};
22
+ __export(robot_mission_control_exports, {
23
+ course: () => course,
24
+ practice: () => practice
25
+ });
26
+ module.exports = __toCommonJS(robot_mission_control_exports);
27
+
28
+ // src/mission-authoring.ts
29
+ var JUNIOR_CODER_MISSION_STAGE_ORDER_V1 = [
30
+ "learn",
31
+ "predict",
32
+ "build",
33
+ "run",
34
+ "assess",
35
+ "inspect",
36
+ "fix",
37
+ "explain",
38
+ "reward"
39
+ ];
40
+
41
+ // src/course-contracts.ts
42
+ var LEARNING_COURSE_STAGE_ORDER = JUNIOR_CODER_MISSION_STAGE_ORDER_V1;
43
+ var LEARNING_COURSE_LIMITS = Object.freeze({
44
+ missions: 6,
45
+ stagesPerMission: 9,
46
+ projectFiles: 8,
47
+ sourceCharactersPerFile: 64e3,
48
+ sourceCharactersPerProject: 96e3
49
+ });
50
+ var ID = /^[a-z0-9][a-z0-9.-]{0,159}$/u;
51
+ var FILE_PATH = /^[a-z0-9][a-z0-9_-]{0,63}\.(?:js|py|cpp|html|css|json)$/u;
52
+ var VERSION = /^\d+\.\d+\.\d+$/u;
53
+ var LANGUAGES = ["javascript", "python", "cpp", "html", "css", "blocks", "json"];
54
+ var CATEGORIES = ["game", "robot", "vibe", "web-app"];
55
+ var PLACEHOLDER = /\b(?:TODO|TBD|coming soon|placeholder|lorem ipsum)\b/iu;
56
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
57
+ var text = (value, minimum = 1, maximum = 8e3) => typeof value === "string" && value.trim().length >= minimum && value.length <= maximum;
58
+ var id = (value) => typeof value === "string" && ID.test(value);
59
+ var integer = (value, minimum, maximum) => typeof value === "number" && Number.isSafeInteger(value) && value >= minimum && value <= maximum;
60
+ var exactKeys = (value, keys) => Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
61
+ var LearningCourseInputError = class extends Error {
62
+ constructor() {
63
+ super("Invalid learning course input.");
64
+ this.name = "LearningCourseInputError";
65
+ }
66
+ };
67
+ function fileDefinitions(value) {
68
+ return Array.isArray(value) && value.length >= 1 && value.length <= LEARNING_COURSE_LIMITS.projectFiles && value.every((file) => record(file) && exactKeys(file, ["path", "language", "maximumCharacters"]) && typeof file.path === "string" && FILE_PATH.test(file.path) && typeof file.language === "string" && LANGUAGES.includes(file.language) && integer(file.maximumCharacters, 1, LEARNING_COURSE_LIMITS.sourceCharactersPerFile)) && new Set(value.map((file) => file.path)).size === value.length;
69
+ }
70
+ function parseLearningProject(course2, value) {
71
+ if (!fileDefinitions(course2.projectFiles) || !record(value) || !exactKeys(value, ["files"]) || !Array.isArray(value.files) || value.files.length !== course2.projectFiles.length) throw new LearningCourseInputError();
72
+ const files = [];
73
+ const seen = /* @__PURE__ */ new Set();
74
+ let characters = 0;
75
+ for (const file of value.files) {
76
+ if (!record(file) || !exactKeys(file, ["path", "source"]) || typeof file.path !== "string" || typeof file.source !== "string" || seen.has(file.path)) throw new LearningCourseInputError();
77
+ const definition = course2.projectFiles.find((candidate) => candidate.path === file.path);
78
+ if (!definition || file.source.length > definition.maximumCharacters || file.source.includes("\0")) throw new LearningCourseInputError();
79
+ characters += file.source.length;
80
+ if (characters > LEARNING_COURSE_LIMITS.sourceCharactersPerProject) throw new LearningCourseInputError();
81
+ seen.add(file.path);
82
+ files.push({ path: file.path, source: file.source });
83
+ }
84
+ return { files: course2.projectFiles.map((definition) => files.find((file) => file.path === definition.path)) };
85
+ }
86
+ function validateLearningCourse(value) {
87
+ const issues = [];
88
+ const add = (code, path) => {
89
+ issues.push({ code, path });
90
+ };
91
+ if (!record(value)) return [{ code: "invalid-manifest", path: "$" }];
92
+ if (!exactKeys(value, [
93
+ "schemaVersion",
94
+ "moduleId",
95
+ "moduleVersion",
96
+ "slug",
97
+ "title",
98
+ "summary",
99
+ "runtimeId",
100
+ "category",
101
+ "estimatedMinutes",
102
+ "completionAssessmentId",
103
+ "projectFiles",
104
+ "starterProject",
105
+ "reference",
106
+ "missions",
107
+ "completionBadge"
108
+ ]) || value.schemaVersion !== "1" || !id(value.moduleId) || !id(value.slug) || !id(value.runtimeId) || !text(value.moduleVersion) || !VERSION.test(value.moduleVersion) || typeof value.category !== "string" || !CATEGORIES.includes(value.category) || !text(value.title, 3, 160) || !text(value.summary, 40, 2e3) || !integer(value.estimatedMinutes, 60, 3600) || !id(value.completionAssessmentId) || !record(value.completionBadge) || !exactKeys(value.completionBadge, ["id", "title"]) || !id(value.completionBadge.id) || !text(value.completionBadge.title, 3, 160)) add("invalid-manifest", "$");
109
+ if (!fileDefinitions(value.projectFiles)) add("invalid-project", "projectFiles");
110
+ else {
111
+ try {
112
+ parseLearningProject({ projectFiles: value.projectFiles }, value.starterProject);
113
+ } catch {
114
+ add("invalid-project", "starterProject");
115
+ }
116
+ }
117
+ if (!Array.isArray(value.reference) || value.reference.length < 1 || value.reference.length > 80 || value.reference.some((entry) => !record(entry) || !exactKeys(entry, ["name", "signature", "description", "example"]) || !text(entry.name) || !text(entry.signature) || !text(entry.description, 20) || !text(entry.example))) add("incomplete-content", "reference");
118
+ if (!Array.isArray(value.missions)) {
119
+ add("mission-count", "missions");
120
+ return issues;
121
+ }
122
+ if (value.missions.length !== LEARNING_COURSE_LIMITS.missions) add("mission-count", "missions");
123
+ if (value.missions.length > LEARNING_COURSE_LIMITS.missions) return issues;
124
+ const seen = /* @__PURE__ */ new Set();
125
+ const checkId = (candidate, path) => {
126
+ if (!id(candidate)) add("invalid-manifest", path);
127
+ else if (seen.has(candidate)) add("duplicate-id", path);
128
+ else seen.add(candidate);
129
+ };
130
+ let minutes = 0;
131
+ value.missions.forEach((mission, missionIndex) => {
132
+ const path = `missions[${missionIndex}]`;
133
+ if (!record(mission)) {
134
+ add("invalid-manifest", path);
135
+ return;
136
+ }
137
+ checkId(mission.id, `${path}.id`);
138
+ checkId(mission.assessmentId, `${path}.assessmentId`);
139
+ if (!exactKeys(mission, ["id", "title", "concepts", "estimatedMinutes", "goals", "assessmentId", "stages", "extension"]) || !text(mission.title, 3, 160) || !integer(mission.estimatedMinutes, 10, 600) || !Array.isArray(mission.concepts) || mission.concepts.length < 1 || mission.concepts.length > 12 || mission.concepts.some((concept) => !text(concept, 2, 120)) || !Array.isArray(mission.goals) || mission.goals.length < 1 || mission.goals.length > 12 || mission.goals.some((goal) => !text(goal, 20, 2e3)) || !text(mission.extension, 30, 2e3)) add("incomplete-content", path);
140
+ if (typeof mission.estimatedMinutes === "number") minutes += mission.estimatedMinutes;
141
+ if (!Array.isArray(mission.stages) || mission.stages.length !== LEARNING_COURSE_LIMITS.stagesPerMission) {
142
+ add("stage-order", `${path}.stages`);
143
+ return;
144
+ }
145
+ mission.stages.forEach((stage, stageIndex) => {
146
+ const stagePath = `${path}.stages[${stageIndex}]`;
147
+ if (!record(stage)) {
148
+ add("invalid-manifest", stagePath);
149
+ return;
150
+ }
151
+ checkId(stage.id, `${stagePath}.id`);
152
+ if (stage.kind !== LEARNING_COURSE_STAGE_ORDER[stageIndex]) add("stage-order", stagePath);
153
+ if (!exactKeys(stage, ["id", "kind", "title", "instruction", "help"]) || !text(stage.title, 3, 160) || !text(stage.instruction, 40) || !text(stage.help, 30) || typeof stage.instruction === "string" && PLACEHOLDER.test(stage.instruction)) add("incomplete-content", stagePath);
154
+ });
155
+ });
156
+ if (minutes !== value.estimatedMinutes) add("invalid-manifest", "estimatedMinutes");
157
+ return issues;
158
+ }
159
+ function parseLearningCourse(value) {
160
+ if (validateLearningCourse(value).length) throw new LearningCourseInputError();
161
+ return structuredClone(value);
162
+ }
163
+
164
+ // src/courses/course-authoring.ts
165
+ function authorCourse(header, missions) {
166
+ const course2 = parseLearningCourse({
167
+ ...header,
168
+ schemaVersion: "1",
169
+ moduleVersion: "2.0.0",
170
+ moduleId: `junior-coder.${header.slug}`,
171
+ runtimeId: `${header.slug}.v2`,
172
+ estimatedMinutes: missions.length * 60,
173
+ completionAssessmentId: `${header.slug}.final`,
174
+ completionBadge: { id: `${header.slug}.completed`, title: `${header.title} creator` },
175
+ missions: missions.map((mission, index) => ({
176
+ id: `${header.slug}.m${index + 1}`,
177
+ title: mission.title,
178
+ concepts: mission.concepts,
179
+ goals: mission.goals,
180
+ estimatedMinutes: 60,
181
+ assessmentId: `${header.slug}.m${index + 1}.assessment`,
182
+ extension: mission.extension,
183
+ stages: LEARNING_COURSE_STAGE_ORDER.map((kind) => ({
184
+ id: `${header.slug}.m${index + 1}.${kind}`,
185
+ kind,
186
+ title: `${kind.charAt(0).toUpperCase()}${kind.slice(1)}: ${mission.title}`,
187
+ instruction: mission.activities[kind][0],
188
+ help: mission.activities[kind][1]
189
+ }))
190
+ }))
191
+ });
192
+ const practice2 = structuredClone(missions.flatMap((mission, index) => ["learn", "predict", "explain"].map((kind) => ({ ...mission.questions[kind], stageId: `${header.slug}.m${index + 1}.${kind}` }))));
193
+ for (const question of practice2) {
194
+ if (question.question.trim().length < 20 || question.feedback.trim().length < 40 || question.choices.length !== 3 || question.choices.some((choice) => choice.trim().length < 2) || new Set(question.choices).size !== 3 || !Number.isInteger(question.correctChoice) || question.correctChoice < 0 || question.correctChoice > 2) throw new Error("Invalid course practice question.");
195
+ }
196
+ return { course: course2, practice: practice2 };
197
+ }
198
+
199
+ // src/courses/web-course-authoring.ts
200
+ var webProjectFiles = [
201
+ { path: "index.html", language: "html", maximumCharacters: 24e3 },
202
+ { path: "app.css", language: "css", maximumCharacters: 16e3 },
203
+ { path: "app.js", language: "javascript", maximumCharacters: 32e3 }
204
+ ];
205
+ var webStarterCss = `:root { color: #172a29; background-color: #f5f4ec; font-family: system-ui, sans-serif; line-height: 1.6; }
206
+ * { box-sizing: border-box; }
207
+ main { max-width: 60rem; margin-inline: auto; padding: 1rem; overflow-wrap: anywhere; }
208
+ h1, h2, p { margin-top: 0; }
209
+ section, article, fieldset { border: 1px solid #647874; border-radius: 0.5rem; padding: 1rem; margin-block: 1rem; min-width: 0; }
210
+ label { display: block; font-weight: 700; }
211
+ button, input, select, textarea { font-family: inherit; font-size: 1rem; line-height: 1.6; min-height: 2.75rem; max-width: 100%; border: 2px solid #465e59; border-radius: 0.25rem; padding: 0.5rem; color: inherit; background-color: #ffffff; }
212
+ button { cursor: pointer; }
213
+ button:disabled { cursor: default; border-style: dashed; }
214
+ :focus-visible { outline: 3px solid #075bab; outline-offset: 3px; }
215
+ [aria-invalid="true"] { border-color: #a11c25; }
216
+ .error { color: #a11c25; }
217
+ .actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
218
+ .cards { display: grid; gap: 1rem; padding: 0; list-style-type: none; }
219
+ progress, meter { width: 100%; }
220
+ @media (min-width: 48rem) { .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
221
+ @media (prefers-reduced-motion: reduce) { * { transition-duration: 0s; } }
222
+ @media (prefers-color-scheme: dark) {
223
+ :root { color: #edf4ef; background-color: #142623; }
224
+ button, input, select, textarea { color: #edf4ef; background-color: #203a34; border-color: #b8cec5; }
225
+ :focus-visible { outline-color: #a8d6ff; }
226
+ .error { color: #ffb8bf; }
227
+ [aria-invalid="true"] { border-color: #ffb8bf; }
228
+ }
229
+ `;
230
+ var webReferences = [
231
+ { name: "editable web files", signature: "index.html + app.css + app.js", description: "Write a semantic HTML fragment beginning with main, scoped responsive CSS and pure JavaScript initialState/update/view functions. The host compiles HTML/CSS and runs JavaScript separately. No document, DOM, imports, network, localStorage, timers, images or external resources are available. Fictional project data stays inside the preview; course source is saved through the bound account.", example: '<main><h1>My project</h1><p role="status" data-text="message"></p></main>' },
232
+ { name: "bindings", signature: "view(state) \u2192 plain JSON display data", description: "data-text displays scalar text; data-value controls a native field; data-checked, data-disabled, data-pressed, data-invalid and data-if require booleans. data-label supplies a nonempty accessible label. data-repeat repeats its element for records with unique string id, resolving nested bindings within each record. Do not put static HTML IDs in repeated content. Paths use named fields up to four segments, without expressions.", example: '<li data-repeat="missions"><span data-text="title"></span><button type="button" data-action="toggle" data-id="id" data-pressed="done" data-label="toggleLabel">Toggle</button></li>' },
233
+ { name: "native events", signature: '{ type: "field", name, value } or { type, id? }', description: "A named input, textarea or select emits field with its string value (checkboxes use booleans). A type=button with data-action emits that action and optional bound data-id. A form with data-action owns submission; its submit button has no separate action. Prevent network submission. Preserve native labels, focus and keyboard operation; input text is never executable markup.", example: '<form data-action="add" novalidate><label for="title">Mission</label><input id="title" name="title" data-value="draft.title"><button type="submit">Add mission</button></form>' }
234
+ ];
235
+
236
+ // src/courses/robot-mission-control.ts
237
+ var { course, practice } = authorCourse({
238
+ slug: "robot-mission-control",
239
+ title: "Robot Mission Control",
240
+ category: "web-app",
241
+ summary: "Build a real web control panel for a virtual rover. Separate connection, arming and commands, validate direction and speed, implement dependable STOP and reject stale or unsafe telemetry. Finish a responsive, keyboard-operable simulator with truthful status and deliberate recovery; no physical robot or network connection is required.",
242
+ projectFiles: webProjectFiles,
243
+ starterProject: { files: [
244
+ { path: "index.html", source: `<main>
245
+ <h1>Robot Mission Control</h1><p>This panel controls a virtual rover in the course simulator.</p>
246
+ <section aria-labelledby="state-heading"><h2 id="state-heading">Rover state</h2>
247
+ <p data-text="connectionText"></p><p data-text="armedText"></p><p data-text="telemetryText"></p><p data-text="motorText"></p>
248
+ <div class="actions"><button type="button" data-action="connect" data-disabled="connectDisabled">Connect simulator</button><button type="button" data-action="disconnect" data-disabled="disconnectDisabled">Disconnect</button></div>
249
+ </section>
250
+ <section aria-labelledby="command-heading"><h2 id="command-heading">Prepare a command</h2>
251
+ <label for="direction">Direction</label><select id="direction" name="direction" data-value="direction"><option value="forward">Forward</option><option value="backward">Backward</option><option value="left">Turn left</option><option value="right">Turn right</option></select>
252
+ <label for="speed">Power, from 1 to 40</label><input id="speed" name="speed" type="number" min="1" max="40" step="1" aria-describedby="speed-error" data-value="speed" data-invalid="speedInvalid">
253
+ <p id="speed-error" class="error" role="alert" data-text="speedError"></p>
254
+ <div class="actions"><button type="button" data-action="arm" data-disabled="armDisabled">Arm rover</button><button type="button" data-action="command" data-disabled="commandDisabled">Send command</button><button type="button" data-action="stop">STOP rover</button></div>
255
+ </section>
256
+ <section aria-labelledby="log-heading"><h2 id="log-heading">Command history</h2><ol><li data-repeat="history" data-text="text"></li></ol></section>
257
+ <p role="status" data-text="message"></p><button type="button" data-action="reset">Reset simulation</button>
258
+ </main>` },
259
+ { path: "app.css", source: webStarterCss },
260
+ { path: "app.js", source: `function initialState() {
261
+ return { connected: false, armed: false, left: 0, right: 0, direction: "forward", speed: "20", speedError: "",
262
+ nowMs: 0, lastTelemetryAt: null, lastSequence: 0, battery: null, obstacle: null,
263
+ history: [], nextEventId: 1, message: "Connect the simulator to begin. STOP is always available." };
264
+ }
265
+ function update(state, input) {
266
+ if (input.type === "reset") return initialState();
267
+ return JSON.parse(JSON.stringify(state));
268
+ }
269
+ function isSafe(state) { return false; }
270
+ function view(state) {
271
+ return { connectionText: state.connected ? "Connected to simulator" : "Disconnected", armedText: state.armed ? "Armed" : "Disarmed",
272
+ telemetryText: "No telemetry received", motorText: "Left " + state.left + ", right " + state.right,
273
+ direction: state.direction, speed: state.speed, speedError: state.speedError, speedInvalid: state.speedError !== "",
274
+ connectDisabled: false, disconnectDisabled: true, armDisabled: true, commandDisabled: true, history: [], message: state.message };
275
+ }
276
+ ` }
277
+ ] },
278
+ reference: [
279
+ ...webReferences,
280
+ { name: "initialState", signature: "initialState() \u2192 disconnected stopped rover", description: "Start connected=false, armed=false, left=right=0, direction='forward', speed='20', speedError='', nowMs=0, lastTelemetryAt=null, lastSequence=0, battery=obstacle=null, history=[], nextEventId=1 and useful message. left/right are virtual motor power, always -40 to 40. State is detached from previous inputs and reset makes a fresh simulator, independently of saved source and completion.", example: "{ connected: false, armed: false, left: 0, right: 0 }" },
281
+ { name: "connection", signature: "connect/disconnect/arm", description: "connect from disconnected sets connected and leaves motors zero/disarmed, clearing telemetry values/timestamp while retaining lastSequence. Repeated connect changes only feedback. disconnect always zeros/disarms and clears telemetry. arm is an explicit action requiring isSafe; it sets armed but never moves motors. Fresh safe telemetry after a fault does not automatically arm or resume movement. A new connection also needs a strictly newer telemetry sequence.", example: 'if (!isSafe(next)) return { ...next, message: "Fresh safe telemetry is required before arming." };' },
282
+ { name: "telemetry", signature: '{type:"telemetry",sequence,timeMs,battery,obstacle}', description: "Accept only while connected: sequence is an integer 1\u20131000000 strictly greater than lastSequence; timeMs is an integer 0\u2013nowMs aged at most 500ms; battery is finite 0\u2013100 and obstacle is boolean. Valid packets update these fields. Invalid, duplicate, old or future packets zero/disarm and clear telemetry while retaining lastSequence. Valid battery<20 or obstacle=true is hazardous: retain its data but zero/disarm. Ignored packets while disconnected never refresh state.", example: "const age = state.nowMs - input.timeMs; const fresh = age >= 0 && age <= 500;" },
283
+ { name: "clock and safety", signature: "tick{dtMs} and isSafe(state)", description: "The host supplies integer dtMs 0\u20131000. Valid ticks advance nowMs up to 3600000; if telemetry age becomes greater than 500ms, zero/disarm. At the session limit disconnect/clear telemetry and require reset before reconnecting. isSafe means connected, timestamp present and aged 0\u2013500, battery>=20, obstacle=false and session unfinished. Invalid ticks preserve state. The host independently stops its virtual motors if learner execution fails or exceeds its deadline.", example: "const age = state.nowMs - state.lastTelemetryAt; return state.connected && state.lastTelemetryAt !== null && age >= 0 && age <= 500 && state.battery >= 20 && state.obstacle === false && state.nowMs < 3600000;" },
284
+ { name: "command", signature: "field updates prepare; command applies", description: "field accepts direction in forward/backward/left/right or a speed string up to three characters, clearing speedError on speed change. Fields never change motors. command requires armed and isSafe plus speed as one or two decimal digits representing integer 1\u201340. Map power p to forward(p,p), backward(-p,-p), left(-p,p), right(p,-p). Invalid speed preserves motors/arming and sets linked field feedback; unsafe command zeros/disarms. Unknown fields/actions preserve values.", example: 'const left = next.direction === "backward" || next.direction === "left" ? -power : power;' },
285
+ { name: "STOP", signature: "stop \u2192 motors zero and disarmed", description: "STOP always sets left=right=0 and armed=false, regardless of connection, field validity, telemetry, history capacity or prior mode. It is a native type=button, never a form submit and never disabled. It leaves connection, prepared fields and telemetry intact. Recovery requires a deliberate arm after fresh safe evidence, then a separate command; clearing a fault alone never restarts movement.", example: 'return { ...next, left: 0, right: 0, armed: false, message: "Rover stopped. Arm again when safe before sending another command." };' },
286
+ { name: "history", signature: "[{id,action,at,left,right}] newest first", description: "Record successful connect, disconnect, arm and command; record stop/fault only when it changes connected/armed/motor state. Use increasing event-N IDs and at=nowMs, storing resulting motors, newest first, at most 20 entries. Fields, safe telemetry, ordinary ticks and refused actions do not log success. nextEventId is 1\u20131000000. If exhausted, refuse connect/arm/command; disconnect/STOP/faults still stop immediately and leave history unchanged. Reset renews the history budget.", example: '{ id: "event-3", action: "command", at: 100, left: 20, right: 20 }' },
287
+ { name: "view", signature: "view(state) \u2192 honest status and controls", description: "Return connectionText, armedText, telemetryText describing battery/obstacle/freshness or missing data, motorText with actual outputs, direction/speed/speedError/speedInvalid, connectDisabled/disconnectDisabled/armDisabled/commandDisabled and readable history [{id,text}], message. Permissions follow actual guards; STOP has no disabled binding. Prepared direction/speed are never presented as acknowledged motor output. Read state without effects and avoid announcing every telemetry packet.", example: 'const motorText = "Left " + state.left + ", right " + state.right;' }
288
+ ]
289
+ }, [
290
+ {
291
+ title: "An honest control panel",
292
+ concepts: ["Semantic controls", "Observed state", "Simulation boundaries"],
293
+ goals: ["Build a labelled responsive control panel that truthfully starts disconnected and stopped.", "Separate requested settings from observed virtual motor output."],
294
+ extension: "Sketch a second panel layout for a narrow screen, preserving control order and the distinction between prepared commands and actual rover state.",
295
+ activities: {
296
+ learn: ["This web application controls a virtual rover, not physical hardware. It begins disconnected, disarmed and stopped. The panel must distinguish prepared direction and power from actual motor outputs; a selected Forward option does not mean the rover is already moving.", "The host provides the simulator and later telemetry scenarios. No network or hardware permission is implied by course completion."],
297
+ predict: ["Predict the initial connection, armed status, motor values and available controls. Compare the selected power 20 with actual left/right zero and explain why both can be correct.", "A preparation field describes a possible future command, while observed status describes the current model."],
298
+ build: ["Organise the HTML into labelled rover-state, command and history sections. Style it for 320px and both themes, keeping STOP easy to find and every status understandable in text. Complete the truthful initial view projection.", "Use native buttons and visible labels; do not turn a coloured card into an unlabeled clickable control."],
299
+ run: ["Navigate the disconnected panel by keyboard, zoom the page and inspect heading and control names with a screen reader. Compare displayed motor values with initialState, then reset and repeat.", "At this stage commands remain unfinished; the panel should honestly describe that initial stopped state."],
300
+ assess: ["Check initial stopped values, meaningful semantics, prepared-versus-actual status, a visible enabled STOP button and nonmutating view. Verify the layout wraps controls without clipping text or focus outlines.", "The initial interface assessment does not claim that later connection or motor command logic is implemented yet."],
301
+ inspect: ["If the panel says Moving when only a direction was selected, inspect the source of that status. If STOP disappears on narrow screens, inspect layout order and fixed dimensions.", "Observed motor status must come from left/right output fields rather than from the editable direction label."],
302
+ fix: ["Repair misleading status or layout, then repeat keyboard, zoom and 320px checks. Keep STOP in the normal focus order and ensure text remains readable with reduced motion enabled.", "A safety action should not depend on an animation finishing or on recognising an icon alone."],
303
+ explain: ["Explain the difference between requesting a movement and observing motor output. Choose the initial status that accurately describes the rover before any connection or command.", "Clear status helps a person understand what has happened instead of guessing from the last control they touched."],
304
+ reward: ["Save Honest control room. The panel now describes a stopped simulator clearly and accessibly. Next you will implement connection as a deliberate state transition.", "Keep the disconnected view as the baseline for all later recovery cases."]
305
+ },
306
+ questions: {
307
+ learn: { question: "What does selecting a direction establish by itself?", choices: ["Only a prepared command choice", "Confirmed motor movement", "A physical robot connection"], correctChoice: 0, feedback: "Preparation is separate from execution, so changing a direction field alone must not claim or cause motor movement." },
308
+ predict: { question: "What are initial left and right outputs with prepared speed 20?", choices: ["20 and 20", "0 and 0", "-20 and 20"], correctChoice: 1, feedback: "The rover starts stopped even though the form contains a useful default value for a future deliberate command." },
309
+ explain: { question: "Which status is truthful before connecting?", choices: ["Moving forward", "Armed and ready", "Disconnected, disarmed and stopped"], correctChoice: 2, feedback: "The initial model has no connection or arming evidence and zero motor output, which the view must represent honestly." }
310
+ }
311
+ },
312
+ {
313
+ title: "Connect before commanding",
314
+ concepts: ["Connection state", "Fresh evidence", "Explicit permissions"],
315
+ goals: ["Implement connection and disconnection without implicit arming or motion.", "Accept basic valid telemetry and show whether safe evidence is available."],
316
+ extension: "Draw the difference between connected with no telemetry and connected with fresh safe telemetry. Describe why both states need distinct explanatory text.",
317
+ activities: {
318
+ learn: ["A connection makes telemetry possible but does not establish that movement is safe. connect starts stopped and clears prior telemetry. A valid packet supplies a newer sequence, timestamp, battery and obstacle state. disconnect zeros motors and disarms regardless of current activity.", "Retain the sequence counter across reconnects so an older packet cannot masquerade as new evidence."],
319
+ predict: ["Predict connect followed by no packet, a valid safe packet, disconnect and reconnect. Track connected, armed, motors and telemetry separately through the sequence.", "A successful connection is not a command and must not reuse a previous moving state."],
320
+ build: ["Implement connect/disconnect and the documented telemetry shape checks. Implement isSafe from connected state, fresh timestamp, battery threshold and obstacle absence. Project clear missing-versus-fresh telemetry text while keeping arm and command work for the next mission.", "Validate types and bounds before copying packet fields; a truthy string is not a boolean obstacle reading."],
321
+ run: ["Connect, receive the supplied safe scenario and disconnect using keyboard controls. Reconnect without a new packet and compare the message and permission states with the previous connection.", "The scenario controls belong to the simulator host; the authored page does not make a real network request."],
322
+ assess: ["Check stopped connection, repeated connect without reset effects, disconnect from any state, strict packet fields and no telemetry refresh while disconnected. Verify reconnect clears observations while retaining lastSequence.", "Later missions add detailed expiry and hazard replay, while these checks establish the basic evidence boundary."],
323
+ inspect: ["If reconnect appears ready immediately, inspect stale timestamp retention. If disconnect leaves nonzero motors, inspect whether stopping was incorrectly conditional on being armed.", "Disconnection must establish a safe stopped state even when earlier learner code left inconsistent control fields."],
324
+ fix: ["Repair connection or packet handling and replay connect/packet/disconnect/reconnect. Confirm every new connection waits for a strictly newer valid packet before isSafe can become true.", "A sequence number provides ordering evidence independently of the human-readable connection message."],
325
+ explain: ["Explain why connected does not mean safe to move. Identify the fields that establish safe evidence and describe what must be cleared when a connection ends.", "Connection, freshness and hazard status answer different questions and should not be collapsed into one optimistic label."],
326
+ reward: ["Save Deliberate connection. The panel now distinguishes an open simulator connection from usable evidence. Next you will arm and send bounded motor commands explicitly.", "Keep the reconnect-without-new-packet case for future recovery testing."]
327
+ },
328
+ questions: {
329
+ learn: { question: "What should connecting do to motor outputs?", choices: ["Resume their previous movement", "Leave both zero and disarmed", "Send the selected direction immediately"], correctChoice: 1, feedback: "Connection only establishes the possibility of receiving new evidence; it does not authorize or issue movement." },
330
+ predict: { question: "What is needed after reconnecting before safe evidence exists?", choices: ["The old label text", "A repeated old packet", "A strictly newer valid fresh telemetry packet"], correctChoice: 2, feedback: "Clearing observations and retaining sequence ordering prevents old connection data from being treated as current evidence." },
331
+ explain: { question: "Why keep connection and safe-state checks separate?", choices: ["A connected rover may still lack fresh safe observations", "To hide errors", "Because battery never matters"], correctChoice: 0, feedback: "Connectivity alone cannot establish freshness, battery adequacy or obstacle absence, so each condition needs explicit evidence." }
332
+ }
333
+ },
334
+ {
335
+ title: "Arm a bounded command",
336
+ concepts: ["Deliberate arming", "Field validation", "Differential outputs"],
337
+ goals: ["Separate field preparation, explicit arming and actual command application.", "Validate power and map directions to bounded virtual wheel outputs."],
338
+ extension: "Add explanatory text for each direction's left/right signs, keeping the same command contract and clear distinction between requested and actual output.",
339
+ activities: {
340
+ learn: ["Arming acknowledges fresh safe evidence but leaves motors stopped. A separate command uses validated power 1\u201340 and a direction: forward(p,p), backward(-p,-p), left(-p,p), right(p,-p). Field edits prepare a command without applying it.", "The rover uses differential wheel power in a simulator; negative values mean reverse wheel motion, not a negative speed field."],
341
+ predict: ["Predict outputs for left at power 20, then editing power to 30 without pressing Send command. Compare invalid power 0, 41 and 2.5 with valid boundary values 1 and 40.", "A visible field value is a string that still requires format and range validation before it becomes motor power."],
342
+ build: ["Handle bounded direction/speed fields, implement explicit arm using isSafe, then implement command with exact speed validation and direction mapping. Link speed errors through aria-describedby and aria-invalid, retaining invalid input for correction.", "A valid command must satisfy both control prerequisites and field rules before motor output changes."],
343
+ run: ["Connect and receive safe telemetry, arm, send each direction and edit fields between sends. Try invalid power and an unarmed command; compare actual outputs, messages and field feedback.", "Arming itself should be visibly acknowledged without moving either wheel."],
344
+ assess: ["Check every direction mapping, exact power bounds, malformed numeric strings, preparation without motion, arming without motion and commands rejected before arming. Verify output changes are detached and remain within -40 to 40.", "The host sends direct actions too, so correct disabled styling cannot substitute for transition guards."],
345
+ inspect: ["If changing a select moves the rover, inspect field handling. If left and right turn alike, compare signs in the mapping. If invalid speed partly changes outputs, inspect validation order.", "Keep a single validated power value and compute both wheel outputs before returning the next state."],
346
+ fix: ["Repair the command boundary and replay all four directions with boundary and invalid powers. Confirm corrected input clears field errors and keyboard focus remains in the field while editing.", "A field error should explain the allowed range without causing unrelated connection or telemetry changes."],
347
+ explain: ["Explain why arm and command are separate actions and why field edits remain inert. Choose the left/right output pair for a deliberate right turn.", "Separating preparation, authorization and action lets a person review intent before causing simulated movement."],
348
+ reward: ["Save Intentional movement. The rover now moves only after safe evidence, explicit arming and a validated command. Next you will make stopping dependable under every condition.", "Keep a moving-state fixture for testing STOP with valid and invalid form values."]
349
+ },
350
+ questions: {
351
+ learn: { question: "What should an arm action do to stopped motors?", choices: ["Start them immediately", "Apply the last field edit", "Keep them stopped until a separate command"], correctChoice: 2, feedback: "Arming grants readiness under safe conditions, while a deliberate later command determines actual wheel output." },
352
+ predict: { question: "What outputs represent a right turn at power 20?", choices: ["Left 20, right -20", "Left -20, right 20", "Left 20, right 20"], correctChoice: 0, feedback: "The documented right-turn mapping drives the left wheel forward and the right wheel backward at the same bounded magnitude." },
353
+ explain: { question: "Why should editing speed leave current motors unchanged?", choices: ["To ignore all user input", "Because preparation and command execution are distinct", "Because speed must always remain 20"], correctChoice: 1, feedback: "A person needs to prepare and review settings without every keystroke silently issuing a new movement command." }
354
+ }
355
+ },
356
+ {
357
+ title: "Make STOP dependable",
358
+ concepts: ["Priority actions", "Fail-safe transitions", "Deliberate recovery"],
359
+ goals: ["Make STOP independent of form validity, connection and history limits.", "Require deliberate re-arming and a separate command after every stop."],
360
+ extension: "Describe a STOP failure caused by using a submit button inside a validating form. Explain how native button type and state-transition priority prevent it.",
361
+ activities: {
362
+ learn: ["STOP is always a native type=button with no disabled condition. Its transition zeros both outputs and disarms before considering fields, connection or history. It preserves prepared settings and observations, but movement needs a new explicit arm and command after stopping.", "Stopping is not the same as changing speed to zero in a field; it is an immediate independent action."],
363
+ predict: ["Predict STOP while moving with invalid speed text, STOP while disconnected and repeated STOP. Then predict receiving a safe packet after stopping without pressing Arm.", "Clearing an error or receiving better evidence must not silently restart the rover."],
364
+ build: ["Implement STOP as an unconditional zero/disarm transition and ensure the visible control never submits a form or becomes disabled. Keep recovery explicit: fresh safe evidence, a deliberate arm, then a valid separate command.", "A safety transition must still run when optional logging or a preparation field cannot be processed."],
365
+ run: ["Start moving, type invalid power, then use STOP by keyboard and touch. Repeat while disconnected and already stopped. Receive safe telemetry and confirm outputs stay zero until you deliberately arm and command again.", "Keep the action label readable and focus visible so stopping does not depend on a pointer or a colour cue."],
366
+ assess: ["Check STOP across moving, armed, disconnected, invalid-field, missing-telemetry and exhausted-history states. Verify zero outputs, disarming, preserved prepared fields and no automatic restart after fault clearance.", "The simulator host also stops motors on execution failure; your learner code must independently satisfy the documented STOP contract."],
367
+ inspect: ["If STOP is blocked, inspect whether form validation, connection guards or logging run before it. If movement returns on the next packet, inspect retained arming or implicit command replay.", "The first responsibility of a stop path is establishing zero output, not preserving an optimistic ready state."],
368
+ fix: ["Move the stop transition ahead of unrelated prerequisites and remove automatic recovery movement. Replay each fault condition, then verify normal deliberate re-arming still permits a new valid command.", "A dependable stop should not make recovery impossible; it should make recovery a conscious, checked action."],
369
+ explain: ["Explain why STOP ignores invalid form input and why safe telemetry alone does not authorize restarting. Choose which state must be cleared immediately even if logging is unavailable.", "Keeping prepared values can be convenient, but keeping permission to resume movement automatically would defeat deliberate recovery."],
370
+ reward: ["Save Dependable STOP. Your control panel can halt movement without being trapped by another feature. Next you will keep stale and unsafe evidence from authorizing commands.", "Retain the invalid-field STOP test because it crosses interface validation and safety behaviour."]
371
+ },
372
+ questions: {
373
+ learn: { question: "Which condition may block STOP?", choices: ["None of the form, connection or history conditions", "An invalid speed field", "An exhausted history counter"], correctChoice: 0, feedback: "The stop action must zero outputs and disarm regardless of unrelated validation, connection or logging prerequisites." },
374
+ predict: { question: "Safe telemetry arrives after STOP; what should happen to motors?", choices: ["Resume automatically", "Stay zero until deliberate arming and a new command", "Use the last typed speed immediately"], correctChoice: 1, feedback: "Fresh evidence can make recovery possible but does not replace the person's explicit decision to arm and command again." },
375
+ explain: { question: "Which fields must STOP change immediately?", choices: ["Only the heading", "Only the speed draft", "Both motor outputs and armed state"], correctChoice: 2, feedback: "Stopping establishes zero wheel output and removes authorization to resume without another deliberate arming step." }
376
+ }
377
+ },
378
+ {
379
+ title: "Trust only fresh telemetry",
380
+ concepts: ["Watchdogs", "Packet ordering", "Bounded event history"],
381
+ goals: ["Stop on expired, malformed, out-of-order or hazardous evidence.", "Keep recovery deliberate and record meaningful transitions without unbounded history."],
382
+ extension: "Compare a repeated packet with a new packet that reports the same values. Explain why identical observations can still differ in their freshness evidence.",
383
+ activities: {
384
+ learn: ["Telemetry is useful only while its identity, timing and values are valid. Age up to 500ms is fresh; at 501ms the clock must zero and disarm. Battery below 20 or an obstacle also stops movement. Duplicate, older, malformed and future packets clear observations and stop rather than refreshing trust.", "The host supplies bounded clock ticks and independently stops virtual motors if learner execution fails or times out."],
385
+ predict: ["Predict motion at telemetry ages 500 and 501ms, then for a duplicate sequence, a future timestamp, battery 19 and obstacle=true. Compare receiving a later safe packet with explicitly arming after that packet.", "A new packet may restore valid observations while armed remains false; evidence and permission have different lifetimes."],
386
+ build: ["Finish tick expiry, strict sequence/timestamp checks and hazard stopping. Add newest-first history for committed connect/disconnect/arm/command and state-changing stop/fault events, bounded to 20 entries with stable increasing IDs. Ensure history exhaustion cannot block stop or disconnect.", "Routine telemetry and clock updates need not produce history entries or live announcements unless they cause a meaningful state change."],
387
+ run: ["Use supplied stale, reordered, future and hazardous packet scenarios while moving. Confirm the exact stop boundary, then send fresh safe evidence and recover deliberately. Inspect history for the first fault without repeated idle-stop entries.", "Show missing, fresh, stale and hazardous observations honestly instead of retaining a green label from an earlier packet."],
388
+ assess: ["Check age boundaries, strict increasing sequences, finite battery bounds, boolean obstacle, future timestamps, disconnected packets, session expiry and no automatic recovery. Check event order, resulting output records, 20-entry limit and safety behaviour when IDs are exhausted.", "A watchdog needs an independent advancing clock; checking age only when another packet arrives cannot detect a lost connection."],
389
+ inspect: ["If silence never stops movement, inspect tick handling rather than packet arrival. If repeated packets keep the rover armed, inspect sequence comparison. If faults flood history, inspect whether any safety state actually changed.", "Separate the first meaningful transition from later observations of the same already-stopped condition."],
390
+ fix: ["Repair the evidence boundary and replay safe-stale-safe input without automatic movement. Then deliberately arm and command, verifying earlier direction, field validation and STOP cases still pass.", "Do not make the freshness window larger merely to hide a failed boundary test; apply the documented 500ms rule consistently."],
391
+ explain: ["Explain why a watchdog checks elapsed time during silence and why duplicate sequences cannot renew trust. Describe how bounded history records useful evidence without becoming the safety authority.", "The current motor and armed state determine safety; a convincing log entry cannot substitute for actually stopping."],
392
+ reward: ["Save Fresh evidence. Your rover now refuses unsafe or outdated observations and recovers only deliberately. The final mission connects the full control-panel journey and its evidence.", "Keep the exact 500/501ms pair as a small regression case for future timing changes."]
393
+ },
394
+ questions: {
395
+ learn: { question: "When does telemetry exceed the documented freshness window?", choices: ["At age 500ms", "At age 501ms", "Only when another packet arrives"], correctChoice: 1, feedback: "Age five hundred is still accepted, while the independently advancing clock must stop the rover once age exceeds it." },
396
+ predict: { question: "A packet repeats the last accepted sequence; what should happen?", choices: ["Refresh trust without checking time", "Keep moving forever", "Stop, disarm and invalidate observations"], correctChoice: 2, feedback: "The course requires strictly newer sequence evidence, so a repeated identity cannot renew trust and triggers a safe refusal." },
397
+ explain: { question: "Why must expiry be checked on clock ticks?", choices: ["No new packet may arrive after a connection is lost", "To make every tick a command", "To remove sequence validation"], correctChoice: 0, feedback: "A watchdog must detect silence itself; relying on packet arrival would leave movement authorized when communication stops entirely." }
398
+ }
399
+ },
400
+ {
401
+ title: "Demonstrate safe mission control",
402
+ concepts: ["Integrated control", "Accessible operation", "Capstone"],
403
+ goals: ["Deliver a truthful simulator panel with bounded commands and dependable recovery.", "Verify current-source behaviour and accessible use across normal and failure scenarios."],
404
+ extension: "Propose an additional virtual command in a separate save, specifying bounds, safe prerequisites, STOP behaviour and failure scenarios before changing the completed panel.",
405
+ activities: {
406
+ learn: ["A complete control panel communicates connection, evidence, arming, intent and actual output clearly. Safe operation includes refused commands and deliberate recovery, not just a successful movement demonstration. The capstone assesses the current source against all established rules.", "This remains a simulator course; physical hardware would require separate approval and engineering protections outside this project."],
407
+ predict: ["Plan a demonstration from disconnected state through fresh telemetry, arming, movement, invalid field input, STOP, stale evidence and deliberate recovery. Predict motor and armed state after every action.", "Include a period of silence so the demonstration proves the watchdog works without another incoming packet."],
408
+ build: ["Finish status text, field errors, control permissions and readable history. Review all three files for consistent bindings and make STOP visibly distinct without relying only on colour. Preserve the host's independent execution-failure stop boundary.", "Display requested values separately from actual motor output so final styling does not reintroduce a misleading shortcut."],
409
+ run: ["Perform the demonstration with keyboard and touch at 320px, enlarged text, both themes and reduced motion. Check control names, error links and meaningful status announcements with a screen reader. Save/reload the source and replay the same simulated inputs.", "Frequent telemetry should not steal focus or overwhelm the announcement of a stop or refused command."],
410
+ assess: ["Run final initial-state, connection, packet-validation, direction, power, arming, STOP, expiry, hazard, history and reset scenarios. Verify no automatic restart, detached state and readable accessible operation, then assess the exact saved project.", "A successful command alone cannot establish dependable control across communication and validation failures."],
411
+ inspect: ["Find the earliest incorrect transition in a failed replay and separate preparation, evidence, permission, output and projection. Compare the tested source with the current saved source before drawing a completion conclusion.", "A status message that says Stopped is insufficient if either underlying motor output remains nonzero."],
412
+ fix: ["Repair the responsible boundary, rerun the focused failure and repeat the full normal-to-fault-to-recovery journey. Save a named final version and obtain fresh assessment evidence after the last edit.", "Retain the direct-action and invalid-form STOP cases even if the visible controls appear correctly disabled."],
413
+ explain: ["Explain how safe evidence differs from permission to move, why STOP bypasses other features and how the watchdog handles silence. Support one accessibility decision with observations from the actual control journey.", "Name the simulator's limits honestly rather than treating a course result as authorization to control real equipment."],
414
+ reward: ["Save Mission control ready and finish the final assessment. You have built an editable web control panel with bounded commands, fresh evidence and deliberate recovery. Replay missions or extend a separate save while retaining earned completion.", "Reset restarts the virtual rover independently of your account-bound source and verified learning progress."]
415
+ },
416
+ questions: {
417
+ learn: { question: "What does a complete control demonstration include?", choices: ["Only the first successful move", "Only the heading design", "Normal commands, refusals, faults and deliberate recovery"], correctChoice: 2, feedback: "Dependable control is established through both intended actions and the ways the system stops or refuses under adverse conditions." },
418
+ predict: { question: "Which state proves STOP actually worked?", choices: ["Both outputs zero and armed false", "Only a Stopped message", "Only a red button"], correctChoice: 0, feedback: "The model must establish zero motor output and remove arming; a label or colour alone cannot prove that transition occurred." },
419
+ explain: { question: "What does finishing this simulator course authorize?", choices: ["Any physical robot command", "Course completion and continued simulator learning only", "Bypassing guardian and hardware controls"], correctChoice: 1, feedback: "The module demonstrates a bounded virtual application; real hardware remains subject to separate protections and authorization." }
420
+ }
421
+ }
422
+ ]);
423
+ // Annotate the CommonJS export names for ESM import in node:
424
+ 0 && (module.exports = {
425
+ course,
426
+ practice
427
+ });
428
+ //# sourceMappingURL=robot-mission-control.cjs.map