@plasius/learning 0.2.24 → 0.3.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 +25 -0
- package/dist/index.cjs +726 -467
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -1
- package/dist/index.d.ts +149 -1
- package/dist/index.js +713 -467
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -847,497 +847,210 @@ var JUNIOR_CODER_ROBOT_RESCUE_PATH_CURRENT = JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_4
|
|
|
847
847
|
var EXTERNAL_LEARNING_CONTENT_REFERENCE_VERSION_V1 = "1";
|
|
848
848
|
var MISSION_AUTHORING_CONTRACT_VERSION_V1 = "1.0.0";
|
|
849
849
|
|
|
850
|
-
// src/
|
|
851
|
-
var
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
"describe-command",
|
|
860
|
-
"describe-inputs",
|
|
861
|
-
"show-example",
|
|
862
|
-
"explain-assessment-failure",
|
|
863
|
-
"suggest-next-experiment",
|
|
864
|
-
"repeat",
|
|
865
|
-
"stop",
|
|
866
|
-
"unresolved"
|
|
867
|
-
]);
|
|
868
|
-
var CONTEXTUAL_VOICE_SUGGESTED_ACTIONS_V1 = Object.freeze([
|
|
869
|
-
"open-help",
|
|
870
|
-
"insert-example",
|
|
871
|
-
"show-inputs",
|
|
872
|
-
"show-assessment-evidence",
|
|
873
|
-
"try-next-experiment",
|
|
874
|
-
"repeat",
|
|
875
|
-
"stop",
|
|
876
|
-
"type-question"
|
|
877
|
-
]);
|
|
878
|
-
var OPAQUE_ID = /^[a-z0-9]+(?:[._:-][a-z0-9]+)*$/u;
|
|
879
|
-
var SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u;
|
|
880
|
-
var SHA_256 = /^sha256:[a-f0-9]{64}$/u;
|
|
881
|
-
var LOCALE = /^[a-z]{2,3}(?:-[A-Z]{2})?$/u;
|
|
882
|
-
var MEDIA_TYPE = /^audio\/[a-z0-9.+-]+$/u;
|
|
883
|
-
function containsDisallowedControlCharacter(value) {
|
|
884
|
-
return [...value].some((character) => {
|
|
885
|
-
const code = character.codePointAt(0) ?? 0;
|
|
886
|
-
return code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127;
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
function requireOpaqueId(value, label, maximum = 160) {
|
|
890
|
-
if (value.length === 0 || value.length > maximum || !OPAQUE_ID.test(value)) {
|
|
891
|
-
throw new Error(`${label} must be a bounded opaque identifier.`);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
function requireSemver(value, label) {
|
|
895
|
-
if (!SEMVER.test(value)) {
|
|
896
|
-
throw new Error(`${label} must be an immutable semantic version.`);
|
|
897
|
-
}
|
|
850
|
+
// src/rubric-validation.ts
|
|
851
|
+
var DIMENSION_TOTALS = {
|
|
852
|
+
structure: 20,
|
|
853
|
+
behaviour: 50,
|
|
854
|
+
resilience: 20,
|
|
855
|
+
safety: 10
|
|
856
|
+
};
|
|
857
|
+
function rubricIssue(code, message, path, moduleId) {
|
|
858
|
+
return { code, message, path, ...moduleId ? { moduleId } : {} };
|
|
898
859
|
}
|
|
899
|
-
function
|
|
900
|
-
|
|
901
|
-
|
|
860
|
+
function validateAssessmentRubric(rubric2, path = "assessment", moduleId) {
|
|
861
|
+
const issues = [];
|
|
862
|
+
const criterionIds = /* @__PURE__ */ new Set();
|
|
863
|
+
const dimensionTotals = {
|
|
864
|
+
structure: 0,
|
|
865
|
+
behaviour: 0,
|
|
866
|
+
resilience: 0,
|
|
867
|
+
safety: 0
|
|
868
|
+
};
|
|
869
|
+
let rubricTotal = 0;
|
|
870
|
+
for (const criterion of rubric2.criteria) {
|
|
871
|
+
rubricTotal += criterion.points;
|
|
872
|
+
dimensionTotals[criterion.dimension] += criterion.points;
|
|
873
|
+
if (criterionIds.has(criterion.id)) {
|
|
874
|
+
issues.push(
|
|
875
|
+
rubricIssue(
|
|
876
|
+
"duplicate-criterion-id",
|
|
877
|
+
`Duplicate assessment criterion ${criterion.id}.`,
|
|
878
|
+
`${path}.criteria`,
|
|
879
|
+
moduleId
|
|
880
|
+
)
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
criterionIds.add(criterion.id);
|
|
902
884
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
885
|
+
if (rubricTotal !== 100) {
|
|
886
|
+
issues.push(
|
|
887
|
+
rubricIssue(
|
|
888
|
+
"rubric-total",
|
|
889
|
+
`Assessment rubric totals ${rubricTotal}; expected 100.`,
|
|
890
|
+
`${path}.criteria`,
|
|
891
|
+
moduleId
|
|
892
|
+
)
|
|
893
|
+
);
|
|
907
894
|
}
|
|
908
|
-
|
|
909
|
-
|
|
895
|
+
for (const [dimension, expected] of Object.entries(DIMENSION_TOTALS)) {
|
|
896
|
+
if (dimensionTotals[dimension] !== expected) {
|
|
897
|
+
issues.push(
|
|
898
|
+
rubricIssue(
|
|
899
|
+
"rubric-dimension-total",
|
|
900
|
+
`${dimension} criteria total ${dimensionTotals[dimension]}; expected ${expected}.`,
|
|
901
|
+
`${path}.criteria`,
|
|
902
|
+
moduleId
|
|
903
|
+
)
|
|
904
|
+
);
|
|
905
|
+
}
|
|
910
906
|
}
|
|
911
|
-
if (!
|
|
912
|
-
|
|
907
|
+
if (!rubric2.criteria.some(
|
|
908
|
+
(criterion) => criterion.dimension === "safety" && criterion.mandatory
|
|
909
|
+
)) {
|
|
910
|
+
issues.push(
|
|
911
|
+
rubricIssue(
|
|
912
|
+
"missing-mandatory-safety",
|
|
913
|
+
"Every module requires a mandatory safety criterion.",
|
|
914
|
+
`${path}.criteria`,
|
|
915
|
+
moduleId
|
|
916
|
+
)
|
|
917
|
+
);
|
|
913
918
|
}
|
|
914
|
-
|
|
915
|
-
requireSemver(value.moduleVersion, "moduleVersion");
|
|
916
|
-
requireSemver(value.manifestVersion, "manifestVersion");
|
|
917
|
-
requireOpaqueId(value.helpId, "helpId");
|
|
919
|
+
return issues;
|
|
918
920
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
921
|
+
|
|
922
|
+
// src/mission-authoring.ts
|
|
923
|
+
var JUNIOR_CODER_MISSION_STAGE_ORDER_V1 = [
|
|
924
|
+
"learn",
|
|
925
|
+
"predict",
|
|
926
|
+
"build",
|
|
927
|
+
"run",
|
|
928
|
+
"assess",
|
|
929
|
+
"inspect",
|
|
930
|
+
"fix",
|
|
931
|
+
"explain",
|
|
932
|
+
"reward"
|
|
933
|
+
];
|
|
934
|
+
var LEARNER_STARTER_KINDS = /* @__PURE__ */ new Set([
|
|
935
|
+
"starter-code",
|
|
936
|
+
"starter-assets",
|
|
937
|
+
"sample-data"
|
|
938
|
+
]);
|
|
939
|
+
var LEARNER_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
|
|
940
|
+
"facilitator-note",
|
|
941
|
+
"answer-key",
|
|
942
|
+
"protected-test"
|
|
943
|
+
]);
|
|
944
|
+
var SINGLE_MODE_REQUIRES_ALTERNATIVE = /* @__PURE__ */ new Set([
|
|
945
|
+
"pointer",
|
|
946
|
+
"drag",
|
|
947
|
+
"audio",
|
|
948
|
+
"colour",
|
|
949
|
+
"motion"
|
|
950
|
+
]);
|
|
951
|
+
function authoringIssue(code, message, path) {
|
|
952
|
+
return { code, message, path };
|
|
933
953
|
}
|
|
934
|
-
function
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
if (value.assessmentEvidenceId !== void 0) {
|
|
945
|
-
requireOpaqueId(value.assessmentEvidenceId, "assessmentEvidenceId");
|
|
954
|
+
function reportDuplicateIds(ids, path) {
|
|
955
|
+
const seen = /* @__PURE__ */ new Set();
|
|
956
|
+
const issues = [];
|
|
957
|
+
for (const id2 of ids) {
|
|
958
|
+
if (seen.has(id2)) {
|
|
959
|
+
issues.push(
|
|
960
|
+
authoringIssue("duplicate-id", `Duplicate authored ID ${id2}.`, path)
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
seen.add(id2);
|
|
946
964
|
}
|
|
965
|
+
return issues;
|
|
947
966
|
}
|
|
948
|
-
function
|
|
949
|
-
|
|
950
|
-
if (
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
967
|
+
function validateMissionAuthoringBundle(bundle, module2) {
|
|
968
|
+
const issues = [];
|
|
969
|
+
if (bundle.version !== MISSION_AUTHORING_CONTRACT_VERSION_V1) {
|
|
970
|
+
issues.push(
|
|
971
|
+
authoringIssue(
|
|
972
|
+
"bundle-version-mismatch",
|
|
973
|
+
`Unsupported mission authoring version ${bundle.version}.`,
|
|
974
|
+
"version"
|
|
975
|
+
)
|
|
976
|
+
);
|
|
958
977
|
}
|
|
959
|
-
if (
|
|
960
|
-
|
|
978
|
+
if (bundle.moduleId !== module2.id || bundle.moduleVersion !== module2.version) {
|
|
979
|
+
issues.push(
|
|
980
|
+
authoringIssue(
|
|
981
|
+
"module-reference-mismatch",
|
|
982
|
+
`Bundle ${bundle.moduleId}@${bundle.moduleVersion} does not match ${module2.id}@${module2.version}.`,
|
|
983
|
+
"moduleId"
|
|
984
|
+
)
|
|
985
|
+
);
|
|
961
986
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
987
|
+
if (!module2.missions.some((mission2) => mission2.id === bundle.missionId)) {
|
|
988
|
+
issues.push(
|
|
989
|
+
authoringIssue(
|
|
990
|
+
"mission-reference-mismatch",
|
|
991
|
+
`Mission ${bundle.missionId} does not exist in module ${module2.id}.`,
|
|
992
|
+
"missionId"
|
|
993
|
+
)
|
|
994
|
+
);
|
|
969
995
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
996
|
+
const learner = bundle.learner;
|
|
997
|
+
const facilitator = bundle.facilitator;
|
|
998
|
+
if (learner.estimatedMinutes < 15 || learner.estimatedMinutes > 25) {
|
|
999
|
+
issues.push(
|
|
1000
|
+
authoringIssue(
|
|
1001
|
+
"invalid-duration",
|
|
1002
|
+
"A mission must last between 15 and 25 minutes.",
|
|
1003
|
+
"learner.estimatedMinutes"
|
|
1004
|
+
)
|
|
1005
|
+
);
|
|
975
1006
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
if (!CONTEXTUAL_VOICE_INTENTS_V1.includes(value.intent)) {
|
|
981
|
-
throw new Error("intent is unsupported.");
|
|
982
|
-
}
|
|
983
|
-
if (value.transcriptRetention !== "request-memory-only") {
|
|
984
|
-
throw new Error("transcriptRetention must be request-memory-only.");
|
|
985
|
-
}
|
|
986
|
-
if (value.status !== "resolved" && value.status !== "suggestions" && value.status !== "stopped") {
|
|
987
|
-
throw new Error("status is unsupported.");
|
|
988
|
-
}
|
|
989
|
-
if (value.transientTranscript.length > 500 || containsDisallowedControlCharacter(value.transientTranscript)) {
|
|
990
|
-
throw new Error("transientTranscript is invalid.");
|
|
991
|
-
}
|
|
992
|
-
if (value.answer) {
|
|
993
|
-
requireOpaqueId(value.answer.helpId, "answer.helpId");
|
|
994
|
-
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)) {
|
|
995
|
-
throw new Error("answer.text is invalid.");
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
if (value.suggestedActions.some(
|
|
999
|
-
(action) => !CONTEXTUAL_VOICE_SUGGESTED_ACTIONS_V1.includes(action)
|
|
1000
|
-
)) {
|
|
1001
|
-
throw new Error("suggestedActions contains an unsupported action.");
|
|
1002
|
-
}
|
|
1003
|
-
if (value.mayAssignScore !== false || value.mayAwardReward !== false || value.mayPublish !== false || value.mayControlHardware !== false) {
|
|
1004
|
-
throw new Error("mayAssignScore, reward, publish and hardware authority must remain false.");
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// src/publishing.ts
|
|
1009
|
-
var STATIC_PROJECT_PUBLISHING_CONTRACT_VERSION_V1 = "1.0.0";
|
|
1010
|
-
var STATIC_PROJECT_SCANNER_VERSION_V1 = "junior-coder-static-scan-v1";
|
|
1011
|
-
var STATIC_PROJECT_APPROVAL_STATEMENT_VERSION_V1 = "guardian-static-publication-v1";
|
|
1012
|
-
var STATIC_PROJECT_DEFAULT_LIFETIME_DAYS_V1 = 90;
|
|
1013
|
-
var STATIC_PROJECT_MAX_SOURCE_CHARACTERS_V1 = 8e3;
|
|
1014
|
-
var STATIC_PROJECT_SAFETY_CHECK_IDS_V1 = [
|
|
1015
|
-
"allow-listed-source",
|
|
1016
|
-
"no-personal-details",
|
|
1017
|
-
"no-external-network",
|
|
1018
|
-
"no-executable-markup",
|
|
1019
|
-
"no-transmitting-forms",
|
|
1020
|
-
"no-uploads-or-embeds",
|
|
1021
|
-
"no-trackers-or-advertising",
|
|
1022
|
-
"no-account-identifiers"
|
|
1023
|
-
];
|
|
1024
|
-
var DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
1025
|
-
var IDENTIFIER_PATTERN = /^(?:account|approval|publication|snapshot)_[A-Za-z0-9_-]{20,128}$/u;
|
|
1026
|
-
var VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u;
|
|
1027
|
-
var RANDOM_SLUG_PATTERN = /^[A-Za-z0-9_-]{40,128}$/u;
|
|
1028
|
-
function validDate(value) {
|
|
1029
|
-
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
1030
|
-
}
|
|
1031
|
-
function allSafetyChecksPresent(scan) {
|
|
1032
|
-
if (!Array.isArray(scan.checks) || scan.checks.length !== STATIC_PROJECT_SAFETY_CHECK_IDS_V1.length) {
|
|
1033
|
-
return false;
|
|
1034
|
-
}
|
|
1035
|
-
const supplied = new Set(scan.checks.map((check) => check.id));
|
|
1036
|
-
return STATIC_PROJECT_SAFETY_CHECK_IDS_V1.every((id) => supplied.has(id)) && scan.checks.every((check) => check.passed === true);
|
|
1037
|
-
}
|
|
1038
|
-
function safeAdventureRender(value) {
|
|
1039
|
-
const headings = ["Moonbase Missions", "Forest Rescue Plans", "Ocean Quest Board"];
|
|
1040
|
-
const titles = ["Find the moon crystal", "Guide the lost firefly", "Map the coral garden"];
|
|
1041
|
-
const days = ["Saturday", "Sunday", "School holiday"];
|
|
1042
|
-
const messages = ["Choose a day", "Add a mission title", "Mission ready!"];
|
|
1043
|
-
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;
|
|
1044
|
-
}
|
|
1045
|
-
function safeCreatureRender(value) {
|
|
1046
|
-
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;
|
|
1047
|
-
}
|
|
1048
|
-
function safeRobotRender(value) {
|
|
1049
|
-
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);
|
|
1050
|
-
}
|
|
1051
|
-
function validateStaticProjectSnapshot(value) {
|
|
1052
|
-
const issues = [];
|
|
1053
|
-
if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
|
|
1054
|
-
if (value.contractVersion !== STATIC_PROJECT_PUBLISHING_CONTRACT_VERSION_V1) {
|
|
1055
|
-
issues.push("invalid-contract-version");
|
|
1056
|
-
}
|
|
1057
|
-
if (!IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.subjectAccountId)) {
|
|
1058
|
-
issues.push("invalid-identifier");
|
|
1059
|
-
}
|
|
1060
|
-
if (!value.moduleId.startsWith("junior-coder.") || value.moduleId !== `junior-coder.${value.moduleSlug}`) {
|
|
1061
|
-
issues.push("invalid-module");
|
|
1062
|
-
}
|
|
1063
|
-
if (!VERSION_PATTERN.test(value.moduleVersion)) issues.push("invalid-version");
|
|
1064
|
-
if (!DIGEST_PATTERN.test(value.sourceDigest)) issues.push("invalid-source-digest");
|
|
1065
|
-
if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
|
|
1066
|
-
if (!Number.isInteger(value.sourceCharacterCount) || value.sourceCharacterCount < 1 || value.sourceCharacterCount > STATIC_PROJECT_MAX_SOURCE_CHARACTERS_V1) {
|
|
1067
|
-
issues.push("invalid-source-size");
|
|
1068
|
-
}
|
|
1069
|
-
if (!Number.isInteger(value.assessmentScore) || value.assessmentScore < 80 || value.assessmentScore > 100) {
|
|
1070
|
-
issues.push("assessment-score-incomplete");
|
|
1071
|
-
}
|
|
1072
|
-
if (value.mandatorySafetyPassed !== true) issues.push("mandatory-safety-missing");
|
|
1073
|
-
if (value.scan?.passed !== true || value.scan?.scannerVersion !== STATIC_PROJECT_SCANNER_VERSION_V1) {
|
|
1074
|
-
issues.push("scan-not-passed");
|
|
1075
|
-
}
|
|
1076
|
-
if (!value.scan || !allSafetyChecksPresent(value.scan)) issues.push("scan-checks-incomplete");
|
|
1077
|
-
if (value.moduleSlug !== value.renderModel?.kind) issues.push("module-render-model-mismatch");
|
|
1078
|
-
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;
|
|
1079
|
-
if (!renderSafe) issues.push("unsafe-render-model");
|
|
1080
|
-
if (value.state !== "pending-review" || !validDate(value.createdAt)) issues.push("invalid-created-at");
|
|
1081
|
-
return [...new Set(issues)];
|
|
1082
|
-
}
|
|
1083
|
-
function assertValidStaticProjectSnapshot(value) {
|
|
1084
|
-
const issues = validateStaticProjectSnapshot(value);
|
|
1085
|
-
if (issues.length > 0) throw new Error(`Invalid static project snapshot: ${issues.join(", ")}`);
|
|
1086
|
-
}
|
|
1087
|
-
function validateStaticProjectGuardianApproval(value) {
|
|
1088
|
-
const issues = [];
|
|
1089
|
-
if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
|
|
1090
|
-
if (!IDENTIFIER_PATTERN.test(value.approvalId) || !IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.guardianActorAccountId)) {
|
|
1091
|
-
issues.push("invalid-identifier");
|
|
1092
|
-
}
|
|
1093
|
-
if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
|
|
1094
|
-
if (value.statementVersion !== STATIC_PROJECT_APPROVAL_STATEMENT_VERSION_V1) {
|
|
1095
|
-
issues.push("invalid-approval-statement");
|
|
1096
|
-
}
|
|
1097
|
-
if (!validDate(value.approvedAt)) issues.push("invalid-approved-at");
|
|
1098
|
-
return [...new Set(issues)];
|
|
1099
|
-
}
|
|
1100
|
-
function assertValidStaticProjectGuardianApproval(value) {
|
|
1101
|
-
const issues = validateStaticProjectGuardianApproval(value);
|
|
1102
|
-
if (issues.length > 0) throw new Error(`Invalid static project approval: ${issues.join(", ")}`);
|
|
1103
|
-
}
|
|
1104
|
-
function validateStaticProjectPublication(value) {
|
|
1105
|
-
const issues = [];
|
|
1106
|
-
if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
|
|
1107
|
-
if (!IDENTIFIER_PATTERN.test(value.publicationId) || !IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.approvalId)) {
|
|
1108
|
-
issues.push("invalid-identifier");
|
|
1109
|
-
}
|
|
1110
|
-
if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
|
|
1111
|
-
if (!RANDOM_SLUG_PATTERN.test(value.randomSlug)) issues.push("invalid-random-slug");
|
|
1112
|
-
let parsedUrl;
|
|
1113
|
-
try {
|
|
1114
|
-
parsedUrl = new URL(value.publicUrl);
|
|
1115
|
-
} catch {
|
|
1116
|
-
parsedUrl = void 0;
|
|
1117
|
-
}
|
|
1118
|
-
if (!parsedUrl || parsedUrl.protocol !== "https:" || parsedUrl.username || parsedUrl.password) {
|
|
1119
|
-
issues.push("invalid-public-url");
|
|
1120
|
-
}
|
|
1121
|
-
if (!parsedUrl?.pathname.endsWith(`/${value.randomSlug}`)) issues.push("public-url-slug-mismatch");
|
|
1122
|
-
if (value.state !== "published") issues.push("published-state-required");
|
|
1123
|
-
if (!validDate(value.publishedAt)) issues.push("invalid-published-at");
|
|
1124
|
-
if (!validDate(value.expiresAt) || validDate(value.publishedAt) && Date.parse(value.expiresAt) <= Date.parse(value.publishedAt)) {
|
|
1125
|
-
issues.push("invalid-publication-expiry");
|
|
1126
|
-
}
|
|
1127
|
-
if (value.renewedAt !== void 0 && !validDate(value.renewedAt) || value.unpublishedAt !== void 0 && !validDate(value.unpublishedAt)) {
|
|
1128
|
-
issues.push("invalid-lifecycle-timestamp");
|
|
1129
|
-
}
|
|
1130
|
-
return [...new Set(issues)];
|
|
1131
|
-
}
|
|
1132
|
-
function assertValidStaticProjectPublication(value) {
|
|
1133
|
-
const issues = validateStaticProjectPublication(value);
|
|
1134
|
-
if (issues.length > 0) throw new Error(`Invalid static project publication: ${issues.join(", ")}`);
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// src/rubric-validation.ts
|
|
1138
|
-
var DIMENSION_TOTALS = {
|
|
1139
|
-
structure: 20,
|
|
1140
|
-
behaviour: 50,
|
|
1141
|
-
resilience: 20,
|
|
1142
|
-
safety: 10
|
|
1143
|
-
};
|
|
1144
|
-
function rubricIssue(code, message, path, moduleId) {
|
|
1145
|
-
return { code, message, path, ...moduleId ? { moduleId } : {} };
|
|
1146
|
-
}
|
|
1147
|
-
function validateAssessmentRubric(rubric2, path = "assessment", moduleId) {
|
|
1148
|
-
const issues = [];
|
|
1149
|
-
const criterionIds = /* @__PURE__ */ new Set();
|
|
1150
|
-
const dimensionTotals = {
|
|
1151
|
-
structure: 0,
|
|
1152
|
-
behaviour: 0,
|
|
1153
|
-
resilience: 0,
|
|
1154
|
-
safety: 0
|
|
1155
|
-
};
|
|
1156
|
-
let rubricTotal = 0;
|
|
1157
|
-
for (const criterion of rubric2.criteria) {
|
|
1158
|
-
rubricTotal += criterion.points;
|
|
1159
|
-
dimensionTotals[criterion.dimension] += criterion.points;
|
|
1160
|
-
if (criterionIds.has(criterion.id)) {
|
|
1007
|
+
const stageKinds = learner.stages.map((stage) => stage.kind);
|
|
1008
|
+
for (const requiredStage of JUNIOR_CODER_MISSION_STAGE_ORDER_V1) {
|
|
1009
|
+
const count = stageKinds.filter((stage) => stage === requiredStage).length;
|
|
1010
|
+
if (count === 0) {
|
|
1161
1011
|
issues.push(
|
|
1162
|
-
|
|
1163
|
-
"
|
|
1164
|
-
`
|
|
1165
|
-
|
|
1166
|
-
|
|
1012
|
+
authoringIssue(
|
|
1013
|
+
"missing-stage",
|
|
1014
|
+
`Mission stage ${requiredStage} is required.`,
|
|
1015
|
+
"learner.stages"
|
|
1016
|
+
)
|
|
1017
|
+
);
|
|
1018
|
+
} else if (count > 1) {
|
|
1019
|
+
issues.push(
|
|
1020
|
+
authoringIssue(
|
|
1021
|
+
"duplicate-stage",
|
|
1022
|
+
`Mission stage ${requiredStage} appears more than once.`,
|
|
1023
|
+
"learner.stages"
|
|
1167
1024
|
)
|
|
1168
1025
|
);
|
|
1169
1026
|
}
|
|
1170
|
-
criterionIds.add(criterion.id);
|
|
1171
1027
|
}
|
|
1172
|
-
if (
|
|
1028
|
+
if (stageKinds.length === JUNIOR_CODER_MISSION_STAGE_ORDER_V1.length && stageKinds.some(
|
|
1029
|
+
(stage, index) => stage !== JUNIOR_CODER_MISSION_STAGE_ORDER_V1[index]
|
|
1030
|
+
)) {
|
|
1173
1031
|
issues.push(
|
|
1174
|
-
|
|
1175
|
-
"
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
moduleId
|
|
1032
|
+
authoringIssue(
|
|
1033
|
+
"stage-order",
|
|
1034
|
+
"Mission stages must follow the canonical learner journey.",
|
|
1035
|
+
"learner.stages"
|
|
1179
1036
|
)
|
|
1180
1037
|
);
|
|
1181
1038
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
)
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1039
|
+
if (learner.readinessChecks.length === 0) {
|
|
1040
|
+
issues.push(
|
|
1041
|
+
authoringIssue(
|
|
1042
|
+
"missing-readiness-check",
|
|
1043
|
+
"At least one unscored readiness check is required.",
|
|
1044
|
+
"learner.readinessChecks"
|
|
1045
|
+
)
|
|
1046
|
+
);
|
|
1193
1047
|
}
|
|
1194
|
-
if (
|
|
1195
|
-
(criterion) => criterion.dimension === "safety" && criterion.mandatory
|
|
1196
|
-
)) {
|
|
1048
|
+
if (learner.readinessChecks.some((check) => check.scored !== false)) {
|
|
1197
1049
|
issues.push(
|
|
1198
|
-
|
|
1199
|
-
"
|
|
1200
|
-
"
|
|
1201
|
-
|
|
1202
|
-
moduleId
|
|
1203
|
-
)
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
1206
|
-
return issues;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// src/mission-authoring.ts
|
|
1210
|
-
var JUNIOR_CODER_MISSION_STAGE_ORDER_V1 = [
|
|
1211
|
-
"learn",
|
|
1212
|
-
"predict",
|
|
1213
|
-
"build",
|
|
1214
|
-
"run",
|
|
1215
|
-
"assess",
|
|
1216
|
-
"inspect",
|
|
1217
|
-
"fix",
|
|
1218
|
-
"explain",
|
|
1219
|
-
"reward"
|
|
1220
|
-
];
|
|
1221
|
-
var LEARNER_STARTER_KINDS = /* @__PURE__ */ new Set([
|
|
1222
|
-
"starter-code",
|
|
1223
|
-
"starter-assets",
|
|
1224
|
-
"sample-data"
|
|
1225
|
-
]);
|
|
1226
|
-
var LEARNER_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
|
|
1227
|
-
"facilitator-note",
|
|
1228
|
-
"answer-key",
|
|
1229
|
-
"protected-test"
|
|
1230
|
-
]);
|
|
1231
|
-
var SINGLE_MODE_REQUIRES_ALTERNATIVE = /* @__PURE__ */ new Set([
|
|
1232
|
-
"pointer",
|
|
1233
|
-
"drag",
|
|
1234
|
-
"audio",
|
|
1235
|
-
"colour",
|
|
1236
|
-
"motion"
|
|
1237
|
-
]);
|
|
1238
|
-
function authoringIssue(code, message, path) {
|
|
1239
|
-
return { code, message, path };
|
|
1240
|
-
}
|
|
1241
|
-
function reportDuplicateIds(ids, path) {
|
|
1242
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1243
|
-
const issues = [];
|
|
1244
|
-
for (const id of ids) {
|
|
1245
|
-
if (seen.has(id)) {
|
|
1246
|
-
issues.push(
|
|
1247
|
-
authoringIssue("duplicate-id", `Duplicate authored ID ${id}.`, path)
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1250
|
-
seen.add(id);
|
|
1251
|
-
}
|
|
1252
|
-
return issues;
|
|
1253
|
-
}
|
|
1254
|
-
function validateMissionAuthoringBundle(bundle, module2) {
|
|
1255
|
-
const issues = [];
|
|
1256
|
-
if (bundle.version !== MISSION_AUTHORING_CONTRACT_VERSION_V1) {
|
|
1257
|
-
issues.push(
|
|
1258
|
-
authoringIssue(
|
|
1259
|
-
"bundle-version-mismatch",
|
|
1260
|
-
`Unsupported mission authoring version ${bundle.version}.`,
|
|
1261
|
-
"version"
|
|
1262
|
-
)
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
1265
|
-
if (bundle.moduleId !== module2.id || bundle.moduleVersion !== module2.version) {
|
|
1266
|
-
issues.push(
|
|
1267
|
-
authoringIssue(
|
|
1268
|
-
"module-reference-mismatch",
|
|
1269
|
-
`Bundle ${bundle.moduleId}@${bundle.moduleVersion} does not match ${module2.id}@${module2.version}.`,
|
|
1270
|
-
"moduleId"
|
|
1271
|
-
)
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
if (!module2.missions.some((mission2) => mission2.id === bundle.missionId)) {
|
|
1275
|
-
issues.push(
|
|
1276
|
-
authoringIssue(
|
|
1277
|
-
"mission-reference-mismatch",
|
|
1278
|
-
`Mission ${bundle.missionId} does not exist in module ${module2.id}.`,
|
|
1279
|
-
"missionId"
|
|
1280
|
-
)
|
|
1281
|
-
);
|
|
1282
|
-
}
|
|
1283
|
-
const learner = bundle.learner;
|
|
1284
|
-
const facilitator = bundle.facilitator;
|
|
1285
|
-
if (learner.estimatedMinutes < 15 || learner.estimatedMinutes > 25) {
|
|
1286
|
-
issues.push(
|
|
1287
|
-
authoringIssue(
|
|
1288
|
-
"invalid-duration",
|
|
1289
|
-
"A mission must last between 15 and 25 minutes.",
|
|
1290
|
-
"learner.estimatedMinutes"
|
|
1291
|
-
)
|
|
1292
|
-
);
|
|
1293
|
-
}
|
|
1294
|
-
const stageKinds = learner.stages.map((stage) => stage.kind);
|
|
1295
|
-
for (const requiredStage of JUNIOR_CODER_MISSION_STAGE_ORDER_V1) {
|
|
1296
|
-
const count = stageKinds.filter((stage) => stage === requiredStage).length;
|
|
1297
|
-
if (count === 0) {
|
|
1298
|
-
issues.push(
|
|
1299
|
-
authoringIssue(
|
|
1300
|
-
"missing-stage",
|
|
1301
|
-
`Mission stage ${requiredStage} is required.`,
|
|
1302
|
-
"learner.stages"
|
|
1303
|
-
)
|
|
1304
|
-
);
|
|
1305
|
-
} else if (count > 1) {
|
|
1306
|
-
issues.push(
|
|
1307
|
-
authoringIssue(
|
|
1308
|
-
"duplicate-stage",
|
|
1309
|
-
`Mission stage ${requiredStage} appears more than once.`,
|
|
1310
|
-
"learner.stages"
|
|
1311
|
-
)
|
|
1312
|
-
);
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
if (stageKinds.length === JUNIOR_CODER_MISSION_STAGE_ORDER_V1.length && stageKinds.some(
|
|
1316
|
-
(stage, index) => stage !== JUNIOR_CODER_MISSION_STAGE_ORDER_V1[index]
|
|
1317
|
-
)) {
|
|
1318
|
-
issues.push(
|
|
1319
|
-
authoringIssue(
|
|
1320
|
-
"stage-order",
|
|
1321
|
-
"Mission stages must follow the canonical learner journey.",
|
|
1322
|
-
"learner.stages"
|
|
1323
|
-
)
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1326
|
-
if (learner.readinessChecks.length === 0) {
|
|
1327
|
-
issues.push(
|
|
1328
|
-
authoringIssue(
|
|
1329
|
-
"missing-readiness-check",
|
|
1330
|
-
"At least one unscored readiness check is required.",
|
|
1331
|
-
"learner.readinessChecks"
|
|
1332
|
-
)
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
if (learner.readinessChecks.some((check) => check.scored !== false)) {
|
|
1336
|
-
issues.push(
|
|
1337
|
-
authoringIssue(
|
|
1338
|
-
"scored-readiness-check",
|
|
1339
|
-
"Readiness checks must not affect the deterministic score.",
|
|
1340
|
-
"learner.readinessChecks"
|
|
1050
|
+
authoringIssue(
|
|
1051
|
+
"scored-readiness-check",
|
|
1052
|
+
"Readiness checks must not affect the deterministic score.",
|
|
1053
|
+
"learner.readinessChecks"
|
|
1341
1054
|
)
|
|
1342
1055
|
);
|
|
1343
1056
|
}
|
|
@@ -7034,6 +6747,526 @@ var ROAD_HOPPER_RALLY_MISSION_ONE_AUTHORING_V1 = {
|
|
|
7034
6747
|
}
|
|
7035
6748
|
};
|
|
7036
6749
|
|
|
6750
|
+
// src/course-contracts.ts
|
|
6751
|
+
var LEARNING_COURSE_STAGE_ORDER = JUNIOR_CODER_MISSION_STAGE_ORDER_V1;
|
|
6752
|
+
var LEARNING_COURSE_LIMITS = Object.freeze({
|
|
6753
|
+
missions: 6,
|
|
6754
|
+
stagesPerMission: 9,
|
|
6755
|
+
projectFiles: 8,
|
|
6756
|
+
sourceCharactersPerFile: 64e3,
|
|
6757
|
+
sourceCharactersPerProject: 96e3
|
|
6758
|
+
});
|
|
6759
|
+
var ID = /^[a-z0-9][a-z0-9.-]{0,159}$/u;
|
|
6760
|
+
var FILE_PATH = /^[a-z0-9][a-z0-9_-]{0,63}\.(?:js|py|cpp|html|css|json)$/u;
|
|
6761
|
+
var VERSION = /^\d+\.\d+\.\d+$/u;
|
|
6762
|
+
var LANGUAGES = ["javascript", "python", "cpp", "html", "css", "blocks", "json"];
|
|
6763
|
+
var CATEGORIES = ["game", "robot", "vibe", "web-app"];
|
|
6764
|
+
var PLACEHOLDER = /\b(?:TODO|TBD|coming soon|placeholder|lorem ipsum)\b/iu;
|
|
6765
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6766
|
+
var text = (value, minimum = 1, maximum = 8e3) => typeof value === "string" && value.trim().length >= minimum && value.length <= maximum;
|
|
6767
|
+
var id = (value) => typeof value === "string" && ID.test(value);
|
|
6768
|
+
var integer = (value, minimum, maximum) => typeof value === "number" && Number.isSafeInteger(value) && value >= minimum && value <= maximum;
|
|
6769
|
+
var exactKeys = (value, keys2) => Object.keys(value).length === keys2.length && keys2.every((key) => Object.hasOwn(value, key));
|
|
6770
|
+
var LearningCourseInputError = class extends Error {
|
|
6771
|
+
constructor() {
|
|
6772
|
+
super("Invalid learning course input.");
|
|
6773
|
+
this.name = "LearningCourseInputError";
|
|
6774
|
+
}
|
|
6775
|
+
};
|
|
6776
|
+
function fileDefinitions(value) {
|
|
6777
|
+
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;
|
|
6778
|
+
}
|
|
6779
|
+
function parseLearningProject(course, value) {
|
|
6780
|
+
if (!fileDefinitions(course.projectFiles) || !record(value) || !exactKeys(value, ["files"]) || !Array.isArray(value.files) || value.files.length !== course.projectFiles.length) throw new LearningCourseInputError();
|
|
6781
|
+
const files = [];
|
|
6782
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6783
|
+
let characters = 0;
|
|
6784
|
+
for (const file of value.files) {
|
|
6785
|
+
if (!record(file) || !exactKeys(file, ["path", "source"]) || typeof file.path !== "string" || typeof file.source !== "string" || seen.has(file.path)) throw new LearningCourseInputError();
|
|
6786
|
+
const definition = course.projectFiles.find((candidate) => candidate.path === file.path);
|
|
6787
|
+
if (!definition || file.source.length > definition.maximumCharacters || file.source.includes("\0")) throw new LearningCourseInputError();
|
|
6788
|
+
characters += file.source.length;
|
|
6789
|
+
if (characters > LEARNING_COURSE_LIMITS.sourceCharactersPerProject) throw new LearningCourseInputError();
|
|
6790
|
+
seen.add(file.path);
|
|
6791
|
+
files.push({ path: file.path, source: file.source });
|
|
6792
|
+
}
|
|
6793
|
+
return { files: course.projectFiles.map((definition) => files.find((file) => file.path === definition.path)) };
|
|
6794
|
+
}
|
|
6795
|
+
function parseLearningCourseDraft(course, value) {
|
|
6796
|
+
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();
|
|
6797
|
+
return {
|
|
6798
|
+
schemaVersion: "1",
|
|
6799
|
+
moduleVersion: course.moduleVersion,
|
|
6800
|
+
activeStageId: value.activeStageId,
|
|
6801
|
+
project: parseLearningProject(course, value.project)
|
|
6802
|
+
};
|
|
6803
|
+
}
|
|
6804
|
+
function parseLearningSaveSlotId(value) {
|
|
6805
|
+
if (value === "auto" || typeof value === "string" && /^[1-9]$/u.test(value)) return value;
|
|
6806
|
+
throw new LearningCourseInputError();
|
|
6807
|
+
}
|
|
6808
|
+
function validateLearningCourse(value) {
|
|
6809
|
+
const issues = [];
|
|
6810
|
+
const add = (code, path) => {
|
|
6811
|
+
issues.push({ code, path });
|
|
6812
|
+
};
|
|
6813
|
+
if (!record(value)) return [{ code: "invalid-manifest", path: "$" }];
|
|
6814
|
+
if (!exactKeys(value, [
|
|
6815
|
+
"schemaVersion",
|
|
6816
|
+
"moduleId",
|
|
6817
|
+
"moduleVersion",
|
|
6818
|
+
"slug",
|
|
6819
|
+
"title",
|
|
6820
|
+
"summary",
|
|
6821
|
+
"runtimeId",
|
|
6822
|
+
"category",
|
|
6823
|
+
"estimatedMinutes",
|
|
6824
|
+
"completionAssessmentId",
|
|
6825
|
+
"projectFiles",
|
|
6826
|
+
"starterProject",
|
|
6827
|
+
"reference",
|
|
6828
|
+
"missions",
|
|
6829
|
+
"completionBadge"
|
|
6830
|
+
]) || 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", "$");
|
|
6831
|
+
if (!fileDefinitions(value.projectFiles)) add("invalid-project", "projectFiles");
|
|
6832
|
+
else {
|
|
6833
|
+
try {
|
|
6834
|
+
parseLearningProject({ projectFiles: value.projectFiles }, value.starterProject);
|
|
6835
|
+
} catch {
|
|
6836
|
+
add("invalid-project", "starterProject");
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
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");
|
|
6840
|
+
if (!Array.isArray(value.missions)) {
|
|
6841
|
+
add("mission-count", "missions");
|
|
6842
|
+
return issues;
|
|
6843
|
+
}
|
|
6844
|
+
if (value.missions.length !== LEARNING_COURSE_LIMITS.missions) add("mission-count", "missions");
|
|
6845
|
+
if (value.missions.length > LEARNING_COURSE_LIMITS.missions) return issues;
|
|
6846
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6847
|
+
const checkId = (candidate, path) => {
|
|
6848
|
+
if (!id(candidate)) add("invalid-manifest", path);
|
|
6849
|
+
else if (seen.has(candidate)) add("duplicate-id", path);
|
|
6850
|
+
else seen.add(candidate);
|
|
6851
|
+
};
|
|
6852
|
+
let minutes = 0;
|
|
6853
|
+
value.missions.forEach((mission2, missionIndex) => {
|
|
6854
|
+
const path = `missions[${missionIndex}]`;
|
|
6855
|
+
if (!record(mission2)) {
|
|
6856
|
+
add("invalid-manifest", path);
|
|
6857
|
+
return;
|
|
6858
|
+
}
|
|
6859
|
+
checkId(mission2.id, `${path}.id`);
|
|
6860
|
+
checkId(mission2.assessmentId, `${path}.assessmentId`);
|
|
6861
|
+
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);
|
|
6862
|
+
if (typeof mission2.estimatedMinutes === "number") minutes += mission2.estimatedMinutes;
|
|
6863
|
+
if (!Array.isArray(mission2.stages) || mission2.stages.length !== LEARNING_COURSE_LIMITS.stagesPerMission) {
|
|
6864
|
+
add("stage-order", `${path}.stages`);
|
|
6865
|
+
return;
|
|
6866
|
+
}
|
|
6867
|
+
mission2.stages.forEach((stage, stageIndex) => {
|
|
6868
|
+
const stagePath = `${path}.stages[${stageIndex}]`;
|
|
6869
|
+
if (!record(stage)) {
|
|
6870
|
+
add("invalid-manifest", stagePath);
|
|
6871
|
+
return;
|
|
6872
|
+
}
|
|
6873
|
+
checkId(stage.id, `${stagePath}.id`);
|
|
6874
|
+
if (stage.kind !== LEARNING_COURSE_STAGE_ORDER[stageIndex]) add("stage-order", stagePath);
|
|
6875
|
+
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);
|
|
6876
|
+
});
|
|
6877
|
+
});
|
|
6878
|
+
if (minutes !== value.estimatedMinutes) add("invalid-manifest", "estimatedMinutes");
|
|
6879
|
+
return issues;
|
|
6880
|
+
}
|
|
6881
|
+
function parseLearningCourse(value) {
|
|
6882
|
+
if (validateLearningCourse(value).length) throw new LearningCourseInputError();
|
|
6883
|
+
return structuredClone(value);
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
// src/course-progress.ts
|
|
6887
|
+
var DIGEST = /^[a-f0-9]{64}$/u;
|
|
6888
|
+
var REFERENCE = /^[a-z0-9][a-z0-9.-]{0,159}$/u;
|
|
6889
|
+
var stages = (course) => course.missions.flatMap((mission2) => mission2.stages);
|
|
6890
|
+
var record2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6891
|
+
var keys = (value, expected) => Object.keys(value).length === expected.length && expected.every((key) => Object.hasOwn(value, key));
|
|
6892
|
+
var identifier = (value) => typeof value === "string" && REFERENCE.test(value);
|
|
6893
|
+
var digest = (value) => typeof value === "string" && DIGEST.test(value);
|
|
6894
|
+
function validResult(value) {
|
|
6895
|
+
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;
|
|
6896
|
+
const arrays = [value.passedCriterionIds, value.failedCriterionIds, value.failedMandatoryCriterionIds];
|
|
6897
|
+
if (arrays.some((items) => !Array.isArray(items) || items.length > 64 || !items.every(identifier) || new Set(items).size !== items.length)) return false;
|
|
6898
|
+
const passed = value.passedCriterionIds;
|
|
6899
|
+
const failed = value.failedCriterionIds;
|
|
6900
|
+
const mandatory = value.failedMandatoryCriterionIds;
|
|
6901
|
+
const band = value.score >= 95 ? "mastered" : value.score >= 80 ? "mission-complete" : value.score >= 60 ? "nearly-there" : "keep-exploring";
|
|
6902
|
+
return value.band === band && value.completed === (value.score >= 80 && mandatory.length === 0) && !passed.some((id2) => failed.includes(id2)) && mandatory.every((id2) => failed.includes(id2));
|
|
6903
|
+
}
|
|
6904
|
+
function assertProgress(course, progress) {
|
|
6905
|
+
const ordered = stages(course);
|
|
6906
|
+
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) => {
|
|
6907
|
+
if (!record2(item) || !keys(item, ["missionId", "assessmentId", "referenceId", "sourceDigest", "result"]) || !identifier(item.referenceId) || !digest(item.sourceDigest) || !validResult(item.result)) return true;
|
|
6908
|
+
const mission2 = course.missions.find((candidate) => candidate.id === item.missionId);
|
|
6909
|
+
return item.missionId === "final" ? progress.completedMissionIds.length !== 6 || item.assessmentId !== course.completionAssessmentId : !mission2 || item.assessmentId !== mission2.assessmentId;
|
|
6910
|
+
}) || 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();
|
|
6911
|
+
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();
|
|
6912
|
+
}
|
|
6913
|
+
function parseLearningCourseProgress(course, value) {
|
|
6914
|
+
if (!record2(value) || !keys(value, ["schemaVersion", "moduleId", "moduleVersion", "completedStageIds", "completedMissionIds", "assessments", "completion"])) throw new LearningCourseInputError();
|
|
6915
|
+
assertProgress(course, value);
|
|
6916
|
+
return structuredClone(value);
|
|
6917
|
+
}
|
|
6918
|
+
function createLearningCourseProgress(course) {
|
|
6919
|
+
if (validateLearningCourse(course).length) throw new LearningCourseInputError();
|
|
6920
|
+
return {
|
|
6921
|
+
schemaVersion: "1",
|
|
6922
|
+
moduleId: course.moduleId,
|
|
6923
|
+
moduleVersion: course.moduleVersion,
|
|
6924
|
+
completedStageIds: [],
|
|
6925
|
+
completedMissionIds: [],
|
|
6926
|
+
assessments: [],
|
|
6927
|
+
completion: null
|
|
6928
|
+
};
|
|
6929
|
+
}
|
|
6930
|
+
function resolveLearningCourseStage(course, progress, requestedId) {
|
|
6931
|
+
assertProgress(course, progress);
|
|
6932
|
+
const ordered = stages(course);
|
|
6933
|
+
const maximum = Math.min(progress.completedStageIds.length, ordered.length - 1);
|
|
6934
|
+
const requestedIndex = ordered.findIndex((stage) => stage.id === requestedId);
|
|
6935
|
+
return requestedIndex >= 0 && requestedIndex <= maximum ? requestedId : ordered[maximum].id;
|
|
6936
|
+
}
|
|
6937
|
+
function recordLearningCourseAssessment(course, progress, authority) {
|
|
6938
|
+
assertProgress(course, progress);
|
|
6939
|
+
const final = authority.missionId === "final";
|
|
6940
|
+
const missionIndex = course.missions.findIndex((mission2) => mission2.id === authority.missionId);
|
|
6941
|
+
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();
|
|
6942
|
+
let result;
|
|
6943
|
+
try {
|
|
6944
|
+
result = calculateAssessment(authority.rubric, authority.checks);
|
|
6945
|
+
} catch {
|
|
6946
|
+
throw new LearningCourseInputError();
|
|
6947
|
+
}
|
|
6948
|
+
const assessment = {
|
|
6949
|
+
missionId: authority.missionId,
|
|
6950
|
+
assessmentId: authority.assessmentId,
|
|
6951
|
+
referenceId: authority.referenceId,
|
|
6952
|
+
sourceDigest: authority.sourceDigest,
|
|
6953
|
+
result
|
|
6954
|
+
};
|
|
6955
|
+
const duplicate = progress.assessments.find((item) => item.referenceId === authority.referenceId);
|
|
6956
|
+
if (duplicate && JSON.stringify(duplicate) !== JSON.stringify(assessment)) throw new LearningCourseInputError();
|
|
6957
|
+
const next = structuredClone(progress);
|
|
6958
|
+
next.assessments = [...next.assessments.filter((item) => item.missionId !== authority.missionId), assessment];
|
|
6959
|
+
if (final && result.completed) {
|
|
6960
|
+
next.completion = next.completion ? { ...next.completion, bestScore: Math.max(next.completion.bestScore, result.score) } : { referenceId: authority.referenceId, sourceDigest: authority.sourceDigest, bestScore: result.score };
|
|
6961
|
+
}
|
|
6962
|
+
return next;
|
|
6963
|
+
}
|
|
6964
|
+
function completeVerifiedLearningActivity(course, progress, proof) {
|
|
6965
|
+
assertProgress(course, progress);
|
|
6966
|
+
if (proof.accepted !== true || !DIGEST.test(proof.sourceDigest)) throw new LearningCourseInputError();
|
|
6967
|
+
const ordered = stages(course);
|
|
6968
|
+
const index = ordered.findIndex((stage2) => stage2.id === proof.stageId);
|
|
6969
|
+
if (index < 0 || index > progress.completedStageIds.length) throw new LearningCourseInputError();
|
|
6970
|
+
if (index < progress.completedStageIds.length) return structuredClone(progress);
|
|
6971
|
+
const stage = ordered[index];
|
|
6972
|
+
const mission2 = course.missions[Math.floor(index / 9)];
|
|
6973
|
+
if (["assess", "inspect", "fix", "reward"].includes(stage.kind)) {
|
|
6974
|
+
const assessment = progress.assessments.find((item) => item.missionId === mission2.id);
|
|
6975
|
+
if (!assessment || assessment.sourceDigest !== proof.sourceDigest || (stage.kind === "fix" || stage.kind === "reward") && !assessment.result.completed) throw new LearningCourseInputError();
|
|
6976
|
+
}
|
|
6977
|
+
const next = structuredClone(progress);
|
|
6978
|
+
next.completedStageIds.push(proof.stageId);
|
|
6979
|
+
if (stage.kind === "reward") next.completedMissionIds.push(mission2.id);
|
|
6980
|
+
return next;
|
|
6981
|
+
}
|
|
6982
|
+
|
|
6983
|
+
// src/contextual-help.ts
|
|
6984
|
+
var CONTEXTUAL_HELP_CONTRACT_VERSION_V1 = "1.0.0";
|
|
6985
|
+
var CONTEXTUAL_HELP_KINDS_V1 = Object.freeze([
|
|
6986
|
+
"command",
|
|
6987
|
+
"visual-block",
|
|
6988
|
+
"generated-code",
|
|
6989
|
+
"assessment-diagnostic"
|
|
6990
|
+
]);
|
|
6991
|
+
var CONTEXTUAL_VOICE_INTENTS_V1 = Object.freeze([
|
|
6992
|
+
"describe-command",
|
|
6993
|
+
"describe-inputs",
|
|
6994
|
+
"show-example",
|
|
6995
|
+
"explain-assessment-failure",
|
|
6996
|
+
"suggest-next-experiment",
|
|
6997
|
+
"repeat",
|
|
6998
|
+
"stop",
|
|
6999
|
+
"unresolved"
|
|
7000
|
+
]);
|
|
7001
|
+
var CONTEXTUAL_VOICE_SUGGESTED_ACTIONS_V1 = Object.freeze([
|
|
7002
|
+
"open-help",
|
|
7003
|
+
"insert-example",
|
|
7004
|
+
"show-inputs",
|
|
7005
|
+
"show-assessment-evidence",
|
|
7006
|
+
"try-next-experiment",
|
|
7007
|
+
"repeat",
|
|
7008
|
+
"stop",
|
|
7009
|
+
"type-question"
|
|
7010
|
+
]);
|
|
7011
|
+
var OPAQUE_ID = /^[a-z0-9]+(?:[._:-][a-z0-9]+)*$/u;
|
|
7012
|
+
var SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u;
|
|
7013
|
+
var SHA_256 = /^sha256:[a-f0-9]{64}$/u;
|
|
7014
|
+
var LOCALE = /^[a-z]{2,3}(?:-[A-Z]{2})?$/u;
|
|
7015
|
+
var MEDIA_TYPE = /^audio\/[a-z0-9.+-]+$/u;
|
|
7016
|
+
function containsDisallowedControlCharacter(value) {
|
|
7017
|
+
return [...value].some((character) => {
|
|
7018
|
+
const code = character.codePointAt(0) ?? 0;
|
|
7019
|
+
return code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127;
|
|
7020
|
+
});
|
|
7021
|
+
}
|
|
7022
|
+
function requireOpaqueId(value, label, maximum = 160) {
|
|
7023
|
+
if (value.length === 0 || value.length > maximum || !OPAQUE_ID.test(value)) {
|
|
7024
|
+
throw new Error(`${label} must be a bounded opaque identifier.`);
|
|
7025
|
+
}
|
|
7026
|
+
}
|
|
7027
|
+
function requireSemver(value, label) {
|
|
7028
|
+
if (!SEMVER.test(value)) {
|
|
7029
|
+
throw new Error(`${label} must be an immutable semantic version.`);
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
function requireIsoTimestamp(value, label) {
|
|
7033
|
+
if (!/^\d{4}-\d{2}-\d{2}T/u.test(value) || !Number.isFinite(Date.parse(value))) {
|
|
7034
|
+
throw new Error(`${label} must be an ISO-8601 timestamp.`);
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
function assertValidContextualHelpIdentifier(value) {
|
|
7038
|
+
if (value.schemaVersion !== "1") {
|
|
7039
|
+
throw new Error("schemaVersion must be 1.");
|
|
7040
|
+
}
|
|
7041
|
+
if (value.contractVersion !== CONTEXTUAL_HELP_CONTRACT_VERSION_V1) {
|
|
7042
|
+
throw new Error("contractVersion is unsupported.");
|
|
7043
|
+
}
|
|
7044
|
+
if (!CONTEXTUAL_HELP_KINDS_V1.includes(value.kind)) {
|
|
7045
|
+
throw new Error("kind is unsupported.");
|
|
7046
|
+
}
|
|
7047
|
+
requireOpaqueId(value.moduleId, "moduleId");
|
|
7048
|
+
requireSemver(value.moduleVersion, "moduleVersion");
|
|
7049
|
+
requireSemver(value.manifestVersion, "manifestVersion");
|
|
7050
|
+
requireOpaqueId(value.helpId, "helpId");
|
|
7051
|
+
}
|
|
7052
|
+
function assertValidGuardianVoiceConsent(value) {
|
|
7053
|
+
if (value.schemaVersion !== "1") {
|
|
7054
|
+
throw new Error("schemaVersion must be 1.");
|
|
7055
|
+
}
|
|
7056
|
+
requireOpaqueId(value.actorAccountId, "actorAccountId");
|
|
7057
|
+
requireOpaqueId(value.subjectAccountId, "subjectAccountId");
|
|
7058
|
+
requireSemver(value.policyVersion, "policyVersion");
|
|
7059
|
+
if (value.state !== "granted" && value.state !== "withdrawn") {
|
|
7060
|
+
throw new Error("state must be granted or withdrawn.");
|
|
7061
|
+
}
|
|
7062
|
+
if (value.permittedProcessingRoute !== "private-edge-only") {
|
|
7063
|
+
throw new Error("permittedProcessingRoute must be private-edge-only.");
|
|
7064
|
+
}
|
|
7065
|
+
requireIsoTimestamp(value.recordedAt, "recordedAt");
|
|
7066
|
+
}
|
|
7067
|
+
function assertValidContextualVoiceQuestionMetadata(value) {
|
|
7068
|
+
assertValidContextualHelpIdentifier(value.help);
|
|
7069
|
+
if (!LOCALE.test(value.locale)) throw new Error("locale is invalid.");
|
|
7070
|
+
if (!MEDIA_TYPE.test(value.mediaType)) throw new Error("mediaType is invalid.");
|
|
7071
|
+
if (!Number.isInteger(value.durationMs) || value.durationMs < 1 || value.durationMs > 1e4) {
|
|
7072
|
+
throw new Error("durationMs must be between 1 and 10000.");
|
|
7073
|
+
}
|
|
7074
|
+
if (!Number.isInteger(value.audioBytes) || value.audioBytes < 1 || value.audioBytes > 2097152) {
|
|
7075
|
+
throw new Error("audioBytes must be between 1 and 2097152.");
|
|
7076
|
+
}
|
|
7077
|
+
if (value.assessmentEvidenceId !== void 0) {
|
|
7078
|
+
requireOpaqueId(value.assessmentEvidenceId, "assessmentEvidenceId");
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
function assertValidVoiceHelpAvailability(value) {
|
|
7082
|
+
if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
|
|
7083
|
+
if (value.questionProcessingRoute !== "private-edge-only") {
|
|
7084
|
+
throw new Error("questionProcessingRoute must be private-edge-only.");
|
|
7085
|
+
}
|
|
7086
|
+
if (value.guardianVoiceConsentRequired !== true) {
|
|
7087
|
+
throw new Error("guardianVoiceConsentRequired must remain true.");
|
|
7088
|
+
}
|
|
7089
|
+
if (value.maximumRecordingDurationMs !== 1e4 || value.maximumAudioBytes !== 2097152 || value.rawAudioRetention !== "request-memory-only" || value.transcriptRetention !== "request-memory-only") {
|
|
7090
|
+
throw new Error("Voice limits and transient retention are immutable in version one.");
|
|
7091
|
+
}
|
|
7092
|
+
if (value.microphoneAvailable !== (value.microphoneReason === "available")) {
|
|
7093
|
+
throw new Error("microphoneReason must match microphone availability.");
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
function assertValidCanonicalSpokenHelpDescriptor(value) {
|
|
7097
|
+
if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
|
|
7098
|
+
assertValidContextualHelpIdentifier(value.help);
|
|
7099
|
+
requireOpaqueId(value.canonicalTextId, "canonicalTextId");
|
|
7100
|
+
if (!SHA_256.test(value.authoritativeTextDigest)) {
|
|
7101
|
+
throw new Error("authoritativeTextDigest must be a SHA-256 digest.");
|
|
7102
|
+
}
|
|
7103
|
+
if (!LOCALE.test(value.locale)) throw new Error("locale is invalid.");
|
|
7104
|
+
requireOpaqueId(value.voiceProfile, "voiceProfile");
|
|
7105
|
+
requireSemver(value.pronunciationVersion, "pronunciationVersion");
|
|
7106
|
+
if (value.utteranceClass !== "system-generic" || value.sharingScope !== "global" || value.reuse !== "exact-only" || value.containsPersonalData !== false || value.containsLearnerContent !== false) {
|
|
7107
|
+
throw new Error("Canonical spoken help must use global system-generic exact-only reuse.");
|
|
7108
|
+
}
|
|
7109
|
+
}
|
|
7110
|
+
function assertValidContextualVoiceQuestionResult(value) {
|
|
7111
|
+
if (value.schemaVersion !== "1") throw new Error("schemaVersion must be 1.");
|
|
7112
|
+
assertValidContextualHelpIdentifier(value.help);
|
|
7113
|
+
if (!CONTEXTUAL_VOICE_INTENTS_V1.includes(value.intent)) {
|
|
7114
|
+
throw new Error("intent is unsupported.");
|
|
7115
|
+
}
|
|
7116
|
+
if (value.transcriptRetention !== "request-memory-only") {
|
|
7117
|
+
throw new Error("transcriptRetention must be request-memory-only.");
|
|
7118
|
+
}
|
|
7119
|
+
if (value.status !== "resolved" && value.status !== "suggestions" && value.status !== "stopped") {
|
|
7120
|
+
throw new Error("status is unsupported.");
|
|
7121
|
+
}
|
|
7122
|
+
if (value.transientTranscript.length > 500 || containsDisallowedControlCharacter(value.transientTranscript)) {
|
|
7123
|
+
throw new Error("transientTranscript is invalid.");
|
|
7124
|
+
}
|
|
7125
|
+
if (value.answer) {
|
|
7126
|
+
requireOpaqueId(value.answer.helpId, "answer.helpId");
|
|
7127
|
+
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)) {
|
|
7128
|
+
throw new Error("answer.text is invalid.");
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
if (value.suggestedActions.some(
|
|
7132
|
+
(action) => !CONTEXTUAL_VOICE_SUGGESTED_ACTIONS_V1.includes(action)
|
|
7133
|
+
)) {
|
|
7134
|
+
throw new Error("suggestedActions contains an unsupported action.");
|
|
7135
|
+
}
|
|
7136
|
+
if (value.mayAssignScore !== false || value.mayAwardReward !== false || value.mayPublish !== false || value.mayControlHardware !== false) {
|
|
7137
|
+
throw new Error("mayAssignScore, reward, publish and hardware authority must remain false.");
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7140
|
+
|
|
7141
|
+
// src/publishing.ts
|
|
7142
|
+
var STATIC_PROJECT_PUBLISHING_CONTRACT_VERSION_V1 = "1.0.0";
|
|
7143
|
+
var STATIC_PROJECT_SCANNER_VERSION_V1 = "junior-coder-static-scan-v1";
|
|
7144
|
+
var STATIC_PROJECT_APPROVAL_STATEMENT_VERSION_V1 = "guardian-static-publication-v1";
|
|
7145
|
+
var STATIC_PROJECT_DEFAULT_LIFETIME_DAYS_V1 = 90;
|
|
7146
|
+
var STATIC_PROJECT_MAX_SOURCE_CHARACTERS_V1 = 8e3;
|
|
7147
|
+
var STATIC_PROJECT_SAFETY_CHECK_IDS_V1 = [
|
|
7148
|
+
"allow-listed-source",
|
|
7149
|
+
"no-personal-details",
|
|
7150
|
+
"no-external-network",
|
|
7151
|
+
"no-executable-markup",
|
|
7152
|
+
"no-transmitting-forms",
|
|
7153
|
+
"no-uploads-or-embeds",
|
|
7154
|
+
"no-trackers-or-advertising",
|
|
7155
|
+
"no-account-identifiers"
|
|
7156
|
+
];
|
|
7157
|
+
var DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
7158
|
+
var IDENTIFIER_PATTERN = /^(?:account|approval|publication|snapshot)_[A-Za-z0-9_-]{20,128}$/u;
|
|
7159
|
+
var VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/u;
|
|
7160
|
+
var RANDOM_SLUG_PATTERN = /^[A-Za-z0-9_-]{40,128}$/u;
|
|
7161
|
+
function validDate(value) {
|
|
7162
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
7163
|
+
}
|
|
7164
|
+
function allSafetyChecksPresent(scan) {
|
|
7165
|
+
if (!Array.isArray(scan.checks) || scan.checks.length !== STATIC_PROJECT_SAFETY_CHECK_IDS_V1.length) {
|
|
7166
|
+
return false;
|
|
7167
|
+
}
|
|
7168
|
+
const supplied = new Set(scan.checks.map((check) => check.id));
|
|
7169
|
+
return STATIC_PROJECT_SAFETY_CHECK_IDS_V1.every((id2) => supplied.has(id2)) && scan.checks.every((check) => check.passed === true);
|
|
7170
|
+
}
|
|
7171
|
+
function safeAdventureRender(value) {
|
|
7172
|
+
const headings = ["Moonbase Missions", "Forest Rescue Plans", "Ocean Quest Board"];
|
|
7173
|
+
const titles = ["Find the moon crystal", "Guide the lost firefly", "Map the coral garden"];
|
|
7174
|
+
const days = ["Saturday", "Sunday", "School holiday"];
|
|
7175
|
+
const messages = ["Choose a day", "Add a mission title", "Mission ready!"];
|
|
7176
|
+
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;
|
|
7177
|
+
}
|
|
7178
|
+
function safeCreatureRender(value) {
|
|
7179
|
+
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;
|
|
7180
|
+
}
|
|
7181
|
+
function safeRobotRender(value) {
|
|
7182
|
+
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);
|
|
7183
|
+
}
|
|
7184
|
+
function validateStaticProjectSnapshot(value) {
|
|
7185
|
+
const issues = [];
|
|
7186
|
+
if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
|
|
7187
|
+
if (value.contractVersion !== STATIC_PROJECT_PUBLISHING_CONTRACT_VERSION_V1) {
|
|
7188
|
+
issues.push("invalid-contract-version");
|
|
7189
|
+
}
|
|
7190
|
+
if (!IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.subjectAccountId)) {
|
|
7191
|
+
issues.push("invalid-identifier");
|
|
7192
|
+
}
|
|
7193
|
+
if (!value.moduleId.startsWith("junior-coder.") || value.moduleId !== `junior-coder.${value.moduleSlug}`) {
|
|
7194
|
+
issues.push("invalid-module");
|
|
7195
|
+
}
|
|
7196
|
+
if (!VERSION_PATTERN.test(value.moduleVersion)) issues.push("invalid-version");
|
|
7197
|
+
if (!DIGEST_PATTERN.test(value.sourceDigest)) issues.push("invalid-source-digest");
|
|
7198
|
+
if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
|
|
7199
|
+
if (!Number.isInteger(value.sourceCharacterCount) || value.sourceCharacterCount < 1 || value.sourceCharacterCount > STATIC_PROJECT_MAX_SOURCE_CHARACTERS_V1) {
|
|
7200
|
+
issues.push("invalid-source-size");
|
|
7201
|
+
}
|
|
7202
|
+
if (!Number.isInteger(value.assessmentScore) || value.assessmentScore < 80 || value.assessmentScore > 100) {
|
|
7203
|
+
issues.push("assessment-score-incomplete");
|
|
7204
|
+
}
|
|
7205
|
+
if (value.mandatorySafetyPassed !== true) issues.push("mandatory-safety-missing");
|
|
7206
|
+
if (value.scan?.passed !== true || value.scan?.scannerVersion !== STATIC_PROJECT_SCANNER_VERSION_V1) {
|
|
7207
|
+
issues.push("scan-not-passed");
|
|
7208
|
+
}
|
|
7209
|
+
if (!value.scan || !allSafetyChecksPresent(value.scan)) issues.push("scan-checks-incomplete");
|
|
7210
|
+
if (value.moduleSlug !== value.renderModel?.kind) issues.push("module-render-model-mismatch");
|
|
7211
|
+
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;
|
|
7212
|
+
if (!renderSafe) issues.push("unsafe-render-model");
|
|
7213
|
+
if (value.state !== "pending-review" || !validDate(value.createdAt)) issues.push("invalid-created-at");
|
|
7214
|
+
return [...new Set(issues)];
|
|
7215
|
+
}
|
|
7216
|
+
function assertValidStaticProjectSnapshot(value) {
|
|
7217
|
+
const issues = validateStaticProjectSnapshot(value);
|
|
7218
|
+
if (issues.length > 0) throw new Error(`Invalid static project snapshot: ${issues.join(", ")}`);
|
|
7219
|
+
}
|
|
7220
|
+
function validateStaticProjectGuardianApproval(value) {
|
|
7221
|
+
const issues = [];
|
|
7222
|
+
if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
|
|
7223
|
+
if (!IDENTIFIER_PATTERN.test(value.approvalId) || !IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.guardianActorAccountId)) {
|
|
7224
|
+
issues.push("invalid-identifier");
|
|
7225
|
+
}
|
|
7226
|
+
if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
|
|
7227
|
+
if (value.statementVersion !== STATIC_PROJECT_APPROVAL_STATEMENT_VERSION_V1) {
|
|
7228
|
+
issues.push("invalid-approval-statement");
|
|
7229
|
+
}
|
|
7230
|
+
if (!validDate(value.approvedAt)) issues.push("invalid-approved-at");
|
|
7231
|
+
return [...new Set(issues)];
|
|
7232
|
+
}
|
|
7233
|
+
function assertValidStaticProjectGuardianApproval(value) {
|
|
7234
|
+
const issues = validateStaticProjectGuardianApproval(value);
|
|
7235
|
+
if (issues.length > 0) throw new Error(`Invalid static project approval: ${issues.join(", ")}`);
|
|
7236
|
+
}
|
|
7237
|
+
function validateStaticProjectPublication(value) {
|
|
7238
|
+
const issues = [];
|
|
7239
|
+
if (value.schemaVersion !== "1") issues.push("invalid-schema-version");
|
|
7240
|
+
if (!IDENTIFIER_PATTERN.test(value.publicationId) || !IDENTIFIER_PATTERN.test(value.snapshotId) || !IDENTIFIER_PATTERN.test(value.approvalId)) {
|
|
7241
|
+
issues.push("invalid-identifier");
|
|
7242
|
+
}
|
|
7243
|
+
if (!DIGEST_PATTERN.test(value.snapshotDigest)) issues.push("invalid-snapshot-digest");
|
|
7244
|
+
if (!RANDOM_SLUG_PATTERN.test(value.randomSlug)) issues.push("invalid-random-slug");
|
|
7245
|
+
let parsedUrl;
|
|
7246
|
+
try {
|
|
7247
|
+
parsedUrl = new URL(value.publicUrl);
|
|
7248
|
+
} catch {
|
|
7249
|
+
parsedUrl = void 0;
|
|
7250
|
+
}
|
|
7251
|
+
if (!parsedUrl || parsedUrl.protocol !== "https:" || parsedUrl.username || parsedUrl.password) {
|
|
7252
|
+
issues.push("invalid-public-url");
|
|
7253
|
+
}
|
|
7254
|
+
if (!parsedUrl?.pathname.endsWith(`/${value.randomSlug}`)) issues.push("public-url-slug-mismatch");
|
|
7255
|
+
if (value.state !== "published") issues.push("published-state-required");
|
|
7256
|
+
if (!validDate(value.publishedAt)) issues.push("invalid-published-at");
|
|
7257
|
+
if (!validDate(value.expiresAt) || validDate(value.publishedAt) && Date.parse(value.expiresAt) <= Date.parse(value.publishedAt)) {
|
|
7258
|
+
issues.push("invalid-publication-expiry");
|
|
7259
|
+
}
|
|
7260
|
+
if (value.renewedAt !== void 0 && !validDate(value.renewedAt) || value.unpublishedAt !== void 0 && !validDate(value.unpublishedAt)) {
|
|
7261
|
+
issues.push("invalid-lifecycle-timestamp");
|
|
7262
|
+
}
|
|
7263
|
+
return [...new Set(issues)];
|
|
7264
|
+
}
|
|
7265
|
+
function assertValidStaticProjectPublication(value) {
|
|
7266
|
+
const issues = validateStaticProjectPublication(value);
|
|
7267
|
+
if (issues.length > 0) throw new Error(`Invalid static project publication: ${issues.join(", ")}`);
|
|
7268
|
+
}
|
|
7269
|
+
|
|
7037
7270
|
// src/validation.ts
|
|
7038
7271
|
var CANONICAL_TOKEN_SUBUNITS = /^(0|[1-9][0-9]*)$/u;
|
|
7039
7272
|
var EXACT_PACKAGE_NAME = /^@plasius\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
|
|
@@ -7232,6 +7465,9 @@ export {
|
|
|
7232
7465
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_2,
|
|
7233
7466
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_3,
|
|
7234
7467
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_4,
|
|
7468
|
+
LEARNING_COURSE_LIMITS,
|
|
7469
|
+
LEARNING_COURSE_STAGE_ORDER,
|
|
7470
|
+
LearningCourseInputError,
|
|
7235
7471
|
METEOR_SHIELD_MISSION_ONE_AUTHORING_V1,
|
|
7236
7472
|
MISSION_AUTHORING_CONTRACT_VERSION_V1,
|
|
7237
7473
|
OBSTACLE_EXPLORER_MISSION_ONE_AUTHORING_V1,
|
|
@@ -7269,8 +7505,18 @@ export {
|
|
|
7269
7505
|
assertValidStaticProjectSnapshot,
|
|
7270
7506
|
assertValidVoiceHelpAvailability,
|
|
7271
7507
|
calculateAssessment,
|
|
7508
|
+
completeVerifiedLearningActivity,
|
|
7509
|
+
createLearningCourseProgress,
|
|
7272
7510
|
isExternalLearningContentReferenceV1,
|
|
7511
|
+
parseLearningCourse,
|
|
7512
|
+
parseLearningCourseDraft,
|
|
7513
|
+
parseLearningCourseProgress,
|
|
7514
|
+
parseLearningProject,
|
|
7515
|
+
parseLearningSaveSlotId,
|
|
7516
|
+
recordLearningCourseAssessment,
|
|
7517
|
+
resolveLearningCourseStage,
|
|
7273
7518
|
validateAssessmentRubric,
|
|
7519
|
+
validateLearningCourse,
|
|
7274
7520
|
validateLearningPath,
|
|
7275
7521
|
validateMissionAuthoringBundle,
|
|
7276
7522
|
validateStaticProjectGuardianApproval,
|