@plasius/learning 0.7.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.
package/README.md CHANGED
@@ -101,6 +101,27 @@ project limits after replacement. Hosts must show the actual diff and obtain an
101
101
  explicit learner choice before using it. Neither helper executes code, approves
102
102
  a change, writes storage or awards evidence; independently assess the saved result.
103
103
 
104
+ The web entries `courses/adventure-mission-planner`,
105
+ `courses/creature-care-dashboard` and `courses/robot-mission-control` complete the
106
+ set of seventeen learner curricula. Each exports `course` and `practice`, with
107
+ six authored missions, 54 activities, 18 formative checks and editable
108
+ `index.html`, `app.css` and `app.js` starter files. The planner teaches semantic
109
+ forms, validated records, stable identities, revision and corrupt-snapshot
110
+ recovery. Creature Care teaches resource accounting, deterministic time, rest
111
+ versus pause and bounded history. Mission Control teaches connection, explicit
112
+ arming, bounded motor commands, independent STOP, telemetry expiry and deliberate
113
+ recovery. All include responsive styling, native labels, focus, readable feedback
114
+ and accessibility verification in the authored journey.
115
+
116
+ Web starters deliberately leave later behaviour for the learner to implement.
117
+ They are HTML fragments with explicit data/action bindings and pure JavaScript
118
+ state functions, not unrestricted browser scripts. The separately released
119
+ runtime's lazy `/web` compiler and isolated JavaScript worker are host facilities;
120
+ this package imports neither. Preview plan storage is simulated state, distinct
121
+ from account source saves. Robot control is virtual and grants no hardware access.
122
+ See [web course design](docs/tdrs/tdr-0014-complete-web-course-journeys.md) for the
123
+ runtime boundary, progressive assessment scope and host acceptance obligations.
124
+
104
125
  The runtime is supplied separately by `@plasius/learning-runtime`; this package
105
126
  does not execute the project or declare host readiness. The seventeen-course
106
127
  programme remains in development. This additive content does not alter immutable
@@ -0,0 +1,43 @@
1
+ // src/courses/web-course-authoring.ts
2
+ var webProjectFiles = [
3
+ { path: "index.html", language: "html", maximumCharacters: 24e3 },
4
+ { path: "app.css", language: "css", maximumCharacters: 16e3 },
5
+ { path: "app.js", language: "javascript", maximumCharacters: 32e3 }
6
+ ];
7
+ var webStarterCss = `:root { color: #172a29; background-color: #f5f4ec; font-family: system-ui, sans-serif; line-height: 1.6; }
8
+ * { box-sizing: border-box; }
9
+ main { max-width: 60rem; margin-inline: auto; padding: 1rem; overflow-wrap: anywhere; }
10
+ h1, h2, p { margin-top: 0; }
11
+ section, article, fieldset { border: 1px solid #647874; border-radius: 0.5rem; padding: 1rem; margin-block: 1rem; min-width: 0; }
12
+ label { display: block; font-weight: 700; }
13
+ 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; }
14
+ button { cursor: pointer; }
15
+ button:disabled { cursor: default; border-style: dashed; }
16
+ :focus-visible { outline: 3px solid #075bab; outline-offset: 3px; }
17
+ [aria-invalid="true"] { border-color: #a11c25; }
18
+ .error { color: #a11c25; }
19
+ .actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
20
+ .cards { display: grid; gap: 1rem; padding: 0; list-style-type: none; }
21
+ progress, meter { width: 100%; }
22
+ @media (min-width: 48rem) { .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
23
+ @media (prefers-reduced-motion: reduce) { * { transition-duration: 0s; } }
24
+ @media (prefers-color-scheme: dark) {
25
+ :root { color: #edf4ef; background-color: #142623; }
26
+ button, input, select, textarea { color: #edf4ef; background-color: #203a34; border-color: #b8cec5; }
27
+ :focus-visible { outline-color: #a8d6ff; }
28
+ .error { color: #ffb8bf; }
29
+ [aria-invalid="true"] { border-color: #ffb8bf; }
30
+ }
31
+ `;
32
+ var webReferences = [
33
+ { 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>' },
34
+ { 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>' },
35
+ { 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>' }
36
+ ];
37
+
38
+ export {
39
+ webProjectFiles,
40
+ webStarterCss,
41
+ webReferences
42
+ };
43
+ //# sourceMappingURL=chunk-UNPCTBNK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/courses/web-course-authoring.ts"],"sourcesContent":["import type { LearningCourseV1 } from \"../course-contracts.js\";\n\n/** The web courses share file limits and a readable starting theme, not lesson content. */\nexport const webProjectFiles: LearningCourseV1[\"projectFiles\"] = [\n { path: \"index.html\", language: \"html\", maximumCharacters: 24000 },\n { path: \"app.css\", language: \"css\", maximumCharacters: 16000 },\n { path: \"app.js\", language: \"javascript\", maximumCharacters: 32000 },\n];\n\nexport const webStarterCss = `:root { color: #172a29; background-color: #f5f4ec; font-family: system-ui, sans-serif; line-height: 1.6; }\n* { box-sizing: border-box; }\nmain { max-width: 60rem; margin-inline: auto; padding: 1rem; overflow-wrap: anywhere; }\nh1, h2, p { margin-top: 0; }\nsection, article, fieldset { border: 1px solid #647874; border-radius: 0.5rem; padding: 1rem; margin-block: 1rem; min-width: 0; }\nlabel { display: block; font-weight: 700; }\nbutton, 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; }\nbutton { cursor: pointer; }\nbutton:disabled { cursor: default; border-style: dashed; }\n:focus-visible { outline: 3px solid #075bab; outline-offset: 3px; }\n[aria-invalid=\"true\"] { border-color: #a11c25; }\n.error { color: #a11c25; }\n.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }\n.cards { display: grid; gap: 1rem; padding: 0; list-style-type: none; }\nprogress, meter { width: 100%; }\n@media (min-width: 48rem) { .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }\n@media (prefers-reduced-motion: reduce) { * { transition-duration: 0s; } }\n@media (prefers-color-scheme: dark) {\n :root { color: #edf4ef; background-color: #142623; }\n button, input, select, textarea { color: #edf4ef; background-color: #203a34; border-color: #b8cec5; }\n :focus-visible { outline-color: #a8d6ff; }\n .error { color: #ffb8bf; }\n [aria-invalid=\"true\"] { border-color: #ffb8bf; }\n}\n`;\n\nexport const webReferences: LearningCourseV1[\"reference\"] = [\n { 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>' },\n { name: \"bindings\", signature: \"view(state) → 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>' },\n { 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>' },\n];\n"],"mappings":";AAGO,IAAM,kBAAoD;AAAA,EAC/D,EAAE,MAAM,cAAc,UAAU,QAAQ,mBAAmB,KAAM;AAAA,EACjE,EAAE,MAAM,WAAW,UAAU,OAAO,mBAAmB,KAAM;AAAA,EAC7D,EAAE,MAAM,UAAU,UAAU,cAAc,mBAAmB,KAAM;AACrE;AAEO,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0BtB,IAAM,gBAA+C;AAAA,EAC1D,EAAE,MAAM,sBAAsB,WAAW,iCAAiC,aAAa,yYAAyY,SAAS,4EAA4E;AAAA,EACrjB,EAAE,MAAM,YAAY,WAAW,8CAAyC,aAAa,gbAAgb,SAAS,sLAAsL;AAAA,EACpsB,EAAE,MAAM,iBAAiB,WAAW,mDAAmD,aAAa,2XAA2X,SAAS,iLAAiL;AAC3pB;","names":[]}
@@ -0,0 +1,436 @@
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/adventure-mission-planner.ts
21
+ var adventure_mission_planner_exports = {};
22
+ __export(adventure_mission_planner_exports, {
23
+ course: () => course,
24
+ practice: () => practice
25
+ });
26
+ module.exports = __toCommonJS(adventure_mission_planner_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/adventure-mission-planner.ts
237
+ var { course, practice } = authorCourse({
238
+ slug: "adventure-mission-planner",
239
+ title: "Adventure Mission Planner",
240
+ category: "web-app",
241
+ summary: "Build a real three-file web application for a fictional expedition. Structure a readable page, validate a labelled form, create and revise mission records, then recover a saved plan without accepting corrupt data. Finish an accessible planner with truthful counts and explicit feedback, using simulated storage inside the preview.",
242
+ projectFiles: webProjectFiles,
243
+ starterProject: { files: [
244
+ { path: "index.html", source: `<main>
245
+ <h1>Adventure Mission Planner</h1>
246
+ <p>Plan a fictional expedition, one mission at a time.</p>
247
+ <section aria-labelledby="entry-heading">
248
+ <h2 id="entry-heading">New mission</h2>
249
+ <form data-action="add" novalidate>
250
+ <label for="title">Mission title</label>
251
+ <input id="title" name="title" type="text" maxlength="80" aria-required="true" aria-describedby="title-error" data-value="draft.title" data-invalid="titleInvalid">
252
+ <p id="title-error" class="error" role="alert" data-text="titleError"></p>
253
+ <label for="minutes">Minutes, from 5 to 180</label>
254
+ <input id="minutes" name="minutes" type="number" min="5" max="180" step="1" aria-required="true" aria-describedby="minutes-error" data-value="draft.minutes" data-invalid="minutesInvalid">
255
+ <p id="minutes-error" class="error" role="alert" data-text="minutesError"></p>
256
+ <label for="priority">Priority</label>
257
+ <select id="priority" name="priority" data-value="draft.priority"><option value="low">Low</option><option value="normal">Normal</option><option value="high">High</option></select>
258
+ <button type="submit">Add mission</button>
259
+ </form>
260
+ </section>
261
+ <section aria-labelledby="plan-heading"><h2 id="plan-heading">Your plan</h2>
262
+ <p data-text="summary"></p><ul class="cards"><li data-repeat="missions"><strong data-text="title"></strong><p data-text="details"></p></li></ul>
263
+ </section>
264
+ <p role="status" data-text="message"></p>
265
+ <button type="button" data-action="reset">Reset preview</button>
266
+ </main>` },
267
+ { path: "app.css", source: webStarterCss },
268
+ { path: "app.js", source: `function initialState() {
269
+ return { missions: [], draft: { title: "", minutes: "15", priority: "normal" }, editingId: null,
270
+ filter: "all", nextId: 1, savedSnapshot: null, titleError: "", minutesError: "", message: "Add a fictional mission to begin." };
271
+ }
272
+ function update(state, input) {
273
+ if (input.type === "reset") return initialState();
274
+ return JSON.parse(JSON.stringify(state));
275
+ }
276
+ function validateDraft(draft) { return { valid: false, titleError: "Check the title.", minutesError: "Check the minutes." }; }
277
+ function snapshot(state) { return ""; }
278
+ function restoreSnapshot(text) { return null; }
279
+ function view(state) {
280
+ return { draft: { ...state.draft }, missions: [], totalCount: state.missions.length, completedCount: 0,
281
+ totalMinutes: 0, summary: "No missions yet.", isCreating: state.editingId === null, isEditing: state.editingId !== null,
282
+ titleError: state.titleError, minutesError: state.minutesError, titleInvalid: state.titleError !== "",
283
+ minutesInvalid: state.minutesError !== "", hasSavedPlan: state.savedSnapshot !== null, message: state.message };
284
+ }
285
+ ` }
286
+ ] },
287
+ reference: [
288
+ ...webReferences,
289
+ { name: "initialState", signature: "initialState() \u2192 empty planner", description: "Fresh state has missions=[], draft {title:'',minutes:'15',priority:'normal'}, editingId=null, filter='all', nextId=1, savedSnapshot=null, empty titleError/minutesError and a useful message. Records have only id, title, minutes, priority and done. There are at most 20 missions. IDs are mission-N for integers N from 1 to 999999; nextId is an integer from 1 to 1000000 and always exceeds every allocated ID.", example: '{ id: "mission-1", title: "Find the beacon", minutes: 15, priority: "normal", done: false }' },
290
+ { name: "validateDraft", signature: "validateDraft(draft) \u2192 {valid,titleError,minutesError}", description: "Title is trimmed, 1\u201380 characters; reject control characters. Minutes is a string of one to three decimal digits whose integer value is 5\u2013180; reject blanks, signs, fractions and exponent notation. Priority must be low, normal or high. Return empty errors for valid fields and useful field errors otherwise. valid requires all three fields; an invalid priority also produces general feedback. Do not change the supplied draft.", example: "const minutesValid = /^\\d{1,3}$/.test(draft.minutes) && Number(draft.minutes) >= 5 && Number(draft.minutes) <= 180;" },
291
+ { name: "update", signature: "update(state, input) \u2192 detached next state", description: "field accepts string title/minutes/priority values bounded to 80/3/6 characters; unrecognised fields or invalid priorities preserve state. add requires creating mode, a valid draft, room below 20 and an available ID. Append one trimmed record, increment nextId, clear draft/errors, preserve existing records and give feedback. Failed add/save keeps records, nextId and draft intact while setting errors/message. Unknown actions preserve values.", example: 'update(state, { type: "field", name: "title", value: "Find the beacon" });' },
292
+ { name: "revision actions", signature: "edit/save/cancel/toggle/remove/filter", description: "edit{id} copies an existing record into the draft and selects its ID; save validates then changes its title/minutes/priority while retaining ID/done; cancel clears draft and editingId. toggle{id} flips done once. remove{id} removes exactly that record, cancelling if it was being edited. Unknown IDs preserve state. filter is a named field whose value is all/open/done; it changes only filter and cancels editing. add never acts as save and save never acts as add.", example: 'update(state, { type: "edit", id: "mission-1" });' },
293
+ { name: "view", signature: "view(state) \u2192 labelled display projection", description: "Return draft, filtered missions with id/title/done/details/toggleLabel/editLabel/removeLabel, totalCount/completedCount/totalMinutes from all records, summary, isCreating/isEditing, titleError/minutesError and matching titleInvalid/minutesInvalid booleans, hasSavedPlan and message. Include filter for a bound select. Render record actions with data-id and data-label; never use record text as HTML. Reading view changes no state.", example: 'const toggleLabel = (mission.done ? "Reopen " : "Complete ") + mission.title;' },
294
+ { name: "snapshot", signature: "snapshot(state) \u2192 JSON string", description: "Serialize only {schemaVersion:1,nextId,missions}, at most 16000 characters. store sets savedSnapshot to that string and reports success. Preview storage is simulated in state: it is not localStorage, an account API or a cloud backup. reset clears the preview including this snapshot; account project saves remain separate.", example: "JSON.stringify({ schemaVersion: 1, nextId: state.nextId, missions: state.missions });" },
295
+ { name: "restoreSnapshot", signature: "restoreSnapshot(text) \u2192 validated snapshot | null", description: "Check string length, parse errors, exact object/record keys, version 1, 0\u201320 records, unique mission-N IDs, trimmed valid titles, bounded integer minutes, allowed priorities, boolean done and nextId greater than every record ID. Reject duplicate IDs, extra properties and corrupt data as null. reload validates savedSnapshot before replacing records/nextId; success clears draft/errors/editing and resets filter to all. Invalid/missing snapshots preserve working data and change only message. Retain savedSnapshot on either outcome.", example: 'const restored = restoreSnapshot(state.savedSnapshot); if (restored === null) return { ...state, message: "Saved plan could not be loaded." };' }
296
+ ]
297
+ }, [
298
+ {
299
+ title: "A page with a purpose",
300
+ concepts: ["Semantic HTML", "CSS layout", "Display data"],
301
+ goals: ["Give a fictional planner a clear heading structure and readable responsive layout.", "Distinguish editable HTML, CSS and JavaScript responsibilities."],
302
+ extension: "Create a second expedition theme in a named save while preserving semantic headings, readable text and the same data contract.",
303
+ activities: {
304
+ learn: ["The planner uses three editable files. HTML names the page, form and mission list; CSS controls readable layout; JavaScript produces state and display data. Start with one main landmark and an h1, then h2 headings for entering and reviewing missions.", "The preview accepts an HTML fragment. It supplies the surrounding document and loads app.css separately, so do not add script or link tags."],
305
+ predict: ["Predict what changes when you edit only the heading text, only the main padding, or only the initial message. Identify which file owns each effect before running the starter.", "A visual change does not automatically change the mission records stored in JavaScript state."],
306
+ build: ["Give the page a fictional expedition identity, retain visible field labels and organise the two sections. Adjust app.css so the form and list fit 320px without horizontal page scrolling and remain readable in both colour themes.", "Use flexible widths, wrapping text and min-width:0 for layout children. Keep visible focus outlines on controls."],
307
+ run: ["Open the empty planner, move through its controls with the keyboard and inspect its heading order. Zoom the page and compare narrow and wide views. The initial summary should truthfully say there are no missions.", "The Add button is intentionally unfinished at this stage; later lessons connect its action to validated state changes."],
308
+ assess: ["Check the main landmark, meaningful headings, visible control labels, empty-state projection and responsive style rules. Confirm view reads state without changing it and reset produces a fresh empty planner.", "This milestone assesses an honest starting interface; it does not require the later record-creation behaviour yet."],
309
+ inspect: ["If a panel overflows, inspect widths and long text before shrinking the font. If a control is hard to find, inspect its label and focus styling rather than relying on a decorative icon.", "A narrow preview is a useful way to reveal assumptions that a wide desktop view hides."],
310
+ fix: ["Repair the page structure or styles and repeat the keyboard, zoom and 320px checks. Keep the message bound through data-text so changing state updates actual text instead of executable markup.", "The same source should work at multiple widths; avoid separate narrow-page copies that drift apart."],
311
+ explain: ["Explain why headings describe the document while colours and spacing belong in CSS. Choose the change that improves navigation for someone using a screen reader.", "A heading's meaning comes from its HTML level and text, not from making an ordinary paragraph look large."],
312
+ reward: ["Save Readable expedition. Your planner now has a clear structure and a truthful empty view. The next mission gives the form useful validation and feedback.", "Keep this working three-file starting point so later changes can be compared with it."]
313
+ },
314
+ questions: {
315
+ learn: { question: "Which file should describe the planner's heading structure?", choices: ["index.html", "app.css only", "The saved snapshot"], correctChoice: 0, feedback: "Semantic headings belong in HTML, while CSS styles their appearance and JavaScript supplies changing application data." },
316
+ predict: { question: "What does changing only main padding normally affect?", choices: ["The mission IDs", "The space around page content", "The number of saved missions"], correctChoice: 1, feedback: "Padding is a layout property, so it changes spacing without changing the planner's records or identities." },
317
+ explain: { question: "Which change helps heading-based screen-reader navigation?", choices: ["A larger paragraph font", "A decorative border", "Meaningful h1 and h2 elements"], correctChoice: 2, feedback: "Real heading elements expose document structure to assistive technology instead of communicating it only visually." }
318
+ }
319
+ },
320
+ {
321
+ title: "A form that explains itself",
322
+ concepts: ["Native controls", "Validation", "Accessible errors"],
323
+ goals: ["Handle bounded field input without changing mission records.", "Validate titles, minutes and priorities and explain invalid fields accessibly."],
324
+ extension: "Compare error wording with a partner using fictional input. Revise the wording to describe how to recover while keeping the same validation rules.",
325
+ activities: {
326
+ learn: ["A form needs to explain what went wrong without losing the user's work. Native controls emit field actions. validateDraft checks trimmed title, decimal minutes and allowed priority. A labelled error linked with aria-describedby and a matching aria-invalid state makes the failure discoverable.", "The form uses novalidate so your own consistent feedback can run; JavaScript must still enforce every documented rule."],
327
+ predict: ["Predict validation for a blank title, whitespace around Beacon walk, minutes 4, 15.5, 1e2 and 180. Separate the displayed draft from the normalised values you will eventually save.", "Number inputs still emit strings. Accepting Number(value) alone would also accept some formats the planner explicitly excludes."],
328
+ build: ["Handle title, minutes and priority field actions using detached state. Implement validateDraft and make add report field errors for invalid input without creating records yet. Bind the error text and booleans, and clear a field's stale error when that field changes.", "Do not replace the draft with trimmed data while the user is typing. Normalise only when a valid submission is committed."],
329
+ run: ["Submit empty and malformed drafts, then correct them using the keyboard. Verify visible errors, retained input, screen-reader associations and focus preservation while typing. Try the same form at 320px.", "A repeated render should not move focus to the page top or replace the active field's editing selection."],
330
+ assess: ["Check accepted boundaries 5 and 180, rejected blank/fraction/exponent minutes, whitespace-only and overlong titles, forbidden control characters and invalid priorities. Verify errors match fields and validation has no mutation effects.", "Useful validation checks both the successful case and nearby inputs that look plausible but violate a documented rule."],
331
+ inspect: ["If a rejected input disappears, inspect draft updates. If an error remains after correction, inspect error clearing. If the form reloads the preview, inspect submission ownership and remove any separate action on its submit button.", "One submission belongs to the form; two independent handlers can accidentally create duplicate transitions later."],
332
+ fix: ["Repair validation and field feedback, then replay invalid-to-valid input. Confirm a valid draft reports empty field errors, invalid priority cannot slip through, and no attempt has changed missions or nextId.", "Form correction is separate from record creation; preserve that boundary until the next mission implements commit behaviour."],
333
+ explain: ["Explain why the original draft is kept after failure and why aria-invalid alone is insufficient without useful text. Identify the format check that prevents exponent notation being accepted as ordinary minutes.", "A person needs both an indication of the problem and enough information to correct it."],
334
+ reward: ["Save Helpful form. Your controls accept bounded input and explain validation failures. Next you will commit valid drafts into uniquely identified mission records.", "Retain the invalid-to-valid examples as regression cases for later editing and storage work."]
335
+ },
336
+ questions: {
337
+ learn: { question: "What should a failed submission preserve for the user?", choices: ["Only the heading", "Their draft and existing mission records", "Only the invalid field's colour"], correctChoice: 1, feedback: "Keeping the draft and existing records lets the person correct the problem without losing their current or earlier work." },
338
+ predict: { question: "Which minutes string satisfies the documented format and bounds?", choices: ["15.5", "1e2", "180"], correctChoice: 2, feedback: "The planner accepts one to three decimal digits with an integer value from five to one hundred and eighty." },
339
+ explain: { question: "Why link an invalid input to a written error?", choices: ["To explain how to correct it beyond colour or a flag", "To submit twice", "To remove the field label"], correctChoice: 0, feedback: "A linked, useful error communicates the reason and recovery step to people who may not perceive the visual styling." }
340
+ }
341
+ },
342
+ {
343
+ title: "Turn entries into missions",
344
+ concepts: ["Arrays and records", "Stable identity", "Atomic changes"],
345
+ goals: ["Commit valid drafts as unique records with bounded storage.", "Project mission cards and summary counts from the same source of truth."],
346
+ extension: "Show a total-time sentence that changes as missions are added. Keep the calculation derived from records rather than maintaining a second editable total.",
347
+ activities: {
348
+ learn: ["A mission is a record with a stable ID, title, minutes, priority and done flag. Valid add commits one record and increments nextId together. Keep at most 20 missions and never reuse IDs after removal; summaries are calculated from records rather than separately counted clicks.", "Stable identity matters even when two fictional missions share the same title."],
349
+ predict: ["Predict IDs and totals after adding two valid missions with an invalid submission between them. Then predict an attempt to add a twenty-first mission and whether its failure should consume an ID.", "A refused operation must not partly update the list or counter before checking all prerequisites."],
350
+ build: ["Complete add in creating mode: validate, check capacity and ID availability, append one trimmed record, increment nextId and clear the successful draft. Implement view with repeated mission records, total/completed counts, total minutes and a truthful summary.", "Use data-repeat with unique id values and data-text for mission text. Keep HTML IDs out of repeated card content."],
351
+ run: ["Create two missions, compare their labels, IDs and minute total, then attempt an invalid third. Check that typing text resembling HTML displays as text and cannot create elements in the page.", "The renderer's text boundary and your app's record validation serve different purposes; both must remain intact."],
352
+ assess: ["Check single-record commits, trimmed titles, numeric minutes, unique increasing IDs, 20-record capacity, exhausted IDs, unchanged records on refusal and nonmutating display projection. Verify summaries describe the complete list.", "An attractive card cannot establish that the underlying record or identity was committed correctly."],
353
+ inspect: ["If duplicates appear, inspect whether the form submits twice or nextId increments separately from insertion. If totals drift after refusal, replace independent counters with calculations over the current records.", "Look for the first state transition that differs from your prediction, not just the last visible symptom."],
354
+ fix: ["Repair the commit boundary and rerun valid-invalid-valid submissions. Confirm the failed attempt leaves no partial record, the successful draft clears, and the next record receives the next unused ID.", "Prepare a detached next state, then return the coherent result only after all required conditions are known."],
355
+ explain: ["Explain why a title is a poor identifier and why a failed add should leave nextId unchanged. Choose the implementation that keeps list and count changes coherent.", "Identity lets later edits target a record even when its title or displayed position changes."],
356
+ reward: ["Save Mission maker. Your planner now turns valid input into meaningful records and honest summaries. The next mission adds editing, completion and focused views.", "Keep a two-record example with different priorities for exercising revision without confusing identity with position."]
357
+ },
358
+ questions: {
359
+ learn: { question: "Why does each mission need an ID separate from its title?", choices: ["Titles can never change", "Titles must be secret", "A record must remain identifiable after edits or reordering"], correctChoice: 2, feedback: "Stable identity keeps later actions attached to the intended record even when its human-readable title changes." },
360
+ predict: { question: "Two valid adds with one refused add between them produce which IDs?", choices: ["mission-1 and mission-2", "mission-1 and mission-3", "Two copies of mission-1"], correctChoice: 0, feedback: "The refused add commits neither a record nor an ID increment, so the next successful record receives mission-2." },
361
+ explain: { question: "Where should totalMinutes come from?", choices: ["The number of Add clicks", "The current complete mission list", "The last typed minutes field"], correctChoice: 1, feedback: "Deriving the summary from current records prevents rejected attempts and later edits from drifting a separate total." }
362
+ }
363
+ },
364
+ {
365
+ title: "Revise without losing the plan",
366
+ concepts: ["Editing state", "Record actions", "Filtering"],
367
+ goals: ["Edit, complete and remove exactly the intended record.", "Separate filtered presentation from stored data and keep editing modes explicit."],
368
+ extension: "Add a visible explanation of the current filter. Check that global totals remain understandable when only one part of the plan is visible.",
369
+ activities: {
370
+ learn: ["Editing copies a record into a draft while preserving its ID and done status until save. Toggle and remove also target IDs. Filtering changes what view returns, never what missions contains. Cancelling or changing filter clears editing so a hidden record is not accidentally modified.", "Creating and editing are distinct modes: add must not become save merely because a draft happens to contain existing text."],
371
+ predict: ["Predict a plan after completing its first record, filtering to open, editing the second and cancelling. Compare visible cards with global counts, then predict removing the record currently being edited.", "The total count describes all records even when the filtered list is smaller."],
372
+ build: ["Add labelled per-record edit/toggle/remove buttons using data-id and data-label. Implement edit, save, cancel, toggle, remove and the filter field. Show the correct Add or Save controls for each mode, retain ID/done on save and clear editing when removal targets it.", "Use data-if for mode-specific controls and data-pressed for completion state, with readable labels that do not rely on colour."],
373
+ run: ["Use the keyboard to edit one of two similarly titled missions, submit an invalid edit, correct it, cancel another edit and change filters. Remove one record and add a new one to check that its ID is not reused.", "Follow focus through the interaction; when a clicked record disappears, the host should restore focus to a sensible surviving control."],
374
+ assess: ["Check exact-ID targeting, invalid-edit preservation, retained completion state, cancel without effects, unknown IDs, filter preservation and no ID reuse. Check that save cannot create a record and add cannot overwrite one.", "These cases cross several features, so include them alongside the earlier creation and validation checks."],
375
+ inspect: ["If the wrong card changes, inspect index-based targeting. If completed work vanishes after filtering, inspect whether a filtered array replaced the source list. If an edit becomes a duplicate, inspect mode guards.", "A display index is temporary and should never become the record's long-term identity."],
376
+ fix: ["Repair the responsible transition and replay edit, filter, remove and add with two similar titles. Recheck summaries and focus, including the case where no visible missions remain.", "An empty filtered list needs an understandable message while preserving the hidden records."],
377
+ explain: ["Explain the difference between filtering records and deleting them. Choose which fields a successful edit must retain and describe why separate modes protect the plan.", "Useful editing changes the intended content while preserving the record's identity and established completion state."],
378
+ reward: ["Save Revisable plan. Your planner can change its mind without losing records or confusing identities. Next you will store and safely restore a complete plan.", "Keep the mixed completed/open example as a useful snapshot recovery fixture."]
379
+ },
380
+ questions: {
381
+ learn: { question: "What should filtering change in the stored mission list?", choices: ["Nothing; it changes the view", "It deletes hidden records", "It replaces every ID"], correctChoice: 0, feedback: "Filtering selects a presentation of the existing records, so switching back can reveal the same intact plan." },
382
+ predict: { question: "What survives a successful title-and-minutes edit?", choices: ["Only the old title", "The record's ID and done flag", "No part of the existing record"], correctChoice: 1, feedback: "An edit changes the draft-owned fields while retaining stable identity and the record's completion state." },
383
+ explain: { question: "Why should add refuse to run during editing mode?", choices: ["To disable all keyboard controls", "To hide field errors", "To prevent an edit from accidentally becoming a duplicate record"], correctChoice: 2, feedback: "Explicit operation modes prevent one user intention from silently creating a different kind of state change." }
384
+ }
385
+ },
386
+ {
387
+ title: "Recover a saved plan",
388
+ concepts: ["Serialization", "Untrusted data", "Recovery"],
389
+ goals: ["Serialize only the plan data needed for recovery.", "Validate a whole snapshot before replacing working state and explain recovery failures."],
390
+ extension: "Design a recovery message for an unsupported future snapshot version. Explain why guessing at its meaning could damage an otherwise valid current plan.",
391
+ activities: {
392
+ learn: ["Saved text is input that must be checked again. snapshot writes version, nextId and missions; restoreSnapshot validates the full shape and every record before returning data. The exercise uses simulated storage in preview state, separate from account saves of the source project.", "A snapshot does not include errors, active editing or filters, because recovery should reopen a stable plan rather than a half-finished interaction."],
393
+ predict: ["Predict recovery for valid data, truncated JSON, duplicate IDs, a wrong version and nextId equal to an existing record number. Decide which state should survive when restoreSnapshot returns null.", "Successful JSON parsing proves syntax only; it does not establish that the data describes a valid plan."],
394
+ build: ["Implement bounded snapshot and restoreSnapshot, checking exact keys, types, record limits, identity uniqueness and nextId ordering. Add Store plan and Reload plan controls. On valid reload replace records/nextId together and reset interaction state; on failure preserve the working plan and report a useful message.", "Validate before replacement. A try/catch around JSON.parse is necessary but insufficient for record and identity rules."],
395
+ run: ["Store a mixed plan, edit it, then reload the stored version. Use the supplied corrupt-snapshot scenarios and confirm current records survive each refusal. Try reload before any snapshot exists and compare the feedback.", "The saved snapshot lives only in this preview session; the course's account-bound source saves are a separate feature."],
396
+ assess: ["Check round-trip equality, version and exact-key rules, malformed/oversized input, invalid record fields, duplicate identities and invalid nextId. Verify corrupt reload has no partial effects and successful reload resets filter, draft and editing consistently.", "Restoring valid records one at a time before the whole snapshot passes can leave a mixed old-and-new plan."],
397
+ inspect: ["If restore accepts broken data, identify the first missing validation rule. If failure destroys the current plan, inspect assignment order. If a later add duplicates an ID, inspect nextId against the greatest restored record number.", "A saved counter and its records form one coherent data set; checking them separately is not enough."],
398
+ fix: ["Repair the validator or replacement boundary and replay valid-corrupt-valid recovery. Confirm the snapshot itself remains available, recovery errors are readable, and the current source still passes earlier editing and creation checks.", "Keep rejected data from altering either working records or the snapshot that the user deliberately stored."],
399
+ explain: ["Explain why syntactically valid JSON can still be invalid application data. Describe the difference between storing a preview plan and saving this three-file project to your Plasius account.", "One preserves simulated application data for the exercise; the other preserves authored source and learning progress."],
400
+ reward: ["Save Recoverable plan. Your planner can recover known data and refuse corrupt input without losing current work. The final mission combines the whole journey into a complete accessible application.", "Retain the duplicate-ID and wrong-version cases because they test different recovery boundaries."]
401
+ },
402
+ questions: {
403
+ learn: { question: "What does successful JSON.parse establish?", choices: ["All mission IDs are unique", "The text has valid JSON syntax", "The nextId counter is safe"], correctChoice: 1, feedback: "Parsing checks representation syntax; application rules such as uniqueness, versions and counter ordering still need validation." },
404
+ predict: { question: "What should remain after reloading a corrupt snapshot?", choices: ["An empty planner", "Half of the snapshot's records", "The current working plan with useful failure feedback"], correctChoice: 2, feedback: "Validation must finish before replacement, so a rejected snapshot cannot partially overwrite or erase the working plan." },
405
+ explain: { question: "Why must restored nextId exceed every existing mission number?", choices: ["To prevent the next add reusing an existing identity", "To make JSON prettier", "To change all old mission titles"], correctChoice: 0, feedback: "The allocation counter must agree with restored records or a future add could create an ambiguous duplicate identity." }
406
+ }
407
+ },
408
+ {
409
+ title: "Your complete adventure planner",
410
+ concepts: ["Integrated behaviour", "Accessibility evidence", "Capstone"],
411
+ goals: ["Deliver a coherent planner with validation, revision, summaries and recoverable data.", "Demonstrate the complete keyboard journey and assess the exact saved source."],
412
+ extension: "Propose a new planning feature in a separate save, naming its data invariants, accessible interactions and recovery implications before implementing it.",
413
+ activities: {
414
+ learn: ["A complete planner connects structure, validation, record identity, revision and recovery into one understandable task. Its visual theme supports that task, while readable errors and stable focus help people recover from mistakes. The capstone checks the whole current source.", "A previous successful assessment cannot prove a later edit still works; final evidence must match the saved project."],
415
+ predict: ["Plan a demonstration that adds two missions, rejects an invalid edit, completes one, filters the list, stores a snapshot, removes a record and reloads. Predict counts, minutes, identities and editing mode at each step.", "Include a refusal and a recovery so the demonstration covers more than the easiest successful path."],
416
+ build: ["Finish the authored page, labels, summaries and status messages. Review the three files together, remove abandoned controls, keep Store/Reload clearly described as simulated preview storage and ensure reset is separate from account saving.", "Consistency means a visible action, its JavaScript rule and its explanatory text describe the same operation."],
417
+ run: ["Perform the demonstration with keyboard and touch, then at 320px, with zoom, both colour themes and reduced motion. Check headings, errors and status with a screen reader. Save and reload the project source, then repeat the same plan.", "Avoid announcing every keystroke as a status update; announce completed actions and useful failures without duplicating field errors."],
418
+ assess: ["Run the final validation, creation, edit/cancel, toggle/remove, filter, identity, capacity, serialization and corrupt-recovery scenarios. Verify detached state, inert text and responsive accessible controls, then obtain current-source evidence for the final project.", "The reference scenarios check actual behaviour; a filled-out page alone does not complete this module."],
419
+ inspect: ["For any failure, locate the earliest incorrect state transition and its owning file. Compare the saved source with the tested source, especially if a recent style or label edit changed bindings.", "A renamed binding can break data flow even when the page still looks convincing."],
420
+ fix: ["Repair the smallest responsible rule, rerun its focused scenario and then the complete demonstration. Save a named final project and assess it again before completing the course.", "Keep successful earlier scenarios in the final regression set so a local repair does not undo another mission's work."],
421
+ explain: ["Explain how validation and stable identity protect the plan, why recovery checks untrusted data, and how semantic controls make the same workflow usable through different input methods.", "Use one concrete example from your own tested project for each claim rather than relying on a general statement that it works."],
422
+ reward: ["Save Expedition ready and finish the final assessment. You have built a real editable web planner from semantic structure through recoverable application state. Replay a mission or extend a separate save while retaining earned completion.", "Your source project remains account-bound; the simulated plan can be reset independently for another demonstration."]
423
+ },
424
+ questions: {
425
+ learn: { question: "What establishes the final planner's behaviour?", choices: ["A screenshot of the title", "A previous assessment of different source", "Verified scenarios against the current saved source"], correctChoice: 2, feedback: "Evidence must exercise the implemented behaviour and match the exact source being submitted for completion." },
426
+ predict: { question: "Which demonstration best covers recovery as well as normal use?", choices: ["Create, edit, store, change and reload a plan", "Open the heading once", "Change only the background colour"], correctChoice: 0, feedback: "A sequence spanning creation, revision and recovery exposes interactions that an isolated visual check cannot establish." },
427
+ explain: { question: "What should a later extension preserve?", choices: ["Only the theme colours", "Existing invariants, accessible operation and recovery behaviour", "Only the newest feature"], correctChoice: 1, feedback: "Extending a useful application means retaining the working rules and access paths that its current users already depend on." }
428
+ }
429
+ }
430
+ ]);
431
+ // Annotate the CommonJS export names for ESM import in node:
432
+ 0 && (module.exports = {
433
+ course,
434
+ practice
435
+ });
436
+ //# sourceMappingURL=adventure-mission-planner.cjs.map