@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/README.md +41 -0
- package/dist/chunk-OXWMIHYI.js +6076 -0
- package/dist/chunk-OXWMIHYI.js.map +1 -0
- package/dist/course-authoring-DImvrK5r.d.cts +514 -0
- package/dist/course-authoring-DImvrK5r.d.ts +514 -0
- package/dist/courses/robot-maze-dash.cjs +355 -0
- package/dist/courses/robot-maze-dash.cjs.map +1 -0
- package/dist/courses/robot-maze-dash.d.cts +6 -0
- package/dist/courses/robot-maze-dash.d.ts +6 -0
- package/dist/courses/robot-maze-dash.js +196 -0
- package/dist/courses/robot-maze-dash.js.map +1 -0
- package/dist/index.cjs +726 -467
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -413
- package/dist/index.d.ts +60 -413
- package/dist/index.js +145 -5903
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
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/
|
|
940
|
-
var
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
998
|
-
|
|
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 (!
|
|
1001
|
-
|
|
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
|
-
|
|
1004
|
-
requireSemver(value.moduleVersion, "moduleVersion");
|
|
1005
|
-
requireSemver(value.manifestVersion, "manifestVersion");
|
|
1006
|
-
requireOpaqueId(value.helpId, "helpId");
|
|
1021
|
+
return issues;
|
|
1007
1022
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
|
1038
|
-
|
|
1039
|
-
if (
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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 (
|
|
1049
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
1252
|
-
"
|
|
1253
|
-
`
|
|
1254
|
-
|
|
1255
|
-
|
|
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 (
|
|
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
|
-
|
|
1264
|
-
"
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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 (
|
|
1284
|
-
(criterion) => criterion.dimension === "safety" && criterion.mandatory
|
|
1285
|
-
)) {
|
|
1150
|
+
if (learner.readinessChecks.some((check) => check.scored !== false)) {
|
|
1286
1151
|
issues.push(
|
|
1287
|
-
|
|
1288
|
-
"
|
|
1289
|
-
"
|
|
1290
|
-
|
|
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,
|