@plasius/learning 0.2.25 → 0.4.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/dist/index.cjs CHANGED
@@ -40,6 +40,9 @@ __export(index_exports, {
40
40
  JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_2: () => JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_2,
41
41
  JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_3: () => JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_3,
42
42
  JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_4: () => JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_4,
43
+ LEARNING_COURSE_LIMITS: () => LEARNING_COURSE_LIMITS,
44
+ LEARNING_COURSE_STAGE_ORDER: () => LEARNING_COURSE_STAGE_ORDER,
45
+ LearningCourseInputError: () => LearningCourseInputError,
43
46
  METEOR_SHIELD_MISSION_ONE_AUTHORING_V1: () => METEOR_SHIELD_MISSION_ONE_AUTHORING_V1,
44
47
  MISSION_AUTHORING_CONTRACT_VERSION_V1: () => MISSION_AUTHORING_CONTRACT_VERSION_V1,
45
48
  OBSTACLE_EXPLORER_MISSION_ONE_AUTHORING_V1: () => OBSTACLE_EXPLORER_MISSION_ONE_AUTHORING_V1,
@@ -77,8 +80,18 @@ __export(index_exports, {
77
80
  assertValidStaticProjectSnapshot: () => assertValidStaticProjectSnapshot,
78
81
  assertValidVoiceHelpAvailability: () => assertValidVoiceHelpAvailability,
79
82
  calculateAssessment: () => calculateAssessment,
83
+ completeVerifiedLearningActivity: () => completeVerifiedLearningActivity,
84
+ createLearningCourseProgress: () => createLearningCourseProgress,
80
85
  isExternalLearningContentReferenceV1: () => isExternalLearningContentReferenceV1,
86
+ parseLearningCourse: () => parseLearningCourse,
87
+ parseLearningCourseDraft: () => parseLearningCourseDraft,
88
+ parseLearningCourseProgress: () => parseLearningCourseProgress,
89
+ parseLearningProject: () => parseLearningProject,
90
+ parseLearningSaveSlotId: () => parseLearningSaveSlotId,
91
+ recordLearningCourseAssessment: () => recordLearningCourseAssessment,
92
+ resolveLearningCourseStage: () => resolveLearningCourseStage,
81
93
  validateAssessmentRubric: () => validateAssessmentRubric,
94
+ validateLearningCourse: () => validateLearningCourse,
82
95
  validateLearningPath: () => validateLearningPath,
83
96
  validateMissionAuthoringBundle: () => validateMissionAuthoringBundle,
84
97
  validateStaticProjectGuardianApproval: () => validateStaticProjectGuardianApproval,
@@ -936,497 +949,210 @@ var JUNIOR_CODER_ROBOT_RESCUE_PATH_CURRENT = JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_4
936
949
  var EXTERNAL_LEARNING_CONTENT_REFERENCE_VERSION_V1 = "1";
937
950
  var MISSION_AUTHORING_CONTRACT_VERSION_V1 = "1.0.0";
938
951
 
939
- // src/contextual-help.ts
940
- var CONTEXTUAL_HELP_CONTRACT_VERSION_V1 = "1.0.0";
941
- var CONTEXTUAL_HELP_KINDS_V1 = Object.freeze([
942
- "command",
943
- "visual-block",
944
- "generated-code",
945
- "assessment-diagnostic"
946
- ]);
947
- var CONTEXTUAL_VOICE_INTENTS_V1 = Object.freeze([
948
- "describe-command",
949
- "describe-inputs",
950
- "show-example",
951
- "explain-assessment-failure",
952
- "suggest-next-experiment",
953
- "repeat",
954
- "stop",
955
- "unresolved"
956
- ]);
957
- var CONTEXTUAL_VOICE_SUGGESTED_ACTIONS_V1 = Object.freeze([
958
- "open-help",
959
- "insert-example",
960
- "show-inputs",
961
- "show-assessment-evidence",
962
- "try-next-experiment",
963
- "repeat",
964
- "stop",
965
- "type-question"
966
- ]);
967
- var OPAQUE_ID = /^[a-z0-9]+(?:[._:-][a-z0-9]+)*$/u;
968
- var SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u;
969
- var SHA_256 = /^sha256:[a-f0-9]{64}$/u;
970
- var LOCALE = /^[a-z]{2,3}(?:-[A-Z]{2})?$/u;
971
- var MEDIA_TYPE = /^audio\/[a-z0-9.+-]+$/u;
972
- function containsDisallowedControlCharacter(value) {
973
- return [...value].some((character) => {
974
- const code = character.codePointAt(0) ?? 0;
975
- return code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127;
976
- });
977
- }
978
- function requireOpaqueId(value, label, maximum = 160) {
979
- if (value.length === 0 || value.length > maximum || !OPAQUE_ID.test(value)) {
980
- throw new Error(`${label} must be a bounded opaque identifier.`);
981
- }
982
- }
983
- function requireSemver(value, label) {
984
- if (!SEMVER.test(value)) {
985
- throw new Error(`${label} must be an immutable semantic version.`);
986
- }
952
+ // src/rubric-validation.ts
953
+ var DIMENSION_TOTALS = {
954
+ structure: 20,
955
+ behaviour: 50,
956
+ resilience: 20,
957
+ safety: 10
958
+ };
959
+ function rubricIssue(code, message, path, moduleId) {
960
+ return { code, message, path, ...moduleId ? { moduleId } : {} };
987
961
  }
988
- function requireIsoTimestamp(value, label) {
989
- if (!/^\d{4}-\d{2}-\d{2}T/u.test(value) || !Number.isFinite(Date.parse(value))) {
990
- throw new Error(`${label} must be an ISO-8601 timestamp.`);
962
+ function validateAssessmentRubric(rubric2, path = "assessment", moduleId) {
963
+ const issues = [];
964
+ const criterionIds = /* @__PURE__ */ new Set();
965
+ const dimensionTotals = {
966
+ structure: 0,
967
+ behaviour: 0,
968
+ resilience: 0,
969
+ safety: 0
970
+ };
971
+ let rubricTotal = 0;
972
+ for (const criterion of rubric2.criteria) {
973
+ rubricTotal += criterion.points;
974
+ dimensionTotals[criterion.dimension] += criterion.points;
975
+ if (criterionIds.has(criterion.id)) {
976
+ issues.push(
977
+ rubricIssue(
978
+ "duplicate-criterion-id",
979
+ `Duplicate assessment criterion ${criterion.id}.`,
980
+ `${path}.criteria`,
981
+ moduleId
982
+ )
983
+ );
984
+ }
985
+ criterionIds.add(criterion.id);
991
986
  }
992
- }
993
- function assertValidContextualHelpIdentifier(value) {
994
- if (value.schemaVersion !== "1") {
995
- throw new Error("schemaVersion must be 1.");
987
+ if (rubricTotal !== 100) {
988
+ issues.push(
989
+ rubricIssue(
990
+ "rubric-total",
991
+ `Assessment rubric totals ${rubricTotal}; expected 100.`,
992
+ `${path}.criteria`,
993
+ moduleId
994
+ )
995
+ );
996
996
  }
997
- if (value.contractVersion !== CONTEXTUAL_HELP_CONTRACT_VERSION_V1) {
998
- throw new Error("contractVersion is unsupported.");
997
+ for (const [dimension, expected] of Object.entries(DIMENSION_TOTALS)) {
998
+ if (dimensionTotals[dimension] !== expected) {
999
+ issues.push(
1000
+ rubricIssue(
1001
+ "rubric-dimension-total",
1002
+ `${dimension} criteria total ${dimensionTotals[dimension]}; expected ${expected}.`,
1003
+ `${path}.criteria`,
1004
+ moduleId
1005
+ )
1006
+ );
1007
+ }
999
1008
  }
1000
- if (!CONTEXTUAL_HELP_KINDS_V1.includes(value.kind)) {
1001
- throw new Error("kind is unsupported.");
1009
+ if (!rubric2.criteria.some(
1010
+ (criterion) => criterion.dimension === "safety" && criterion.mandatory
1011
+ )) {
1012
+ issues.push(
1013
+ rubricIssue(
1014
+ "missing-mandatory-safety",
1015
+ "Every module requires a mandatory safety criterion.",
1016
+ `${path}.criteria`,
1017
+ moduleId
1018
+ )
1019
+ );
1002
1020
  }
1003
- requireOpaqueId(value.moduleId, "moduleId");
1004
- requireSemver(value.moduleVersion, "moduleVersion");
1005
- requireSemver(value.manifestVersion, "manifestVersion");
1006
- requireOpaqueId(value.helpId, "helpId");
1021
+ return issues;
1007
1022
  }
1008
- function assertValidGuardianVoiceConsent(value) {
1009
- if (value.schemaVersion !== "1") {
1010
- throw new Error("schemaVersion must be 1.");
1011
- }
1012
- requireOpaqueId(value.actorAccountId, "actorAccountId");
1013
- requireOpaqueId(value.subjectAccountId, "subjectAccountId");
1014
- requireSemver(value.policyVersion, "policyVersion");
1015
- if (value.state !== "granted" && value.state !== "withdrawn") {
1016
- throw new Error("state must be granted or withdrawn.");
1017
- }
1018
- if (value.permittedProcessingRoute !== "private-edge-only") {
1019
- throw new Error("permittedProcessingRoute must be private-edge-only.");
1020
- }
1021
- requireIsoTimestamp(value.recordedAt, "recordedAt");
1023
+
1024
+ // src/mission-authoring.ts
1025
+ var JUNIOR_CODER_MISSION_STAGE_ORDER_V1 = [
1026
+ "learn",
1027
+ "predict",
1028
+ "build",
1029
+ "run",
1030
+ "assess",
1031
+ "inspect",
1032
+ "fix",
1033
+ "explain",
1034
+ "reward"
1035
+ ];
1036
+ var LEARNER_STARTER_KINDS = /* @__PURE__ */ new Set([
1037
+ "starter-code",
1038
+ "starter-assets",
1039
+ "sample-data"
1040
+ ]);
1041
+ var LEARNER_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
1042
+ "facilitator-note",
1043
+ "answer-key",
1044
+ "protected-test"
1045
+ ]);
1046
+ var SINGLE_MODE_REQUIRES_ALTERNATIVE = /* @__PURE__ */ new Set([
1047
+ "pointer",
1048
+ "drag",
1049
+ "audio",
1050
+ "colour",
1051
+ "motion"
1052
+ ]);
1053
+ function authoringIssue(code, message, path) {
1054
+ return { code, message, path };
1022
1055
  }
1023
- function assertValidContextualVoiceQuestionMetadata(value) {
1024
- assertValidContextualHelpIdentifier(value.help);
1025
- if (!LOCALE.test(value.locale)) throw new Error("locale is invalid.");
1026
- if (!MEDIA_TYPE.test(value.mediaType)) throw new Error("mediaType is invalid.");
1027
- if (!Number.isInteger(value.durationMs) || value.durationMs < 1 || value.durationMs > 1e4) {
1028
- throw new Error("durationMs must be between 1 and 10000.");
1029
- }
1030
- if (!Number.isInteger(value.audioBytes) || value.audioBytes < 1 || value.audioBytes > 2097152) {
1031
- throw new Error("audioBytes must be between 1 and 2097152.");
1032
- }
1033
- if (value.assessmentEvidenceId !== void 0) {
1034
- requireOpaqueId(value.assessmentEvidenceId, "assessmentEvidenceId");
1056
+ function reportDuplicateIds(ids, path) {
1057
+ const seen = /* @__PURE__ */ new Set();
1058
+ const issues = [];
1059
+ for (const id2 of ids) {
1060
+ if (seen.has(id2)) {
1061
+ issues.push(
1062
+ authoringIssue("duplicate-id", `Duplicate authored ID ${id2}.`, path)
1063
+ );
1064
+ }
1065
+ seen.add(id2);
1035
1066
  }
1067
+ return issues;
1036
1068
  }
1037
- function assertValidVoiceHelpAvailability(value) {
1038
- if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
1039
- if (value.questionProcessingRoute !== "private-edge-only") {
1040
- throw new Error("questionProcessingRoute must be private-edge-only.");
1041
- }
1042
- if (value.guardianVoiceConsentRequired !== true) {
1043
- throw new Error("guardianVoiceConsentRequired must remain true.");
1044
- }
1045
- if (value.maximumRecordingDurationMs !== 1e4 || value.maximumAudioBytes !== 2097152 || value.rawAudioRetention !== "request-memory-only" || value.transcriptRetention !== "request-memory-only") {
1046
- throw new Error("Voice limits and transient retention are immutable in version one.");
1069
+ function validateMissionAuthoringBundle(bundle, module3) {
1070
+ const issues = [];
1071
+ if (bundle.version !== MISSION_AUTHORING_CONTRACT_VERSION_V1) {
1072
+ issues.push(
1073
+ authoringIssue(
1074
+ "bundle-version-mismatch",
1075
+ `Unsupported mission authoring version ${bundle.version}.`,
1076
+ "version"
1077
+ )
1078
+ );
1047
1079
  }
1048
- if (value.microphoneAvailable !== (value.microphoneReason === "available")) {
1049
- throw new Error("microphoneReason must match microphone availability.");
1080
+ if (bundle.moduleId !== module3.id || bundle.moduleVersion !== module3.version) {
1081
+ issues.push(
1082
+ authoringIssue(
1083
+ "module-reference-mismatch",
1084
+ `Bundle ${bundle.moduleId}@${bundle.moduleVersion} does not match ${module3.id}@${module3.version}.`,
1085
+ "moduleId"
1086
+ )
1087
+ );
1050
1088
  }
1051
- }
1052
- function assertValidCanonicalSpokenHelpDescriptor(value) {
1053
- if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
1054
- assertValidContextualHelpIdentifier(value.help);
1055
- requireOpaqueId(value.canonicalTextId, "canonicalTextId");
1056
- if (!SHA_256.test(value.authoritativeTextDigest)) {
1057
- throw new Error("authoritativeTextDigest must be a SHA-256 digest.");
1089
+ if (!module3.missions.some((mission2) => mission2.id === bundle.missionId)) {
1090
+ issues.push(
1091
+ authoringIssue(
1092
+ "mission-reference-mismatch",
1093
+ `Mission ${bundle.missionId} does not exist in module ${module3.id}.`,
1094
+ "missionId"
1095
+ )
1096
+ );
1058
1097
  }
1059
- if (!LOCALE.test(value.locale)) throw new Error("locale is invalid.");
1060
- requireOpaqueId(value.voiceProfile, "voiceProfile");
1061
- requireSemver(value.pronunciationVersion, "pronunciationVersion");
1062
- if (value.utteranceClass !== "system-generic" || value.sharingScope !== "global" || value.reuse !== "exact-only" || value.containsPersonalData !== false || value.containsLearnerContent !== false) {
1063
- throw new Error("Canonical spoken help must use global system-generic exact-only reuse.");
1098
+ const learner = bundle.learner;
1099
+ const facilitator = bundle.facilitator;
1100
+ if (learner.estimatedMinutes < 15 || learner.estimatedMinutes > 25) {
1101
+ issues.push(
1102
+ authoringIssue(
1103
+ "invalid-duration",
1104
+ "A mission must last between 15 and 25 minutes.",
1105
+ "learner.estimatedMinutes"
1106
+ )
1107
+ );
1064
1108
  }
1065
- }
1066
- function assertValidContextualVoiceQuestionResult(value) {
1067
- if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
1068
- assertValidContextualHelpIdentifier(value.help);
1069
- if (!CONTEXTUAL_VOICE_INTENTS_V1.includes(value.intent)) {
1070
- throw new Error("intent is unsupported.");
1071
- }
1072
- if (value.transcriptRetention !== "request-memory-only") {
1073
- throw new Error("transcriptRetention must be request-memory-only.");
1074
- }
1075
- if (value.status !== "resolved" && value.status !== "suggestions" && value.status !== "stopped") {
1076
- throw new Error("status is unsupported.");
1077
- }
1078
- if (value.transientTranscript.length > 500 || containsDisallowedControlCharacter(value.transientTranscript)) {
1079
- throw new Error("transientTranscript is invalid.");
1080
- }
1081
- if (value.answer) {
1082
- requireOpaqueId(value.answer.helpId, "answer.helpId");
1083
- if (value.answer.source !== "module-documentation" && value.answer.source !== "assessment-evidence" && value.answer.source !== "authored-fallback" || value.answer.text.length === 0 || value.answer.text.length > 800 || containsDisallowedControlCharacter(value.answer.text)) {
1084
- throw new Error("answer.text is invalid.");
1085
- }
1086
- }
1087
- if (value.suggestedActions.some(
1088
- (action) => !CONTEXTUAL_VOICE_SUGGESTED_ACTIONS_V1.includes(action)
1089
- )) {
1090
- throw new Error("suggestedActions contains an unsupported action.");
1091
- }
1092
- if (value.mayAssignScore !== false || value.mayAwardReward !== false || value.mayPublish !== false || value.mayControlHardware !== false) {
1093
- throw new Error("mayAssignScore, reward, publish and hardware authority must remain false.");
1094
- }
1095
- }
1096
-
1097
- // src/publishing.ts
1098
- var STATIC_PROJECT_PUBLISHING_CONTRACT_VERSION_V1 = "1.0.0";
1099
- var STATIC_PROJECT_SCANNER_VERSION_V1 = "junior-coder-static-scan-v1";
1100
- var STATIC_PROJECT_APPROVAL_STATEMENT_VERSION_V1 = "guardian-static-publication-v1";
1101
- var STATIC_PROJECT_DEFAULT_LIFETIME_DAYS_V1 = 90;
1102
- var STATIC_PROJECT_MAX_SOURCE_CHARACTERS_V1 = 8e3;
1103
- var STATIC_PROJECT_SAFETY_CHECK_IDS_V1 = [
1104
- "allow-listed-source",
1105
- "no-personal-details",
1106
- "no-external-network",
1107
- "no-executable-markup",
1108
- "no-transmitting-forms",
1109
- "no-uploads-or-embeds",
1110
- "no-trackers-or-advertising",
1111
- "no-account-identifiers"
1112
- ];
1113
- var DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
1114
- var IDENTIFIER_PATTERN = /^(?:account|approval|publication|snapshot)_[A-Za-z0-9_-]{20,128}$/u;
1115
- var VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u;
1116
- var RANDOM_SLUG_PATTERN = /^[A-Za-z0-9_-]{40,128}$/u;
1117
- function validDate(value) {
1118
- return typeof value === "string" && Number.isFinite(Date.parse(value));
1119
- }
1120
- function allSafetyChecksPresent(scan) {
1121
- if (!Array.isArray(scan.checks) || scan.checks.length !== STATIC_PROJECT_SAFETY_CHECK_IDS_V1.length) {
1122
- return false;
1123
- }
1124
- const supplied = new Set(scan.checks.map((check) => check.id));
1125
- return STATIC_PROJECT_SAFETY_CHECK_IDS_V1.every((id) => supplied.has(id)) && scan.checks.every((check) => check.passed === true);
1126
- }
1127
- function safeAdventureRender(value) {
1128
- const headings = ["Moonbase Missions", "Forest Rescue Plans", "Ocean Quest Board"];
1129
- const titles = ["Find the moon crystal", "Guide the lost firefly", "Map the coral garden"];
1130
- const days = ["Saturday", "Sunday", "School holiday"];
1131
- const messages = ["Choose a day", "Add a mission title", "Mission ready!"];
1132
- return headings.includes(value.heading) && Array.isArray(value.missions) && value.missions.length >= 1 && value.missions.length <= 3 && value.missions.every((mission2) => titles.includes(mission2.title) && days.includes(mission2.day)) && messages.includes(value.validationMessage) && value.savedAcrossRestart === true && value.accessibleSummaryEnabled === true;
1133
- }
1134
- function safeCreatureRender(value) {
1135
- return ["Moon Moth", "Cloud Cat", "Pebble Dragon"].includes(value.creature) && ["Resting", "Ready to play", "Snack time"].includes(value.status) && [5, 10, 15].includes(value.timerSeconds) && ["single", "cosy-grid", "wide-grid"].includes(value.layout) && value.reducedMotion === true;
1136
- }
1137
- function safeRobotRender(value) {
1138
- return ["scan", "hold-position", "return-to-base"].includes(value.command) && value.safetyConfirmed === true && [1, 2, 4].includes(value.telemetryRate) && ["line", "bars", "text-only"].includes(value.chartMode) && value.serialSimulation === true && ["SCANNING", "HOLDING", "RETURNING"].includes(value.state);
1139
- }
1140
- function validateStaticProjectSnapshot(value) {
1141
- const issues = [];
1142
- if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
1143
- if (value.contractVersion !== STATIC_PROJECT_PUBLISHING_CONTRACT_VERSION_V1) {
1144
- issues.push("invalid-contract-version");
1145
- }
1146
- if (!IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.subjectAccountId)) {
1147
- issues.push("invalid-identifier");
1148
- }
1149
- if (!value.moduleId.startsWith("junior-coder.") || value.moduleId !== `junior-coder.${value.moduleSlug}`) {
1150
- issues.push("invalid-module");
1151
- }
1152
- if (!VERSION_PATTERN.test(value.moduleVersion)) issues.push("invalid-version");
1153
- if (!DIGEST_PATTERN.test(value.sourceDigest)) issues.push("invalid-source-digest");
1154
- if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
1155
- if (!Number.isInteger(value.sourceCharacterCount) || value.sourceCharacterCount < 1 || value.sourceCharacterCount > STATIC_PROJECT_MAX_SOURCE_CHARACTERS_V1) {
1156
- issues.push("invalid-source-size");
1157
- }
1158
- if (!Number.isInteger(value.assessmentScore) || value.assessmentScore < 80 || value.assessmentScore > 100) {
1159
- issues.push("assessment-score-incomplete");
1160
- }
1161
- if (value.mandatorySafetyPassed !== true) issues.push("mandatory-safety-missing");
1162
- if (value.scan?.passed !== true || value.scan?.scannerVersion !== STATIC_PROJECT_SCANNER_VERSION_V1) {
1163
- issues.push("scan-not-passed");
1164
- }
1165
- if (!value.scan || !allSafetyChecksPresent(value.scan)) issues.push("scan-checks-incomplete");
1166
- if (value.moduleSlug !== value.renderModel?.kind) issues.push("module-render-model-mismatch");
1167
- const renderSafe = value.renderModel?.kind === "adventure-mission-planner" ? safeAdventureRender(value.renderModel) : value.renderModel?.kind === "creature-care-dashboard" ? safeCreatureRender(value.renderModel) : value.renderModel?.kind === "robot-mission-control" ? safeRobotRender(value.renderModel) : false;
1168
- if (!renderSafe) issues.push("unsafe-render-model");
1169
- if (value.state !== "pending-review" || !validDate(value.createdAt)) issues.push("invalid-created-at");
1170
- return [...new Set(issues)];
1171
- }
1172
- function assertValidStaticProjectSnapshot(value) {
1173
- const issues = validateStaticProjectSnapshot(value);
1174
- if (issues.length > 0) throw new Error(`Invalid static project snapshot: ${issues.join(", ")}`);
1175
- }
1176
- function validateStaticProjectGuardianApproval(value) {
1177
- const issues = [];
1178
- if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
1179
- if (!IDENTIFIER_PATTERN.test(value.approvalId) || !IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.guardianActorAccountId)) {
1180
- issues.push("invalid-identifier");
1181
- }
1182
- if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
1183
- if (value.statementVersion !== STATIC_PROJECT_APPROVAL_STATEMENT_VERSION_V1) {
1184
- issues.push("invalid-approval-statement");
1185
- }
1186
- if (!validDate(value.approvedAt)) issues.push("invalid-approved-at");
1187
- return [...new Set(issues)];
1188
- }
1189
- function assertValidStaticProjectGuardianApproval(value) {
1190
- const issues = validateStaticProjectGuardianApproval(value);
1191
- if (issues.length > 0) throw new Error(`Invalid static project approval: ${issues.join(", ")}`);
1192
- }
1193
- function validateStaticProjectPublication(value) {
1194
- const issues = [];
1195
- if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
1196
- if (!IDENTIFIER_PATTERN.test(value.publicationId) || !IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.approvalId)) {
1197
- issues.push("invalid-identifier");
1198
- }
1199
- if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
1200
- if (!RANDOM_SLUG_PATTERN.test(value.randomSlug)) issues.push("invalid-random-slug");
1201
- let parsedUrl;
1202
- try {
1203
- parsedUrl = new URL(value.publicUrl);
1204
- } catch {
1205
- parsedUrl = void 0;
1206
- }
1207
- if (!parsedUrl || parsedUrl.protocol !== "https:" || parsedUrl.username || parsedUrl.password) {
1208
- issues.push("invalid-public-url");
1209
- }
1210
- if (!parsedUrl?.pathname.endsWith(`/${value.randomSlug}`)) issues.push("public-url-slug-mismatch");
1211
- if (value.state !== "published") issues.push("published-state-required");
1212
- if (!validDate(value.publishedAt)) issues.push("invalid-published-at");
1213
- if (!validDate(value.expiresAt) || validDate(value.publishedAt) && Date.parse(value.expiresAt) <= Date.parse(value.publishedAt)) {
1214
- issues.push("invalid-publication-expiry");
1215
- }
1216
- if (value.renewedAt !== void 0 && !validDate(value.renewedAt) || value.unpublishedAt !== void 0 && !validDate(value.unpublishedAt)) {
1217
- issues.push("invalid-lifecycle-timestamp");
1218
- }
1219
- return [...new Set(issues)];
1220
- }
1221
- function assertValidStaticProjectPublication(value) {
1222
- const issues = validateStaticProjectPublication(value);
1223
- if (issues.length > 0) throw new Error(`Invalid static project publication: ${issues.join(", ")}`);
1224
- }
1225
-
1226
- // src/rubric-validation.ts
1227
- var DIMENSION_TOTALS = {
1228
- structure: 20,
1229
- behaviour: 50,
1230
- resilience: 20,
1231
- safety: 10
1232
- };
1233
- function rubricIssue(code, message, path, moduleId) {
1234
- return { code, message, path, ...moduleId ? { moduleId } : {} };
1235
- }
1236
- function validateAssessmentRubric(rubric2, path = "assessment", moduleId) {
1237
- const issues = [];
1238
- const criterionIds = /* @__PURE__ */ new Set();
1239
- const dimensionTotals = {
1240
- structure: 0,
1241
- behaviour: 0,
1242
- resilience: 0,
1243
- safety: 0
1244
- };
1245
- let rubricTotal = 0;
1246
- for (const criterion of rubric2.criteria) {
1247
- rubricTotal += criterion.points;
1248
- dimensionTotals[criterion.dimension] += criterion.points;
1249
- if (criterionIds.has(criterion.id)) {
1109
+ const stageKinds = learner.stages.map((stage) => stage.kind);
1110
+ for (const requiredStage of JUNIOR_CODER_MISSION_STAGE_ORDER_V1) {
1111
+ const count = stageKinds.filter((stage) => stage === requiredStage).length;
1112
+ if (count === 0) {
1250
1113
  issues.push(
1251
- rubricIssue(
1252
- "duplicate-criterion-id",
1253
- `Duplicate assessment criterion ${criterion.id}.`,
1254
- `${path}.criteria`,
1255
- moduleId
1114
+ authoringIssue(
1115
+ "missing-stage",
1116
+ `Mission stage ${requiredStage} is required.`,
1117
+ "learner.stages"
1118
+ )
1119
+ );
1120
+ } else if (count > 1) {
1121
+ issues.push(
1122
+ authoringIssue(
1123
+ "duplicate-stage",
1124
+ `Mission stage ${requiredStage} appears more than once.`,
1125
+ "learner.stages"
1256
1126
  )
1257
1127
  );
1258
1128
  }
1259
- criterionIds.add(criterion.id);
1260
1129
  }
1261
- if (rubricTotal !== 100) {
1130
+ if (stageKinds.length === JUNIOR_CODER_MISSION_STAGE_ORDER_V1.length && stageKinds.some(
1131
+ (stage, index) => stage !== JUNIOR_CODER_MISSION_STAGE_ORDER_V1[index]
1132
+ )) {
1262
1133
  issues.push(
1263
- rubricIssue(
1264
- "rubric-total",
1265
- `Assessment rubric totals ${rubricTotal}; expected 100.`,
1266
- `${path}.criteria`,
1267
- moduleId
1134
+ authoringIssue(
1135
+ "stage-order",
1136
+ "Mission stages must follow the canonical learner journey.",
1137
+ "learner.stages"
1268
1138
  )
1269
1139
  );
1270
1140
  }
1271
- for (const [dimension, expected] of Object.entries(DIMENSION_TOTALS)) {
1272
- if (dimensionTotals[dimension] !== expected) {
1273
- issues.push(
1274
- rubricIssue(
1275
- "rubric-dimension-total",
1276
- `${dimension} criteria total ${dimensionTotals[dimension]}; expected ${expected}.`,
1277
- `${path}.criteria`,
1278
- moduleId
1279
- )
1280
- );
1281
- }
1141
+ if (learner.readinessChecks.length === 0) {
1142
+ issues.push(
1143
+ authoringIssue(
1144
+ "missing-readiness-check",
1145
+ "At least one unscored readiness check is required.",
1146
+ "learner.readinessChecks"
1147
+ )
1148
+ );
1282
1149
  }
1283
- if (!rubric2.criteria.some(
1284
- (criterion) => criterion.dimension === "safety" && criterion.mandatory
1285
- )) {
1150
+ if (learner.readinessChecks.some((check) => check.scored !== false)) {
1286
1151
  issues.push(
1287
- rubricIssue(
1288
- "missing-mandatory-safety",
1289
- "Every module requires a mandatory safety criterion.",
1290
- `${path}.criteria`,
1291
- moduleId
1292
- )
1293
- );
1294
- }
1295
- return issues;
1296
- }
1297
-
1298
- // src/mission-authoring.ts
1299
- var JUNIOR_CODER_MISSION_STAGE_ORDER_V1 = [
1300
- "learn",
1301
- "predict",
1302
- "build",
1303
- "run",
1304
- "assess",
1305
- "inspect",
1306
- "fix",
1307
- "explain",
1308
- "reward"
1309
- ];
1310
- var LEARNER_STARTER_KINDS = /* @__PURE__ */ new Set([
1311
- "starter-code",
1312
- "starter-assets",
1313
- "sample-data"
1314
- ]);
1315
- var LEARNER_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
1316
- "facilitator-note",
1317
- "answer-key",
1318
- "protected-test"
1319
- ]);
1320
- var SINGLE_MODE_REQUIRES_ALTERNATIVE = /* @__PURE__ */ new Set([
1321
- "pointer",
1322
- "drag",
1323
- "audio",
1324
- "colour",
1325
- "motion"
1326
- ]);
1327
- function authoringIssue(code, message, path) {
1328
- return { code, message, path };
1329
- }
1330
- function reportDuplicateIds(ids, path) {
1331
- const seen = /* @__PURE__ */ new Set();
1332
- const issues = [];
1333
- for (const id of ids) {
1334
- if (seen.has(id)) {
1335
- issues.push(
1336
- authoringIssue("duplicate-id", `Duplicate authored ID ${id}.`, path)
1337
- );
1338
- }
1339
- seen.add(id);
1340
- }
1341
- return issues;
1342
- }
1343
- function validateMissionAuthoringBundle(bundle, module3) {
1344
- const issues = [];
1345
- if (bundle.version !== MISSION_AUTHORING_CONTRACT_VERSION_V1) {
1346
- issues.push(
1347
- authoringIssue(
1348
- "bundle-version-mismatch",
1349
- `Unsupported mission authoring version ${bundle.version}.`,
1350
- "version"
1351
- )
1352
- );
1353
- }
1354
- if (bundle.moduleId !== module3.id || bundle.moduleVersion !== module3.version) {
1355
- issues.push(
1356
- authoringIssue(
1357
- "module-reference-mismatch",
1358
- `Bundle ${bundle.moduleId}@${bundle.moduleVersion} does not match ${module3.id}@${module3.version}.`,
1359
- "moduleId"
1360
- )
1361
- );
1362
- }
1363
- if (!module3.missions.some((mission2) => mission2.id === bundle.missionId)) {
1364
- issues.push(
1365
- authoringIssue(
1366
- "mission-reference-mismatch",
1367
- `Mission ${bundle.missionId} does not exist in module ${module3.id}.`,
1368
- "missionId"
1369
- )
1370
- );
1371
- }
1372
- const learner = bundle.learner;
1373
- const facilitator = bundle.facilitator;
1374
- if (learner.estimatedMinutes < 15 || learner.estimatedMinutes > 25) {
1375
- issues.push(
1376
- authoringIssue(
1377
- "invalid-duration",
1378
- "A mission must last between 15 and 25 minutes.",
1379
- "learner.estimatedMinutes"
1380
- )
1381
- );
1382
- }
1383
- const stageKinds = learner.stages.map((stage) => stage.kind);
1384
- for (const requiredStage of JUNIOR_CODER_MISSION_STAGE_ORDER_V1) {
1385
- const count = stageKinds.filter((stage) => stage === requiredStage).length;
1386
- if (count === 0) {
1387
- issues.push(
1388
- authoringIssue(
1389
- "missing-stage",
1390
- `Mission stage ${requiredStage} is required.`,
1391
- "learner.stages"
1392
- )
1393
- );
1394
- } else if (count > 1) {
1395
- issues.push(
1396
- authoringIssue(
1397
- "duplicate-stage",
1398
- `Mission stage ${requiredStage} appears more than once.`,
1399
- "learner.stages"
1400
- )
1401
- );
1402
- }
1403
- }
1404
- if (stageKinds.length === JUNIOR_CODER_MISSION_STAGE_ORDER_V1.length && stageKinds.some(
1405
- (stage, index) => stage !== JUNIOR_CODER_MISSION_STAGE_ORDER_V1[index]
1406
- )) {
1407
- issues.push(
1408
- authoringIssue(
1409
- "stage-order",
1410
- "Mission stages must follow the canonical learner journey.",
1411
- "learner.stages"
1412
- )
1413
- );
1414
- }
1415
- if (learner.readinessChecks.length === 0) {
1416
- issues.push(
1417
- authoringIssue(
1418
- "missing-readiness-check",
1419
- "At least one unscored readiness check is required.",
1420
- "learner.readinessChecks"
1421
- )
1422
- );
1423
- }
1424
- if (learner.readinessChecks.some((check) => check.scored !== false)) {
1425
- issues.push(
1426
- authoringIssue(
1427
- "scored-readiness-check",
1428
- "Readiness checks must not affect the deterministic score.",
1429
- "learner.readinessChecks"
1152
+ authoringIssue(
1153
+ "scored-readiness-check",
1154
+ "Readiness checks must not affect the deterministic score.",
1155
+ "learner.readinessChecks"
1430
1156
  )
1431
1157
  );
1432
1158
  }
@@ -7123,6 +6849,526 @@ var ROAD_HOPPER_RALLY_MISSION_ONE_AUTHORING_V1 = {
7123
6849
  }
7124
6850
  };
7125
6851
 
6852
+ // src/course-contracts.ts
6853
+ var LEARNING_COURSE_STAGE_ORDER = JUNIOR_CODER_MISSION_STAGE_ORDER_V1;
6854
+ var LEARNING_COURSE_LIMITS = Object.freeze({
6855
+ missions: 6,
6856
+ stagesPerMission: 9,
6857
+ projectFiles: 8,
6858
+ sourceCharactersPerFile: 64e3,
6859
+ sourceCharactersPerProject: 96e3
6860
+ });
6861
+ var ID = /^[a-z0-9][a-z0-9.-]{0,159}$/u;
6862
+ var FILE_PATH = /^[a-z0-9][a-z0-9_-]{0,63}\.(?:js|py|cpp|html|css|json)$/u;
6863
+ var VERSION = /^\d+\.\d+\.\d+$/u;
6864
+ var LANGUAGES = ["javascript", "python", "cpp", "html", "css", "blocks", "json"];
6865
+ var CATEGORIES = ["game", "robot", "vibe", "web-app"];
6866
+ var PLACEHOLDER = /\b(?:TODO|TBD|coming soon|placeholder|lorem ipsum)\b/iu;
6867
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6868
+ var text = (value, minimum = 1, maximum = 8e3) => typeof value === "string" && value.trim().length >= minimum && value.length <= maximum;
6869
+ var id = (value) => typeof value === "string" && ID.test(value);
6870
+ var integer = (value, minimum, maximum) => typeof value === "number" && Number.isSafeInteger(value) && value >= minimum && value <= maximum;
6871
+ var exactKeys = (value, keys2) => Object.keys(value).length === keys2.length && keys2.every((key) => Object.hasOwn(value, key));
6872
+ var LearningCourseInputError = class extends Error {
6873
+ constructor() {
6874
+ super("Invalid learning course input.");
6875
+ this.name = "LearningCourseInputError";
6876
+ }
6877
+ };
6878
+ function fileDefinitions(value) {
6879
+ 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;
6880
+ }
6881
+ function parseLearningProject(course, value) {
6882
+ if (!fileDefinitions(course.projectFiles) || !record(value) || !exactKeys(value, ["files"]) || !Array.isArray(value.files) || value.files.length !== course.projectFiles.length) throw new LearningCourseInputError();
6883
+ const files = [];
6884
+ const seen = /* @__PURE__ */ new Set();
6885
+ let characters = 0;
6886
+ for (const file of value.files) {
6887
+ if (!record(file) || !exactKeys(file, ["path", "source"]) || typeof file.path !== "string" || typeof file.source !== "string" || seen.has(file.path)) throw new LearningCourseInputError();
6888
+ const definition = course.projectFiles.find((candidate) => candidate.path === file.path);
6889
+ if (!definition || file.source.length > definition.maximumCharacters || file.source.includes("\0")) throw new LearningCourseInputError();
6890
+ characters += file.source.length;
6891
+ if (characters > LEARNING_COURSE_LIMITS.sourceCharactersPerProject) throw new LearningCourseInputError();
6892
+ seen.add(file.path);
6893
+ files.push({ path: file.path, source: file.source });
6894
+ }
6895
+ return { files: course.projectFiles.map((definition) => files.find((file) => file.path === definition.path)) };
6896
+ }
6897
+ function parseLearningCourseDraft(course, value) {
6898
+ if (!record(value) || !exactKeys(value, ["schemaVersion", "moduleVersion", "activeStageId", "project"]) || value.schemaVersion !== "1" || value.moduleVersion !== course.moduleVersion || typeof value.activeStageId !== "string" || !course.missions.some((mission2) => mission2.stages.some((stage) => stage.id === value.activeStageId))) throw new LearningCourseInputError();
6899
+ return {
6900
+ schemaVersion: "1",
6901
+ moduleVersion: course.moduleVersion,
6902
+ activeStageId: value.activeStageId,
6903
+ project: parseLearningProject(course, value.project)
6904
+ };
6905
+ }
6906
+ function parseLearningSaveSlotId(value) {
6907
+ if (value === "auto" || typeof value === "string" && /^[1-9]$/u.test(value)) return value;
6908
+ throw new LearningCourseInputError();
6909
+ }
6910
+ function validateLearningCourse(value) {
6911
+ const issues = [];
6912
+ const add = (code, path) => {
6913
+ issues.push({ code, path });
6914
+ };
6915
+ if (!record(value)) return [{ code: "invalid-manifest", path: "$" }];
6916
+ if (!exactKeys(value, [
6917
+ "schemaVersion",
6918
+ "moduleId",
6919
+ "moduleVersion",
6920
+ "slug",
6921
+ "title",
6922
+ "summary",
6923
+ "runtimeId",
6924
+ "category",
6925
+ "estimatedMinutes",
6926
+ "completionAssessmentId",
6927
+ "projectFiles",
6928
+ "starterProject",
6929
+ "reference",
6930
+ "missions",
6931
+ "completionBadge"
6932
+ ]) || 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", "$");
6933
+ if (!fileDefinitions(value.projectFiles)) add("invalid-project", "projectFiles");
6934
+ else {
6935
+ try {
6936
+ parseLearningProject({ projectFiles: value.projectFiles }, value.starterProject);
6937
+ } catch {
6938
+ add("invalid-project", "starterProject");
6939
+ }
6940
+ }
6941
+ 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");
6942
+ if (!Array.isArray(value.missions)) {
6943
+ add("mission-count", "missions");
6944
+ return issues;
6945
+ }
6946
+ if (value.missions.length !== LEARNING_COURSE_LIMITS.missions) add("mission-count", "missions");
6947
+ if (value.missions.length > LEARNING_COURSE_LIMITS.missions) return issues;
6948
+ const seen = /* @__PURE__ */ new Set();
6949
+ const checkId = (candidate, path) => {
6950
+ if (!id(candidate)) add("invalid-manifest", path);
6951
+ else if (seen.has(candidate)) add("duplicate-id", path);
6952
+ else seen.add(candidate);
6953
+ };
6954
+ let minutes = 0;
6955
+ value.missions.forEach((mission2, missionIndex) => {
6956
+ const path = `missions[${missionIndex}]`;
6957
+ if (!record(mission2)) {
6958
+ add("invalid-manifest", path);
6959
+ return;
6960
+ }
6961
+ checkId(mission2.id, `${path}.id`);
6962
+ checkId(mission2.assessmentId, `${path}.assessmentId`);
6963
+ if (!exactKeys(mission2, ["id", "title", "concepts", "estimatedMinutes", "goals", "assessmentId", "stages", "extension"]) || !text(mission2.title, 3, 160) || !integer(mission2.estimatedMinutes, 10, 600) || !Array.isArray(mission2.concepts) || mission2.concepts.length < 1 || mission2.concepts.length > 12 || mission2.concepts.some((concept) => !text(concept, 2, 120)) || !Array.isArray(mission2.goals) || mission2.goals.length < 1 || mission2.goals.length > 12 || mission2.goals.some((goal) => !text(goal, 20, 2e3)) || !text(mission2.extension, 30, 2e3)) add("incomplete-content", path);
6964
+ if (typeof mission2.estimatedMinutes === "number") minutes += mission2.estimatedMinutes;
6965
+ if (!Array.isArray(mission2.stages) || mission2.stages.length !== LEARNING_COURSE_LIMITS.stagesPerMission) {
6966
+ add("stage-order", `${path}.stages`);
6967
+ return;
6968
+ }
6969
+ mission2.stages.forEach((stage, stageIndex) => {
6970
+ const stagePath = `${path}.stages[${stageIndex}]`;
6971
+ if (!record(stage)) {
6972
+ add("invalid-manifest", stagePath);
6973
+ return;
6974
+ }
6975
+ checkId(stage.id, `${stagePath}.id`);
6976
+ if (stage.kind !== LEARNING_COURSE_STAGE_ORDER[stageIndex]) add("stage-order", stagePath);
6977
+ 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);
6978
+ });
6979
+ });
6980
+ if (minutes !== value.estimatedMinutes) add("invalid-manifest", "estimatedMinutes");
6981
+ return issues;
6982
+ }
6983
+ function parseLearningCourse(value) {
6984
+ if (validateLearningCourse(value).length) throw new LearningCourseInputError();
6985
+ return structuredClone(value);
6986
+ }
6987
+
6988
+ // src/course-progress.ts
6989
+ var DIGEST = /^[a-f0-9]{64}$/u;
6990
+ var REFERENCE = /^[a-z0-9][a-z0-9.-]{0,159}$/u;
6991
+ var stages = (course) => course.missions.flatMap((mission2) => mission2.stages);
6992
+ var record2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6993
+ var keys = (value, expected) => Object.keys(value).length === expected.length && expected.every((key) => Object.hasOwn(value, key));
6994
+ var identifier = (value) => typeof value === "string" && REFERENCE.test(value);
6995
+ var digest = (value) => typeof value === "string" && DIGEST.test(value);
6996
+ function validResult(value) {
6997
+ if (!record2(value) || !keys(value, ["score", "band", "completed", "passedCriterionIds", "failedCriterionIds", "failedMandatoryCriterionIds"]) || typeof value.score !== "number" || !Number.isInteger(value.score) || value.score < 0 || value.score > 100) return false;
6998
+ const arrays = [value.passedCriterionIds, value.failedCriterionIds, value.failedMandatoryCriterionIds];
6999
+ if (arrays.some((items) => !Array.isArray(items) || items.length > 64 || !items.every(identifier) || new Set(items).size !== items.length)) return false;
7000
+ const passed = value.passedCriterionIds;
7001
+ const failed = value.failedCriterionIds;
7002
+ const mandatory = value.failedMandatoryCriterionIds;
7003
+ const band = value.score >= 95 ? "mastered" : value.score >= 80 ? "mission-complete" : value.score >= 60 ? "nearly-there" : "keep-exploring";
7004
+ return value.band === band && value.completed === (value.score >= 80 && mandatory.length === 0) && !passed.some((id2) => failed.includes(id2)) && mandatory.every((id2) => failed.includes(id2));
7005
+ }
7006
+ function assertProgress(course, progress) {
7007
+ const ordered = stages(course);
7008
+ if (progress.schemaVersion !== "1" || progress.moduleId !== course.moduleId || progress.moduleVersion !== course.moduleVersion || !Array.isArray(progress.completedStageIds) || progress.completedStageIds.length > ordered.length || progress.completedStageIds.some((id2, index) => id2 !== ordered[index].id) || !Array.isArray(progress.completedMissionIds) || progress.completedMissionIds.length !== Math.floor(progress.completedStageIds.length / 9) || progress.completedMissionIds.some((id2, index) => id2 !== course.missions[index].id) || !Array.isArray(progress.assessments) || progress.assessments.length > 7 || progress.assessments.some((item) => {
7009
+ if (!record2(item) || !keys(item, ["missionId", "assessmentId", "referenceId", "sourceDigest", "result"]) || !identifier(item.referenceId) || !digest(item.sourceDigest) || !validResult(item.result)) return true;
7010
+ const mission2 = course.missions.find((candidate) => candidate.id === item.missionId);
7011
+ return item.missionId === "final" ? progress.completedMissionIds.length !== 6 || item.assessmentId !== course.completionAssessmentId : !mission2 || item.assessmentId !== mission2.assessmentId;
7012
+ }) || new Set(progress.assessments.map((item) => item.missionId)).size !== progress.assessments.length || new Set(progress.assessments.map((item) => item.referenceId)).size !== progress.assessments.length) throw new LearningCourseInputError();
7013
+ if (progress.completion !== null && (!record2(progress.completion) || !keys(progress.completion, ["referenceId", "sourceDigest", "bestScore"]) || progress.completedMissionIds.length !== 6 || !identifier(progress.completion.referenceId) || !digest(progress.completion.sourceDigest) || !Number.isInteger(progress.completion.bestScore) || progress.completion.bestScore < 80 || progress.completion.bestScore > 100)) throw new LearningCourseInputError();
7014
+ }
7015
+ function parseLearningCourseProgress(course, value) {
7016
+ if (!record2(value) || !keys(value, ["schemaVersion", "moduleId", "moduleVersion", "completedStageIds", "completedMissionIds", "assessments", "completion"])) throw new LearningCourseInputError();
7017
+ assertProgress(course, value);
7018
+ return structuredClone(value);
7019
+ }
7020
+ function createLearningCourseProgress(course) {
7021
+ if (validateLearningCourse(course).length) throw new LearningCourseInputError();
7022
+ return {
7023
+ schemaVersion: "1",
7024
+ moduleId: course.moduleId,
7025
+ moduleVersion: course.moduleVersion,
7026
+ completedStageIds: [],
7027
+ completedMissionIds: [],
7028
+ assessments: [],
7029
+ completion: null
7030
+ };
7031
+ }
7032
+ function resolveLearningCourseStage(course, progress, requestedId) {
7033
+ assertProgress(course, progress);
7034
+ const ordered = stages(course);
7035
+ const maximum = Math.min(progress.completedStageIds.length, ordered.length - 1);
7036
+ const requestedIndex = ordered.findIndex((stage) => stage.id === requestedId);
7037
+ return requestedIndex >= 0 && requestedIndex <= maximum ? requestedId : ordered[maximum].id;
7038
+ }
7039
+ function recordLearningCourseAssessment(course, progress, authority) {
7040
+ assertProgress(course, progress);
7041
+ const final = authority.missionId === "final";
7042
+ const missionIndex = course.missions.findIndex((mission2) => mission2.id === authority.missionId);
7043
+ if (!DIGEST.test(authority.sourceDigest) || !REFERENCE.test(authority.referenceId) || (final ? progress.completedMissionIds.length !== course.missions.length : missionIndex < 0 || missionIndex > progress.completedMissionIds.length) || authority.assessmentId !== (final ? course.completionAssessmentId : course.missions[missionIndex]?.assessmentId) || authority.rubric.version !== course.moduleVersion || authority.rubric.criteria.length > 64 || authority.checks.length > 64 || authority.checks.some((check) => typeof check.passed !== "boolean") || validateAssessmentRubric(authority.rubric).length) throw new LearningCourseInputError();
7044
+ let result;
7045
+ try {
7046
+ result = calculateAssessment(authority.rubric, authority.checks);
7047
+ } catch {
7048
+ throw new LearningCourseInputError();
7049
+ }
7050
+ const assessment = {
7051
+ missionId: authority.missionId,
7052
+ assessmentId: authority.assessmentId,
7053
+ referenceId: authority.referenceId,
7054
+ sourceDigest: authority.sourceDigest,
7055
+ result
7056
+ };
7057
+ const duplicate = progress.assessments.find((item) => item.referenceId === authority.referenceId);
7058
+ if (duplicate && JSON.stringify(duplicate) !== JSON.stringify(assessment)) throw new LearningCourseInputError();
7059
+ const next = structuredClone(progress);
7060
+ next.assessments = [...next.assessments.filter((item) => item.missionId !== authority.missionId), assessment];
7061
+ if (final && result.completed) {
7062
+ next.completion = next.completion ? { ...next.completion, bestScore: Math.max(next.completion.bestScore, result.score) } : { referenceId: authority.referenceId, sourceDigest: authority.sourceDigest, bestScore: result.score };
7063
+ }
7064
+ return next;
7065
+ }
7066
+ function completeVerifiedLearningActivity(course, progress, proof) {
7067
+ assertProgress(course, progress);
7068
+ if (proof.accepted !== true || !DIGEST.test(proof.sourceDigest)) throw new LearningCourseInputError();
7069
+ const ordered = stages(course);
7070
+ const index = ordered.findIndex((stage2) => stage2.id === proof.stageId);
7071
+ if (index < 0 || index > progress.completedStageIds.length) throw new LearningCourseInputError();
7072
+ if (index < progress.completedStageIds.length) return structuredClone(progress);
7073
+ const stage = ordered[index];
7074
+ const mission2 = course.missions[Math.floor(index / 9)];
7075
+ if (["assess", "inspect", "fix", "reward"].includes(stage.kind)) {
7076
+ const assessment = progress.assessments.find((item) => item.missionId === mission2.id);
7077
+ if (!assessment || assessment.sourceDigest !== proof.sourceDigest || (stage.kind === "fix" || stage.kind === "reward") && !assessment.result.completed) throw new LearningCourseInputError();
7078
+ }
7079
+ const next = structuredClone(progress);
7080
+ next.completedStageIds.push(proof.stageId);
7081
+ if (stage.kind === "reward") next.completedMissionIds.push(mission2.id);
7082
+ return next;
7083
+ }
7084
+
7085
+ // src/contextual-help.ts
7086
+ var CONTEXTUAL_HELP_CONTRACT_VERSION_V1 = "1.0.0";
7087
+ var CONTEXTUAL_HELP_KINDS_V1 = Object.freeze([
7088
+ "command",
7089
+ "visual-block",
7090
+ "generated-code",
7091
+ "assessment-diagnostic"
7092
+ ]);
7093
+ var CONTEXTUAL_VOICE_INTENTS_V1 = Object.freeze([
7094
+ "describe-command",
7095
+ "describe-inputs",
7096
+ "show-example",
7097
+ "explain-assessment-failure",
7098
+ "suggest-next-experiment",
7099
+ "repeat",
7100
+ "stop",
7101
+ "unresolved"
7102
+ ]);
7103
+ var CONTEXTUAL_VOICE_SUGGESTED_ACTIONS_V1 = Object.freeze([
7104
+ "open-help",
7105
+ "insert-example",
7106
+ "show-inputs",
7107
+ "show-assessment-evidence",
7108
+ "try-next-experiment",
7109
+ "repeat",
7110
+ "stop",
7111
+ "type-question"
7112
+ ]);
7113
+ var OPAQUE_ID = /^[a-z0-9]+(?:[._:-][a-z0-9]+)*$/u;
7114
+ var SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u;
7115
+ var SHA_256 = /^sha256:[a-f0-9]{64}$/u;
7116
+ var LOCALE = /^[a-z]{2,3}(?:-[A-Z]{2})?$/u;
7117
+ var MEDIA_TYPE = /^audio\/[a-z0-9.+-]+$/u;
7118
+ function containsDisallowedControlCharacter(value) {
7119
+ return [...value].some((character) => {
7120
+ const code = character.codePointAt(0) ?? 0;
7121
+ return code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127;
7122
+ });
7123
+ }
7124
+ function requireOpaqueId(value, label, maximum = 160) {
7125
+ if (value.length === 0 || value.length > maximum || !OPAQUE_ID.test(value)) {
7126
+ throw new Error(`${label} must be a bounded opaque identifier.`);
7127
+ }
7128
+ }
7129
+ function requireSemver(value, label) {
7130
+ if (!SEMVER.test(value)) {
7131
+ throw new Error(`${label} must be an immutable semantic version.`);
7132
+ }
7133
+ }
7134
+ function requireIsoTimestamp(value, label) {
7135
+ if (!/^\d{4}-\d{2}-\d{2}T/u.test(value) || !Number.isFinite(Date.parse(value))) {
7136
+ throw new Error(`${label} must be an ISO-8601 timestamp.`);
7137
+ }
7138
+ }
7139
+ function assertValidContextualHelpIdentifier(value) {
7140
+ if (value.schemaVersion !== "1") {
7141
+ throw new Error("schemaVersion must be 1.");
7142
+ }
7143
+ if (value.contractVersion !== CONTEXTUAL_HELP_CONTRACT_VERSION_V1) {
7144
+ throw new Error("contractVersion is unsupported.");
7145
+ }
7146
+ if (!CONTEXTUAL_HELP_KINDS_V1.includes(value.kind)) {
7147
+ throw new Error("kind is unsupported.");
7148
+ }
7149
+ requireOpaqueId(value.moduleId, "moduleId");
7150
+ requireSemver(value.moduleVersion, "moduleVersion");
7151
+ requireSemver(value.manifestVersion, "manifestVersion");
7152
+ requireOpaqueId(value.helpId, "helpId");
7153
+ }
7154
+ function assertValidGuardianVoiceConsent(value) {
7155
+ if (value.schemaVersion !== "1") {
7156
+ throw new Error("schemaVersion must be 1.");
7157
+ }
7158
+ requireOpaqueId(value.actorAccountId, "actorAccountId");
7159
+ requireOpaqueId(value.subjectAccountId, "subjectAccountId");
7160
+ requireSemver(value.policyVersion, "policyVersion");
7161
+ if (value.state !== "granted" && value.state !== "withdrawn") {
7162
+ throw new Error("state must be granted or withdrawn.");
7163
+ }
7164
+ if (value.permittedProcessingRoute !== "private-edge-only") {
7165
+ throw new Error("permittedProcessingRoute must be private-edge-only.");
7166
+ }
7167
+ requireIsoTimestamp(value.recordedAt, "recordedAt");
7168
+ }
7169
+ function assertValidContextualVoiceQuestionMetadata(value) {
7170
+ assertValidContextualHelpIdentifier(value.help);
7171
+ if (!LOCALE.test(value.locale)) throw new Error("locale is invalid.");
7172
+ if (!MEDIA_TYPE.test(value.mediaType)) throw new Error("mediaType is invalid.");
7173
+ if (!Number.isInteger(value.durationMs) || value.durationMs < 1 || value.durationMs > 1e4) {
7174
+ throw new Error("durationMs must be between 1 and 10000.");
7175
+ }
7176
+ if (!Number.isInteger(value.audioBytes) || value.audioBytes < 1 || value.audioBytes > 2097152) {
7177
+ throw new Error("audioBytes must be between 1 and 2097152.");
7178
+ }
7179
+ if (value.assessmentEvidenceId !== void 0) {
7180
+ requireOpaqueId(value.assessmentEvidenceId, "assessmentEvidenceId");
7181
+ }
7182
+ }
7183
+ function assertValidVoiceHelpAvailability(value) {
7184
+ if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
7185
+ if (value.questionProcessingRoute !== "private-edge-only") {
7186
+ throw new Error("questionProcessingRoute must be private-edge-only.");
7187
+ }
7188
+ if (value.guardianVoiceConsentRequired !== true) {
7189
+ throw new Error("guardianVoiceConsentRequired must remain true.");
7190
+ }
7191
+ if (value.maximumRecordingDurationMs !== 1e4 || value.maximumAudioBytes !== 2097152 || value.rawAudioRetention !== "request-memory-only" || value.transcriptRetention !== "request-memory-only") {
7192
+ throw new Error("Voice limits and transient retention are immutable in version one.");
7193
+ }
7194
+ if (value.microphoneAvailable !== (value.microphoneReason === "available")) {
7195
+ throw new Error("microphoneReason must match microphone availability.");
7196
+ }
7197
+ }
7198
+ function assertValidCanonicalSpokenHelpDescriptor(value) {
7199
+ if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
7200
+ assertValidContextualHelpIdentifier(value.help);
7201
+ requireOpaqueId(value.canonicalTextId, "canonicalTextId");
7202
+ if (!SHA_256.test(value.authoritativeTextDigest)) {
7203
+ throw new Error("authoritativeTextDigest must be a SHA-256 digest.");
7204
+ }
7205
+ if (!LOCALE.test(value.locale)) throw new Error("locale is invalid.");
7206
+ requireOpaqueId(value.voiceProfile, "voiceProfile");
7207
+ requireSemver(value.pronunciationVersion, "pronunciationVersion");
7208
+ if (value.utteranceClass !== "system-generic" || value.sharingScope !== "global" || value.reuse !== "exact-only" || value.containsPersonalData !== false || value.containsLearnerContent !== false) {
7209
+ throw new Error("Canonical spoken help must use global system-generic exact-only reuse.");
7210
+ }
7211
+ }
7212
+ function assertValidContextualVoiceQuestionResult(value) {
7213
+ if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
7214
+ assertValidContextualHelpIdentifier(value.help);
7215
+ if (!CONTEXTUAL_VOICE_INTENTS_V1.includes(value.intent)) {
7216
+ throw new Error("intent is unsupported.");
7217
+ }
7218
+ if (value.transcriptRetention !== "request-memory-only") {
7219
+ throw new Error("transcriptRetention must be request-memory-only.");
7220
+ }
7221
+ if (value.status !== "resolved" && value.status !== "suggestions" && value.status !== "stopped") {
7222
+ throw new Error("status is unsupported.");
7223
+ }
7224
+ if (value.transientTranscript.length > 500 || containsDisallowedControlCharacter(value.transientTranscript)) {
7225
+ throw new Error("transientTranscript is invalid.");
7226
+ }
7227
+ if (value.answer) {
7228
+ requireOpaqueId(value.answer.helpId, "answer.helpId");
7229
+ if (value.answer.source !== "module-documentation" && value.answer.source !== "assessment-evidence" && value.answer.source !== "authored-fallback" || value.answer.text.length === 0 || value.answer.text.length > 800 || containsDisallowedControlCharacter(value.answer.text)) {
7230
+ throw new Error("answer.text is invalid.");
7231
+ }
7232
+ }
7233
+ if (value.suggestedActions.some(
7234
+ (action) => !CONTEXTUAL_VOICE_SUGGESTED_ACTIONS_V1.includes(action)
7235
+ )) {
7236
+ throw new Error("suggestedActions contains an unsupported action.");
7237
+ }
7238
+ if (value.mayAssignScore !== false || value.mayAwardReward !== false || value.mayPublish !== false || value.mayControlHardware !== false) {
7239
+ throw new Error("mayAssignScore, reward, publish and hardware authority must remain false.");
7240
+ }
7241
+ }
7242
+
7243
+ // src/publishing.ts
7244
+ var STATIC_PROJECT_PUBLISHING_CONTRACT_VERSION_V1 = "1.0.0";
7245
+ var STATIC_PROJECT_SCANNER_VERSION_V1 = "junior-coder-static-scan-v1";
7246
+ var STATIC_PROJECT_APPROVAL_STATEMENT_VERSION_V1 = "guardian-static-publication-v1";
7247
+ var STATIC_PROJECT_DEFAULT_LIFETIME_DAYS_V1 = 90;
7248
+ var STATIC_PROJECT_MAX_SOURCE_CHARACTERS_V1 = 8e3;
7249
+ var STATIC_PROJECT_SAFETY_CHECK_IDS_V1 = [
7250
+ "allow-listed-source",
7251
+ "no-personal-details",
7252
+ "no-external-network",
7253
+ "no-executable-markup",
7254
+ "no-transmitting-forms",
7255
+ "no-uploads-or-embeds",
7256
+ "no-trackers-or-advertising",
7257
+ "no-account-identifiers"
7258
+ ];
7259
+ var DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
7260
+ var IDENTIFIER_PATTERN = /^(?:account|approval|publication|snapshot)_[A-Za-z0-9_-]{20,128}$/u;
7261
+ var VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u;
7262
+ var RANDOM_SLUG_PATTERN = /^[A-Za-z0-9_-]{40,128}$/u;
7263
+ function validDate(value) {
7264
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
7265
+ }
7266
+ function allSafetyChecksPresent(scan) {
7267
+ if (!Array.isArray(scan.checks) || scan.checks.length !== STATIC_PROJECT_SAFETY_CHECK_IDS_V1.length) {
7268
+ return false;
7269
+ }
7270
+ const supplied = new Set(scan.checks.map((check) => check.id));
7271
+ return STATIC_PROJECT_SAFETY_CHECK_IDS_V1.every((id2) => supplied.has(id2)) && scan.checks.every((check) => check.passed === true);
7272
+ }
7273
+ function safeAdventureRender(value) {
7274
+ const headings = ["Moonbase Missions", "Forest Rescue Plans", "Ocean Quest Board"];
7275
+ const titles = ["Find the moon crystal", "Guide the lost firefly", "Map the coral garden"];
7276
+ const days = ["Saturday", "Sunday", "School holiday"];
7277
+ const messages = ["Choose a day", "Add a mission title", "Mission ready!"];
7278
+ return headings.includes(value.heading) && Array.isArray(value.missions) && value.missions.length >= 1 && value.missions.length <= 3 && value.missions.every((mission2) => titles.includes(mission2.title) && days.includes(mission2.day)) && messages.includes(value.validationMessage) && value.savedAcrossRestart === true && value.accessibleSummaryEnabled === true;
7279
+ }
7280
+ function safeCreatureRender(value) {
7281
+ return ["Moon Moth", "Cloud Cat", "Pebble Dragon"].includes(value.creature) && ["Resting", "Ready to play", "Snack time"].includes(value.status) && [5, 10, 15].includes(value.timerSeconds) && ["single", "cosy-grid", "wide-grid"].includes(value.layout) && value.reducedMotion === true;
7282
+ }
7283
+ function safeRobotRender(value) {
7284
+ return ["scan", "hold-position", "return-to-base"].includes(value.command) && value.safetyConfirmed === true && [1, 2, 4].includes(value.telemetryRate) && ["line", "bars", "text-only"].includes(value.chartMode) && value.serialSimulation === true && ["SCANNING", "HOLDING", "RETURNING"].includes(value.state);
7285
+ }
7286
+ function validateStaticProjectSnapshot(value) {
7287
+ const issues = [];
7288
+ if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
7289
+ if (value.contractVersion !== STATIC_PROJECT_PUBLISHING_CONTRACT_VERSION_V1) {
7290
+ issues.push("invalid-contract-version");
7291
+ }
7292
+ if (!IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.subjectAccountId)) {
7293
+ issues.push("invalid-identifier");
7294
+ }
7295
+ if (!value.moduleId.startsWith("junior-coder.") || value.moduleId !== `junior-coder.${value.moduleSlug}`) {
7296
+ issues.push("invalid-module");
7297
+ }
7298
+ if (!VERSION_PATTERN.test(value.moduleVersion)) issues.push("invalid-version");
7299
+ if (!DIGEST_PATTERN.test(value.sourceDigest)) issues.push("invalid-source-digest");
7300
+ if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
7301
+ if (!Number.isInteger(value.sourceCharacterCount) || value.sourceCharacterCount < 1 || value.sourceCharacterCount > STATIC_PROJECT_MAX_SOURCE_CHARACTERS_V1) {
7302
+ issues.push("invalid-source-size");
7303
+ }
7304
+ if (!Number.isInteger(value.assessmentScore) || value.assessmentScore < 80 || value.assessmentScore > 100) {
7305
+ issues.push("assessment-score-incomplete");
7306
+ }
7307
+ if (value.mandatorySafetyPassed !== true) issues.push("mandatory-safety-missing");
7308
+ if (value.scan?.passed !== true || value.scan?.scannerVersion !== STATIC_PROJECT_SCANNER_VERSION_V1) {
7309
+ issues.push("scan-not-passed");
7310
+ }
7311
+ if (!value.scan || !allSafetyChecksPresent(value.scan)) issues.push("scan-checks-incomplete");
7312
+ if (value.moduleSlug !== value.renderModel?.kind) issues.push("module-render-model-mismatch");
7313
+ const renderSafe = value.renderModel?.kind === "adventure-mission-planner" ? safeAdventureRender(value.renderModel) : value.renderModel?.kind === "creature-care-dashboard" ? safeCreatureRender(value.renderModel) : value.renderModel?.kind === "robot-mission-control" ? safeRobotRender(value.renderModel) : false;
7314
+ if (!renderSafe) issues.push("unsafe-render-model");
7315
+ if (value.state !== "pending-review" || !validDate(value.createdAt)) issues.push("invalid-created-at");
7316
+ return [...new Set(issues)];
7317
+ }
7318
+ function assertValidStaticProjectSnapshot(value) {
7319
+ const issues = validateStaticProjectSnapshot(value);
7320
+ if (issues.length > 0) throw new Error(`Invalid static project snapshot: ${issues.join(", ")}`);
7321
+ }
7322
+ function validateStaticProjectGuardianApproval(value) {
7323
+ const issues = [];
7324
+ if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
7325
+ if (!IDENTIFIER_PATTERN.test(value.approvalId) || !IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.guardianActorAccountId)) {
7326
+ issues.push("invalid-identifier");
7327
+ }
7328
+ if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
7329
+ if (value.statementVersion !== STATIC_PROJECT_APPROVAL_STATEMENT_VERSION_V1) {
7330
+ issues.push("invalid-approval-statement");
7331
+ }
7332
+ if (!validDate(value.approvedAt)) issues.push("invalid-approved-at");
7333
+ return [...new Set(issues)];
7334
+ }
7335
+ function assertValidStaticProjectGuardianApproval(value) {
7336
+ const issues = validateStaticProjectGuardianApproval(value);
7337
+ if (issues.length > 0) throw new Error(`Invalid static project approval: ${issues.join(", ")}`);
7338
+ }
7339
+ function validateStaticProjectPublication(value) {
7340
+ const issues = [];
7341
+ if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
7342
+ if (!IDENTIFIER_PATTERN.test(value.publicationId) || !IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.approvalId)) {
7343
+ issues.push("invalid-identifier");
7344
+ }
7345
+ if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
7346
+ if (!RANDOM_SLUG_PATTERN.test(value.randomSlug)) issues.push("invalid-random-slug");
7347
+ let parsedUrl;
7348
+ try {
7349
+ parsedUrl = new URL(value.publicUrl);
7350
+ } catch {
7351
+ parsedUrl = void 0;
7352
+ }
7353
+ if (!parsedUrl || parsedUrl.protocol !== "https:" || parsedUrl.username || parsedUrl.password) {
7354
+ issues.push("invalid-public-url");
7355
+ }
7356
+ if (!parsedUrl?.pathname.endsWith(`/${value.randomSlug}`)) issues.push("public-url-slug-mismatch");
7357
+ if (value.state !== "published") issues.push("published-state-required");
7358
+ if (!validDate(value.publishedAt)) issues.push("invalid-published-at");
7359
+ if (!validDate(value.expiresAt) || validDate(value.publishedAt) && Date.parse(value.expiresAt) <= Date.parse(value.publishedAt)) {
7360
+ issues.push("invalid-publication-expiry");
7361
+ }
7362
+ if (value.renewedAt !== void 0 && !validDate(value.renewedAt) || value.unpublishedAt !== void 0 && !validDate(value.unpublishedAt)) {
7363
+ issues.push("invalid-lifecycle-timestamp");
7364
+ }
7365
+ return [...new Set(issues)];
7366
+ }
7367
+ function assertValidStaticProjectPublication(value) {
7368
+ const issues = validateStaticProjectPublication(value);
7369
+ if (issues.length > 0) throw new Error(`Invalid static project publication: ${issues.join(", ")}`);
7370
+ }
7371
+
7126
7372
  // src/validation.ts
7127
7373
  var CANONICAL_TOKEN_SUBUNITS = /^(0|[1-9][0-9]*)$/u;
7128
7374
  var EXACT_PACKAGE_NAME = /^@plasius\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
@@ -7322,6 +7568,9 @@ ${summary}`);
7322
7568
  JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_2,
7323
7569
  JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_3,
7324
7570
  JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_4,
7571
+ LEARNING_COURSE_LIMITS,
7572
+ LEARNING_COURSE_STAGE_ORDER,
7573
+ LearningCourseInputError,
7325
7574
  METEOR_SHIELD_MISSION_ONE_AUTHORING_V1,
7326
7575
  MISSION_AUTHORING_CONTRACT_VERSION_V1,
7327
7576
  OBSTACLE_EXPLORER_MISSION_ONE_AUTHORING_V1,
@@ -7359,8 +7608,18 @@ ${summary}`);
7359
7608
  assertValidStaticProjectSnapshot,
7360
7609
  assertValidVoiceHelpAvailability,
7361
7610
  calculateAssessment,
7611
+ completeVerifiedLearningActivity,
7612
+ createLearningCourseProgress,
7362
7613
  isExternalLearningContentReferenceV1,
7614
+ parseLearningCourse,
7615
+ parseLearningCourseDraft,
7616
+ parseLearningCourseProgress,
7617
+ parseLearningProject,
7618
+ parseLearningSaveSlotId,
7619
+ recordLearningCourseAssessment,
7620
+ resolveLearningCourseStage,
7363
7621
  validateAssessmentRubric,
7622
+ validateLearningCourse,
7364
7623
  validateLearningPath,
7365
7624
  validateMissionAuthoringBundle,
7366
7625
  validateStaticProjectGuardianApproval,