@plasius/learning 0.2.0 → 0.2.2
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 +50 -7
- package/dist/index.cjs +939 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -1
- package/dist/index.d.ts +117 -1
- package/dist/index.js +931 -59
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -20,13 +20,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
JUNIOR_CODER_MISSION_STAGE_ORDER_V1: () => JUNIOR_CODER_MISSION_STAGE_ORDER_V1,
|
|
23
24
|
JUNIOR_CODER_MODULE_PRICE_V1_1: () => JUNIOR_CODER_MODULE_PRICE_V1_1,
|
|
24
25
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_CURRENT: () => JUNIOR_CODER_ROBOT_RESCUE_PATH_CURRENT,
|
|
25
26
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_V1: () => JUNIOR_CODER_ROBOT_RESCUE_PATH_V1,
|
|
26
27
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_1: () => JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_1,
|
|
28
|
+
MISSION_AUTHORING_CONTRACT_VERSION_V1: () => MISSION_AUTHORING_CONTRACT_VERSION_V1,
|
|
29
|
+
ROAD_HOPPER_RALLY_MISSION_ONE_AUTHORING_V1: () => ROAD_HOPPER_RALLY_MISSION_ONE_AUTHORING_V1,
|
|
30
|
+
ROBOT_MAZE_DASH_MISSION_ONE_AUTHORING_V1: () => ROBOT_MAZE_DASH_MISSION_ONE_AUTHORING_V1,
|
|
27
31
|
assertValidLearningPath: () => assertValidLearningPath,
|
|
32
|
+
assertValidMissionAuthoringBundle: () => assertValidMissionAuthoringBundle,
|
|
28
33
|
calculateAssessment: () => calculateAssessment,
|
|
29
|
-
|
|
34
|
+
validateAssessmentRubric: () => validateAssessmentRubric,
|
|
35
|
+
validateLearningPath: () => validateLearningPath,
|
|
36
|
+
validateMissionAuthoringBundle: () => validateMissionAuthoringBundle
|
|
30
37
|
});
|
|
31
38
|
module.exports = __toCommonJS(index_exports);
|
|
32
39
|
|
|
@@ -600,13 +607,932 @@ var JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_1 = {
|
|
|
600
607
|
};
|
|
601
608
|
var JUNIOR_CODER_ROBOT_RESCUE_PATH_CURRENT = JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_1;
|
|
602
609
|
|
|
603
|
-
// src/
|
|
610
|
+
// src/contracts.ts
|
|
611
|
+
var MISSION_AUTHORING_CONTRACT_VERSION_V1 = "1.0.0";
|
|
612
|
+
|
|
613
|
+
// src/rubric-validation.ts
|
|
604
614
|
var DIMENSION_TOTALS = {
|
|
605
615
|
structure: 20,
|
|
606
616
|
behaviour: 50,
|
|
607
617
|
resilience: 20,
|
|
608
618
|
safety: 10
|
|
609
619
|
};
|
|
620
|
+
function rubricIssue(code, message, path, moduleId) {
|
|
621
|
+
return { code, message, path, ...moduleId ? { moduleId } : {} };
|
|
622
|
+
}
|
|
623
|
+
function validateAssessmentRubric(rubric2, path = "assessment", moduleId) {
|
|
624
|
+
const issues = [];
|
|
625
|
+
const criterionIds = /* @__PURE__ */ new Set();
|
|
626
|
+
const dimensionTotals = {
|
|
627
|
+
structure: 0,
|
|
628
|
+
behaviour: 0,
|
|
629
|
+
resilience: 0,
|
|
630
|
+
safety: 0
|
|
631
|
+
};
|
|
632
|
+
let rubricTotal = 0;
|
|
633
|
+
for (const criterion of rubric2.criteria) {
|
|
634
|
+
rubricTotal += criterion.points;
|
|
635
|
+
dimensionTotals[criterion.dimension] += criterion.points;
|
|
636
|
+
if (criterionIds.has(criterion.id)) {
|
|
637
|
+
issues.push(
|
|
638
|
+
rubricIssue(
|
|
639
|
+
"duplicate-criterion-id",
|
|
640
|
+
`Duplicate assessment criterion ${criterion.id}.`,
|
|
641
|
+
`${path}.criteria`,
|
|
642
|
+
moduleId
|
|
643
|
+
)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
criterionIds.add(criterion.id);
|
|
647
|
+
}
|
|
648
|
+
if (rubricTotal !== 100) {
|
|
649
|
+
issues.push(
|
|
650
|
+
rubricIssue(
|
|
651
|
+
"rubric-total",
|
|
652
|
+
`Assessment rubric totals ${rubricTotal}; expected 100.`,
|
|
653
|
+
`${path}.criteria`,
|
|
654
|
+
moduleId
|
|
655
|
+
)
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
for (const [dimension, expected] of Object.entries(DIMENSION_TOTALS)) {
|
|
659
|
+
if (dimensionTotals[dimension] !== expected) {
|
|
660
|
+
issues.push(
|
|
661
|
+
rubricIssue(
|
|
662
|
+
"rubric-dimension-total",
|
|
663
|
+
`${dimension} criteria total ${dimensionTotals[dimension]}; expected ${expected}.`,
|
|
664
|
+
`${path}.criteria`,
|
|
665
|
+
moduleId
|
|
666
|
+
)
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (!rubric2.criteria.some(
|
|
671
|
+
(criterion) => criterion.dimension === "safety" && criterion.mandatory
|
|
672
|
+
)) {
|
|
673
|
+
issues.push(
|
|
674
|
+
rubricIssue(
|
|
675
|
+
"missing-mandatory-safety",
|
|
676
|
+
"Every module requires a mandatory safety criterion.",
|
|
677
|
+
`${path}.criteria`,
|
|
678
|
+
moduleId
|
|
679
|
+
)
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return issues;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/mission-authoring.ts
|
|
686
|
+
var JUNIOR_CODER_MISSION_STAGE_ORDER_V1 = [
|
|
687
|
+
"learn",
|
|
688
|
+
"predict",
|
|
689
|
+
"build",
|
|
690
|
+
"run",
|
|
691
|
+
"assess",
|
|
692
|
+
"inspect",
|
|
693
|
+
"fix",
|
|
694
|
+
"explain",
|
|
695
|
+
"reward"
|
|
696
|
+
];
|
|
697
|
+
var LEARNER_STARTER_KINDS = /* @__PURE__ */ new Set([
|
|
698
|
+
"starter-code",
|
|
699
|
+
"starter-assets",
|
|
700
|
+
"sample-data"
|
|
701
|
+
]);
|
|
702
|
+
var LEARNER_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
|
|
703
|
+
"facilitator-note",
|
|
704
|
+
"answer-key",
|
|
705
|
+
"protected-test"
|
|
706
|
+
]);
|
|
707
|
+
var SINGLE_MODE_REQUIRES_ALTERNATIVE = /* @__PURE__ */ new Set([
|
|
708
|
+
"pointer",
|
|
709
|
+
"drag",
|
|
710
|
+
"audio",
|
|
711
|
+
"colour",
|
|
712
|
+
"motion"
|
|
713
|
+
]);
|
|
714
|
+
function authoringIssue(code, message, path) {
|
|
715
|
+
return { code, message, path };
|
|
716
|
+
}
|
|
717
|
+
function reportDuplicateIds(ids, path) {
|
|
718
|
+
const seen = /* @__PURE__ */ new Set();
|
|
719
|
+
const issues = [];
|
|
720
|
+
for (const id of ids) {
|
|
721
|
+
if (seen.has(id)) {
|
|
722
|
+
issues.push(
|
|
723
|
+
authoringIssue("duplicate-id", `Duplicate authored ID ${id}.`, path)
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
seen.add(id);
|
|
727
|
+
}
|
|
728
|
+
return issues;
|
|
729
|
+
}
|
|
730
|
+
function validateMissionAuthoringBundle(bundle, module3) {
|
|
731
|
+
const issues = [];
|
|
732
|
+
if (bundle.version !== MISSION_AUTHORING_CONTRACT_VERSION_V1) {
|
|
733
|
+
issues.push(
|
|
734
|
+
authoringIssue(
|
|
735
|
+
"bundle-version-mismatch",
|
|
736
|
+
`Unsupported mission authoring version ${bundle.version}.`,
|
|
737
|
+
"version"
|
|
738
|
+
)
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
if (bundle.moduleId !== module3.id || bundle.moduleVersion !== module3.version) {
|
|
742
|
+
issues.push(
|
|
743
|
+
authoringIssue(
|
|
744
|
+
"module-reference-mismatch",
|
|
745
|
+
`Bundle ${bundle.moduleId}@${bundle.moduleVersion} does not match ${module3.id}@${module3.version}.`,
|
|
746
|
+
"moduleId"
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (!module3.missions.some((mission2) => mission2.id === bundle.missionId)) {
|
|
751
|
+
issues.push(
|
|
752
|
+
authoringIssue(
|
|
753
|
+
"mission-reference-mismatch",
|
|
754
|
+
`Mission ${bundle.missionId} does not exist in module ${module3.id}.`,
|
|
755
|
+
"missionId"
|
|
756
|
+
)
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const learner = bundle.learner;
|
|
760
|
+
const facilitator = bundle.facilitator;
|
|
761
|
+
if (learner.estimatedMinutes < 15 || learner.estimatedMinutes > 25) {
|
|
762
|
+
issues.push(
|
|
763
|
+
authoringIssue(
|
|
764
|
+
"invalid-duration",
|
|
765
|
+
"A mission must last between 15 and 25 minutes.",
|
|
766
|
+
"learner.estimatedMinutes"
|
|
767
|
+
)
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
const stageKinds = learner.stages.map((stage) => stage.kind);
|
|
771
|
+
for (const requiredStage of JUNIOR_CODER_MISSION_STAGE_ORDER_V1) {
|
|
772
|
+
const count = stageKinds.filter((stage) => stage === requiredStage).length;
|
|
773
|
+
if (count === 0) {
|
|
774
|
+
issues.push(
|
|
775
|
+
authoringIssue(
|
|
776
|
+
"missing-stage",
|
|
777
|
+
`Mission stage ${requiredStage} is required.`,
|
|
778
|
+
"learner.stages"
|
|
779
|
+
)
|
|
780
|
+
);
|
|
781
|
+
} else if (count > 1) {
|
|
782
|
+
issues.push(
|
|
783
|
+
authoringIssue(
|
|
784
|
+
"duplicate-stage",
|
|
785
|
+
`Mission stage ${requiredStage} appears more than once.`,
|
|
786
|
+
"learner.stages"
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (stageKinds.length === JUNIOR_CODER_MISSION_STAGE_ORDER_V1.length && stageKinds.some(
|
|
792
|
+
(stage, index) => stage !== JUNIOR_CODER_MISSION_STAGE_ORDER_V1[index]
|
|
793
|
+
)) {
|
|
794
|
+
issues.push(
|
|
795
|
+
authoringIssue(
|
|
796
|
+
"stage-order",
|
|
797
|
+
"Mission stages must follow the canonical learner journey.",
|
|
798
|
+
"learner.stages"
|
|
799
|
+
)
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (learner.readinessChecks.length === 0) {
|
|
803
|
+
issues.push(
|
|
804
|
+
authoringIssue(
|
|
805
|
+
"missing-readiness-check",
|
|
806
|
+
"At least one unscored readiness check is required.",
|
|
807
|
+
"learner.readinessChecks"
|
|
808
|
+
)
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
if (learner.readinessChecks.some((check) => check.scored !== false)) {
|
|
812
|
+
issues.push(
|
|
813
|
+
authoringIssue(
|
|
814
|
+
"scored-readiness-check",
|
|
815
|
+
"Readiness checks must not affect the deterministic score.",
|
|
816
|
+
"learner.readinessChecks"
|
|
817
|
+
)
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
issues.push(
|
|
821
|
+
...reportDuplicateIds(
|
|
822
|
+
learner.readinessChecks.map((check) => check.id),
|
|
823
|
+
"learner.readinessChecks"
|
|
824
|
+
)
|
|
825
|
+
);
|
|
826
|
+
const learnerArtifactIds = new Set(learner.artifacts.map((artifact) => artifact.id));
|
|
827
|
+
if (!learner.artifacts.some((artifact) => LEARNER_STARTER_KINDS.has(artifact.kind))) {
|
|
828
|
+
issues.push(
|
|
829
|
+
authoringIssue(
|
|
830
|
+
"missing-starter-artifact",
|
|
831
|
+
"At least one learner-safe starter artifact is required.",
|
|
832
|
+
"learner.artifacts"
|
|
833
|
+
)
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (learner.artifacts.some(
|
|
837
|
+
(artifact) => artifact.audience !== "learner" || artifact.solutionBearing || LEARNER_FORBIDDEN_KINDS.has(artifact.kind)
|
|
838
|
+
)) {
|
|
839
|
+
issues.push(
|
|
840
|
+
authoringIssue(
|
|
841
|
+
"learner-artifact-leak",
|
|
842
|
+
"Learner artifacts cannot contain facilitator or solution-bearing content.",
|
|
843
|
+
"learner.artifacts"
|
|
844
|
+
)
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
if (facilitator.artifacts.some((artifact) => artifact.audience !== "facilitator")) {
|
|
848
|
+
issues.push(
|
|
849
|
+
authoringIssue(
|
|
850
|
+
"facilitator-artifact-leak",
|
|
851
|
+
"Facilitator artifacts must remain in the facilitator projection.",
|
|
852
|
+
"facilitator.artifacts"
|
|
853
|
+
)
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
issues.push(
|
|
857
|
+
...reportDuplicateIds(
|
|
858
|
+
[...learner.artifacts, ...facilitator.artifacts].map((artifact) => artifact.id),
|
|
859
|
+
"artifacts"
|
|
860
|
+
)
|
|
861
|
+
);
|
|
862
|
+
for (const [stageIndex, stage] of learner.stages.entries()) {
|
|
863
|
+
for (const artifactId of stage.artifactIds) {
|
|
864
|
+
if (!learnerArtifactIds.has(artifactId)) {
|
|
865
|
+
issues.push(
|
|
866
|
+
authoringIssue(
|
|
867
|
+
"unknown-artifact",
|
|
868
|
+
`Stage references unknown learner artifact ${artifactId}.`,
|
|
869
|
+
`learner.stages[${stageIndex}].artifactIds`
|
|
870
|
+
)
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (learner.goals.length === 0) {
|
|
876
|
+
issues.push(
|
|
877
|
+
authoringIssue(
|
|
878
|
+
"missing-visible-goal",
|
|
879
|
+
"At least one visible learner goal is required.",
|
|
880
|
+
"learner.goals"
|
|
881
|
+
)
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (facilitator.protectedGoals.length === 0) {
|
|
885
|
+
issues.push(
|
|
886
|
+
authoringIssue(
|
|
887
|
+
"missing-protected-goal",
|
|
888
|
+
"At least one protected facilitator goal is required.",
|
|
889
|
+
"facilitator.protectedGoals"
|
|
890
|
+
)
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
const allGoals = [...learner.goals, ...facilitator.protectedGoals];
|
|
894
|
+
const learnerGoalIds = new Set(learner.goals.map((goal) => goal.id));
|
|
895
|
+
const seenGoalIds = /* @__PURE__ */ new Set();
|
|
896
|
+
for (const goal of allGoals) {
|
|
897
|
+
if (seenGoalIds.has(goal.id)) {
|
|
898
|
+
issues.push(
|
|
899
|
+
authoringIssue(
|
|
900
|
+
"duplicate-goal-id",
|
|
901
|
+
`Duplicate goal ID ${goal.id}.`,
|
|
902
|
+
"goals"
|
|
903
|
+
)
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
seenGoalIds.add(goal.id);
|
|
907
|
+
}
|
|
908
|
+
if (learner.goals.some((goal) => goal.visibility !== "visible") || facilitator.protectedGoals.some(
|
|
909
|
+
(goal) => goal.visibility !== "protected" || goal.completionRequired
|
|
910
|
+
)) {
|
|
911
|
+
issues.push(
|
|
912
|
+
authoringIssue(
|
|
913
|
+
"invalid-goal-projection",
|
|
914
|
+
"Visible goals belong to learners and protected goals to facilitators.",
|
|
915
|
+
"goals"
|
|
916
|
+
)
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
const criterionById = new Map(
|
|
920
|
+
module3.assessment.criteria.map((criterion) => [criterion.id, criterion])
|
|
921
|
+
);
|
|
922
|
+
for (const goal of allGoals) {
|
|
923
|
+
if (goal.criterionIds.length === 0) {
|
|
924
|
+
issues.push(
|
|
925
|
+
authoringIssue(
|
|
926
|
+
"unknown-criterion",
|
|
927
|
+
`Goal ${goal.id} must reference a deterministic criterion.`,
|
|
928
|
+
"goals"
|
|
929
|
+
)
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
for (const criterionId of goal.criterionIds) {
|
|
933
|
+
const criterion = criterionById.get(criterionId);
|
|
934
|
+
if (!criterion) {
|
|
935
|
+
issues.push(
|
|
936
|
+
authoringIssue(
|
|
937
|
+
"unknown-criterion",
|
|
938
|
+
`Goal ${goal.id} references unknown criterion ${criterionId}.`,
|
|
939
|
+
"goals"
|
|
940
|
+
)
|
|
941
|
+
);
|
|
942
|
+
} else if (criterion.visibility !== goal.visibility) {
|
|
943
|
+
issues.push(
|
|
944
|
+
authoringIssue(
|
|
945
|
+
"criterion-visibility-mismatch",
|
|
946
|
+
`Goal ${goal.id} cannot expose a ${criterion.visibility} criterion as ${goal.visibility}.`,
|
|
947
|
+
"goals"
|
|
948
|
+
)
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (goal.completionRequired && goal.aiRequired) {
|
|
953
|
+
issues.push(
|
|
954
|
+
authoringIssue(
|
|
955
|
+
"ai-dependent-completion",
|
|
956
|
+
`Completion goal ${goal.id} cannot require AI.`,
|
|
957
|
+
"goals"
|
|
958
|
+
)
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
for (const rubricIssue2 of validateAssessmentRubric(module3.assessment)) {
|
|
963
|
+
if (rubricIssue2.code === "rubric-total" || rubricIssue2.code === "rubric-dimension-total" || rubricIssue2.code === "duplicate-criterion-id" || rubricIssue2.code === "missing-mandatory-safety") {
|
|
964
|
+
issues.push(
|
|
965
|
+
authoringIssue(rubricIssue2.code, rubricIssue2.message, rubricIssue2.path)
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const alternativeById = new Map(
|
|
970
|
+
learner.accessibilityAlternatives.map((alternative) => [alternative.id, alternative])
|
|
971
|
+
);
|
|
972
|
+
issues.push(
|
|
973
|
+
...reportDuplicateIds(
|
|
974
|
+
learner.interactions.map((interaction) => interaction.id),
|
|
975
|
+
"learner.interactions"
|
|
976
|
+
),
|
|
977
|
+
...reportDuplicateIds(
|
|
978
|
+
learner.accessibilityAlternatives.map((alternative) => alternative.id),
|
|
979
|
+
"learner.accessibilityAlternatives"
|
|
980
|
+
)
|
|
981
|
+
);
|
|
982
|
+
for (const [interactionIndex, interaction] of learner.interactions.entries()) {
|
|
983
|
+
if (SINGLE_MODE_REQUIRES_ALTERNATIVE.has(interaction.primaryMode) && interaction.alternativeIds.length === 0) {
|
|
984
|
+
issues.push(
|
|
985
|
+
authoringIssue(
|
|
986
|
+
"inaccessible-interaction",
|
|
987
|
+
`Interaction ${interaction.id} requires an equivalent alternative.`,
|
|
988
|
+
`learner.interactions[${interactionIndex}]`
|
|
989
|
+
)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
for (const alternativeId of interaction.alternativeIds) {
|
|
993
|
+
const alternative = alternativeById.get(alternativeId);
|
|
994
|
+
if (!alternative) {
|
|
995
|
+
issues.push(
|
|
996
|
+
authoringIssue(
|
|
997
|
+
"unknown-accessibility-alternative",
|
|
998
|
+
`Interaction ${interaction.id} references unknown alternative ${alternativeId}.`,
|
|
999
|
+
`learner.interactions[${interactionIndex}].alternativeIds`
|
|
1000
|
+
)
|
|
1001
|
+
);
|
|
1002
|
+
} else if (alternative.equivalentOutcome !== true || alternative.modes.length === 0 || alternative.modes.every((mode) => mode === interaction.primaryMode)) {
|
|
1003
|
+
issues.push(
|
|
1004
|
+
authoringIssue(
|
|
1005
|
+
"non-equivalent-accessibility-alternative",
|
|
1006
|
+
`Alternative ${alternativeId} must provide an equivalent outcome through another mode.`,
|
|
1007
|
+
"learner.accessibilityAlternatives"
|
|
1008
|
+
)
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (learner.evidenceRequirements.length === 0) {
|
|
1014
|
+
issues.push(
|
|
1015
|
+
authoringIssue(
|
|
1016
|
+
"missing-evidence",
|
|
1017
|
+
"At least one evidence requirement is required.",
|
|
1018
|
+
"learner.evidenceRequirements"
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
issues.push(
|
|
1023
|
+
...reportDuplicateIds(
|
|
1024
|
+
learner.evidenceRequirements.map((evidence) => evidence.id),
|
|
1025
|
+
"learner.evidenceRequirements"
|
|
1026
|
+
)
|
|
1027
|
+
);
|
|
1028
|
+
for (const [evidenceIndex, evidence] of learner.evidenceRequirements.entries()) {
|
|
1029
|
+
if (evidence.containsPersonalData !== false) {
|
|
1030
|
+
issues.push(
|
|
1031
|
+
authoringIssue(
|
|
1032
|
+
"personal-data-evidence",
|
|
1033
|
+
"Mission evidence cannot request personal data.",
|
|
1034
|
+
`learner.evidenceRequirements[${evidenceIndex}]`
|
|
1035
|
+
)
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
for (const goalId of evidence.goalIds) {
|
|
1039
|
+
if (!learnerGoalIds.has(goalId)) {
|
|
1040
|
+
issues.push(
|
|
1041
|
+
authoringIssue(
|
|
1042
|
+
"unknown-evidence-goal",
|
|
1043
|
+
`Evidence references unknown goal ${goalId}.`,
|
|
1044
|
+
`learner.evidenceRequirements[${evidenceIndex}].goalIds`
|
|
1045
|
+
)
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
for (const goal of learner.goals.filter((entry) => entry.completionRequired)) {
|
|
1051
|
+
if (!learner.evidenceRequirements.some((evidence) => evidence.goalIds.includes(goal.id))) {
|
|
1052
|
+
issues.push(
|
|
1053
|
+
authoringIssue(
|
|
1054
|
+
"missing-evidence",
|
|
1055
|
+
`Completion goal ${goal.id} requires deterministic evidence.`,
|
|
1056
|
+
"learner.evidenceRequirements"
|
|
1057
|
+
)
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const mandatorySafetyGoals = learner.goals.filter(
|
|
1062
|
+
(goal) => goal.completionRequired && goal.criterionIds.some((criterionId) => {
|
|
1063
|
+
const criterion = criterionById.get(criterionId);
|
|
1064
|
+
return criterion?.dimension === "safety" && criterion.mandatory;
|
|
1065
|
+
})
|
|
1066
|
+
);
|
|
1067
|
+
if (mandatorySafetyGoals.length === 0 || !mandatorySafetyGoals.some(
|
|
1068
|
+
(goal) => learner.evidenceRequirements.some((evidence) => evidence.goalIds.includes(goal.id))
|
|
1069
|
+
)) {
|
|
1070
|
+
issues.push(
|
|
1071
|
+
authoringIssue(
|
|
1072
|
+
"missing-safety-evidence",
|
|
1073
|
+
"A completion-required goal must evidence a mandatory safety criterion.",
|
|
1074
|
+
"learner.evidenceRequirements"
|
|
1075
|
+
)
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
issues.push(
|
|
1079
|
+
...reportDuplicateIds(
|
|
1080
|
+
learner.sideAdventures.map((adventure) => adventure.id),
|
|
1081
|
+
"learner.sideAdventures"
|
|
1082
|
+
)
|
|
1083
|
+
);
|
|
1084
|
+
if (learner.sideAdventures.length === 0) {
|
|
1085
|
+
issues.push(
|
|
1086
|
+
authoringIssue(
|
|
1087
|
+
"missing-side-adventure",
|
|
1088
|
+
"At least one optional side adventure is required.",
|
|
1089
|
+
"learner.sideAdventures"
|
|
1090
|
+
)
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
if (learner.sideAdventures.some((adventure) => adventure.completionRequired !== false)) {
|
|
1094
|
+
issues.push(
|
|
1095
|
+
authoringIssue(
|
|
1096
|
+
"mandatory-side-adventure",
|
|
1097
|
+
"Side adventures must remain optional.",
|
|
1098
|
+
"learner.sideAdventures"
|
|
1099
|
+
)
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
const badgeIds = new Set(module3.badges.map((badge) => badge.id));
|
|
1103
|
+
issues.push(
|
|
1104
|
+
...reportDuplicateIds(
|
|
1105
|
+
learner.rewardBindings.map((reward) => reward.id),
|
|
1106
|
+
"learner.rewardBindings"
|
|
1107
|
+
)
|
|
1108
|
+
);
|
|
1109
|
+
for (const [rewardIndex, reward] of learner.rewardBindings.entries()) {
|
|
1110
|
+
const rewardInvalid = reward.deterministic !== true || reward.random !== false || reward.tokenConvertible !== false || reward.goalIds.length === 0 || !badgeIds.has(reward.badgeId) || reward.goalIds.some((goalId) => !learnerGoalIds.has(goalId));
|
|
1111
|
+
if (rewardInvalid) {
|
|
1112
|
+
issues.push(
|
|
1113
|
+
authoringIssue(
|
|
1114
|
+
"invalid-reward",
|
|
1115
|
+
`Reward ${reward.id} must be deterministic, evidence-bound and non-convertible.`,
|
|
1116
|
+
`learner.rewardBindings[${rewardIndex}]`
|
|
1117
|
+
)
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return issues;
|
|
1122
|
+
}
|
|
1123
|
+
function assertValidMissionAuthoringBundle(bundle, module3) {
|
|
1124
|
+
const issues = validateMissionAuthoringBundle(bundle, module3);
|
|
1125
|
+
if (issues.length === 0) return;
|
|
1126
|
+
const summary = issues.map((entry) => `${entry.code} at ${entry.path}: ${entry.message}`).join("\n");
|
|
1127
|
+
throw new Error(`Invalid mission authoring bundle:
|
|
1128
|
+
${summary}`);
|
|
1129
|
+
}
|
|
1130
|
+
var ROBOT_MAZE_DASH_MISSION_ONE_AUTHORING_V1 = {
|
|
1131
|
+
version: MISSION_AUTHORING_CONTRACT_VERSION_V1,
|
|
1132
|
+
moduleId: "junior-coder.robot-maze-dash",
|
|
1133
|
+
moduleVersion: "1.1.0",
|
|
1134
|
+
missionId: "robot-maze-dash-mission-1",
|
|
1135
|
+
learner: {
|
|
1136
|
+
estimatedMinutes: 20,
|
|
1137
|
+
stages: [
|
|
1138
|
+
{
|
|
1139
|
+
kind: "learn",
|
|
1140
|
+
instruction: "Meet the move, turn-left and turn-right action blocks and read what each command does.",
|
|
1141
|
+
artifactIds: ["robot-maze-dash-m1-art"]
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
kind: "predict",
|
|
1145
|
+
instruction: "Predict where the robot will stop after it follows the blocks from top to bottom.",
|
|
1146
|
+
artifactIds: []
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
kind: "build",
|
|
1150
|
+
instruction: "Arrange the action blocks so the rescue robot can reach the beacon.",
|
|
1151
|
+
artifactIds: ["robot-maze-dash-m1-program"]
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
kind: "run",
|
|
1155
|
+
instruction: "Use the Run action button to watch the robot follow your visual program.",
|
|
1156
|
+
artifactIds: ["robot-maze-dash-m1-program"]
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
kind: "assess",
|
|
1160
|
+
instruction: "Run the visible and protected deterministic mission checks.",
|
|
1161
|
+
artifactIds: []
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
kind: "inspect",
|
|
1165
|
+
instruction: "Compare the highlighted block with the first goal that did not pass.",
|
|
1166
|
+
artifactIds: []
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
kind: "fix",
|
|
1170
|
+
instruction: "Move, add or remove one action block, then run the mission again.",
|
|
1171
|
+
artifactIds: ["robot-maze-dash-m1-program"]
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
kind: "explain",
|
|
1175
|
+
instruction: "Explain how the order of your blocks changed the robot path.",
|
|
1176
|
+
artifactIds: []
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
kind: "reward",
|
|
1180
|
+
instruction: "Collect the evidence-bound badge when the score and safety check pass.",
|
|
1181
|
+
artifactIds: []
|
|
1182
|
+
}
|
|
1183
|
+
],
|
|
1184
|
+
readinessChecks: [
|
|
1185
|
+
{
|
|
1186
|
+
id: "robot-maze-dash-m1-read-order",
|
|
1187
|
+
prompt: "Point to the first action the robot will follow.",
|
|
1188
|
+
scored: false
|
|
1189
|
+
}
|
|
1190
|
+
],
|
|
1191
|
+
artifacts: [
|
|
1192
|
+
{
|
|
1193
|
+
id: "robot-maze-dash-m1-program",
|
|
1194
|
+
kind: "starter-code",
|
|
1195
|
+
audience: "learner",
|
|
1196
|
+
solutionBearing: false
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
id: "robot-maze-dash-m1-art",
|
|
1200
|
+
kind: "starter-assets",
|
|
1201
|
+
audience: "learner",
|
|
1202
|
+
solutionBearing: false
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
id: "robot-maze-dash-m1-printable",
|
|
1206
|
+
kind: "printable",
|
|
1207
|
+
audience: "learner",
|
|
1208
|
+
solutionBearing: false
|
|
1209
|
+
}
|
|
1210
|
+
],
|
|
1211
|
+
goals: [
|
|
1212
|
+
{
|
|
1213
|
+
id: "robot-maze-dash-m1-starts",
|
|
1214
|
+
statement: "The visual program is structurally valid and starts.",
|
|
1215
|
+
visibility: "visible",
|
|
1216
|
+
criterionIds: ["robot-maze-dash-build"],
|
|
1217
|
+
completionRequired: true,
|
|
1218
|
+
aiRequired: false
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
id: "robot-maze-dash-m1-reaches-beacon",
|
|
1222
|
+
statement: "The robot follows the action order and reaches the rescue beacon.",
|
|
1223
|
+
visibility: "visible",
|
|
1224
|
+
criterionIds: [
|
|
1225
|
+
"robot-maze-dash-goal-one",
|
|
1226
|
+
"robot-maze-dash-goal-two"
|
|
1227
|
+
],
|
|
1228
|
+
completionRequired: true,
|
|
1229
|
+
aiRequired: false
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
id: "robot-maze-dash-m1-safe-preview",
|
|
1233
|
+
statement: "The robot stays inside the private maze simulator boundary.",
|
|
1234
|
+
visibility: "visible",
|
|
1235
|
+
criterionIds: ["robot-maze-dash-safety"],
|
|
1236
|
+
completionRequired: true,
|
|
1237
|
+
aiRequired: false
|
|
1238
|
+
}
|
|
1239
|
+
],
|
|
1240
|
+
interactions: [
|
|
1241
|
+
{
|
|
1242
|
+
id: "robot-maze-dash-m1-reorder-blocks",
|
|
1243
|
+
description: "Change the order of visual action blocks.",
|
|
1244
|
+
primaryMode: "drag",
|
|
1245
|
+
alternativeIds: ["robot-maze-dash-m1-button-reorder"]
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
id: "robot-maze-dash-m1-run-control",
|
|
1249
|
+
description: "Start the private maze simulation.",
|
|
1250
|
+
primaryMode: "pointer",
|
|
1251
|
+
alternativeIds: ["robot-maze-dash-m1-keyboard-run"]
|
|
1252
|
+
}
|
|
1253
|
+
],
|
|
1254
|
+
accessibilityAlternatives: [
|
|
1255
|
+
{
|
|
1256
|
+
id: "robot-maze-dash-m1-button-reorder",
|
|
1257
|
+
modes: ["keyboard", "pointer"],
|
|
1258
|
+
equivalentOutcome: true,
|
|
1259
|
+
description: "Use labelled Move up and Move down buttons instead of dragging a block."
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
id: "robot-maze-dash-m1-keyboard-run",
|
|
1263
|
+
modes: ["keyboard"],
|
|
1264
|
+
equivalentOutcome: true,
|
|
1265
|
+
description: "Start the same simulation by pressing Enter or Space on the Run button."
|
|
1266
|
+
}
|
|
1267
|
+
],
|
|
1268
|
+
evidenceRequirements: [
|
|
1269
|
+
{
|
|
1270
|
+
id: "robot-maze-dash-m1-assessment",
|
|
1271
|
+
goalIds: [
|
|
1272
|
+
"robot-maze-dash-m1-starts",
|
|
1273
|
+
"robot-maze-dash-m1-reaches-beacon",
|
|
1274
|
+
"robot-maze-dash-m1-safe-preview"
|
|
1275
|
+
],
|
|
1276
|
+
kind: "assessment-result",
|
|
1277
|
+
retention: "entitlement",
|
|
1278
|
+
containsPersonalData: false
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
id: "robot-maze-dash-m1-explanation",
|
|
1282
|
+
goalIds: ["robot-maze-dash-m1-reaches-beacon"],
|
|
1283
|
+
kind: "learner-explanation",
|
|
1284
|
+
retention: "attempt",
|
|
1285
|
+
containsPersonalData: false
|
|
1286
|
+
}
|
|
1287
|
+
],
|
|
1288
|
+
sideAdventures: [
|
|
1289
|
+
{
|
|
1290
|
+
id: "robot-maze-dash-m1-remix",
|
|
1291
|
+
prompt: "Invent a different safe route and describe which action block must change first.",
|
|
1292
|
+
completionRequired: false
|
|
1293
|
+
}
|
|
1294
|
+
],
|
|
1295
|
+
rewardBindings: [
|
|
1296
|
+
{
|
|
1297
|
+
id: "robot-maze-dash-m1-badge",
|
|
1298
|
+
badgeId: "robot-maze-dash-mission-complete",
|
|
1299
|
+
goalIds: [
|
|
1300
|
+
"robot-maze-dash-m1-starts",
|
|
1301
|
+
"robot-maze-dash-m1-reaches-beacon",
|
|
1302
|
+
"robot-maze-dash-m1-safe-preview"
|
|
1303
|
+
],
|
|
1304
|
+
deterministic: true,
|
|
1305
|
+
random: false,
|
|
1306
|
+
tokenConvertible: false
|
|
1307
|
+
}
|
|
1308
|
+
]
|
|
1309
|
+
},
|
|
1310
|
+
facilitator: {
|
|
1311
|
+
artifacts: [
|
|
1312
|
+
{
|
|
1313
|
+
id: "robot-maze-dash-m1-answer-key",
|
|
1314
|
+
kind: "answer-key",
|
|
1315
|
+
audience: "facilitator",
|
|
1316
|
+
solutionBearing: true
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
id: "robot-maze-dash-m1-protected-tests",
|
|
1320
|
+
kind: "protected-test",
|
|
1321
|
+
audience: "facilitator",
|
|
1322
|
+
solutionBearing: true
|
|
1323
|
+
}
|
|
1324
|
+
],
|
|
1325
|
+
protectedGoals: [
|
|
1326
|
+
{
|
|
1327
|
+
id: "robot-maze-dash-m1-protected-bounds",
|
|
1328
|
+
statement: "The interpreter stops safely at walls, bounds and its action limit.",
|
|
1329
|
+
visibility: "protected",
|
|
1330
|
+
criterionIds: [
|
|
1331
|
+
"robot-maze-dash-edge-one",
|
|
1332
|
+
"robot-maze-dash-edge-two"
|
|
1333
|
+
],
|
|
1334
|
+
completionRequired: false,
|
|
1335
|
+
aiRequired: false
|
|
1336
|
+
}
|
|
1337
|
+
],
|
|
1338
|
+
prompts: [
|
|
1339
|
+
"Ask the learner to point to the first action block before suggesting a change.",
|
|
1340
|
+
"Use the command reference and visible goal; never reveal the protected route or expected block list."
|
|
1341
|
+
]
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
var ROAD_HOPPER_RALLY_MISSION_ONE_AUTHORING_V1 = {
|
|
1345
|
+
version: MISSION_AUTHORING_CONTRACT_VERSION_V1,
|
|
1346
|
+
moduleId: "junior-coder.road-hopper-rally",
|
|
1347
|
+
moduleVersion: "1.1.0",
|
|
1348
|
+
missionId: "road-hopper-rally-mission-1",
|
|
1349
|
+
learner: {
|
|
1350
|
+
estimatedMinutes: 20,
|
|
1351
|
+
stages: [
|
|
1352
|
+
{
|
|
1353
|
+
kind: "learn",
|
|
1354
|
+
instruction: "Find the x and y coordinates that place a rescue marker on the road.",
|
|
1355
|
+
artifactIds: ["road-hopper-rally-m1-art"]
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
kind: "predict",
|
|
1359
|
+
instruction: "Predict where the marker will appear before you run the starter project.",
|
|
1360
|
+
artifactIds: []
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
kind: "build",
|
|
1364
|
+
instruction: "Complete the two road-lane drawing commands in the starter code.",
|
|
1365
|
+
artifactIds: ["road-hopper-rally-m1-code"]
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
kind: "run",
|
|
1369
|
+
instruction: "Run the private preview and use the keyboard start control.",
|
|
1370
|
+
artifactIds: ["road-hopper-rally-m1-code"]
|
|
1371
|
+
},
|
|
1372
|
+
{
|
|
1373
|
+
kind: "assess",
|
|
1374
|
+
instruction: "Run the visible and protected deterministic mission checks.",
|
|
1375
|
+
artifactIds: []
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
kind: "inspect",
|
|
1379
|
+
instruction: "Compare the highlighted line with the goal that did not pass.",
|
|
1380
|
+
artifactIds: []
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
kind: "fix",
|
|
1384
|
+
instruction: "Change one coordinate or drawing command, then run the checks again.",
|
|
1385
|
+
artifactIds: ["road-hopper-rally-m1-code"]
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
kind: "explain",
|
|
1389
|
+
instruction: "Explain how your coordinates changed the road on screen.",
|
|
1390
|
+
artifactIds: []
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
kind: "reward",
|
|
1394
|
+
instruction: "Collect the evidence-bound badge when the mission score and safety check pass.",
|
|
1395
|
+
artifactIds: []
|
|
1396
|
+
}
|
|
1397
|
+
],
|
|
1398
|
+
readinessChecks: [
|
|
1399
|
+
{
|
|
1400
|
+
id: "road-hopper-rally-m1-predict-coordinate",
|
|
1401
|
+
prompt: "Point to the pair of numbers that controls horizontal and vertical position.",
|
|
1402
|
+
scored: false
|
|
1403
|
+
}
|
|
1404
|
+
],
|
|
1405
|
+
artifacts: [
|
|
1406
|
+
{
|
|
1407
|
+
id: "road-hopper-rally-m1-code",
|
|
1408
|
+
kind: "starter-code",
|
|
1409
|
+
audience: "learner",
|
|
1410
|
+
solutionBearing: false
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
id: "road-hopper-rally-m1-art",
|
|
1414
|
+
kind: "starter-assets",
|
|
1415
|
+
audience: "learner",
|
|
1416
|
+
solutionBearing: false
|
|
1417
|
+
}
|
|
1418
|
+
],
|
|
1419
|
+
goals: [
|
|
1420
|
+
{
|
|
1421
|
+
id: "road-hopper-rally-m1-starts",
|
|
1422
|
+
statement: "The starter project is structurally valid and starts.",
|
|
1423
|
+
visibility: "visible",
|
|
1424
|
+
criterionIds: ["road-hopper-rally-build"],
|
|
1425
|
+
completionRequired: true,
|
|
1426
|
+
aiRequired: false
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
id: "road-hopper-rally-m1-draws-road",
|
|
1430
|
+
statement: "Two original road lanes appear at the expected coordinates.",
|
|
1431
|
+
visibility: "visible",
|
|
1432
|
+
criterionIds: ["road-hopper-rally-goal-one"],
|
|
1433
|
+
completionRequired: true,
|
|
1434
|
+
aiRequired: false
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
id: "road-hopper-rally-m1-safe-preview",
|
|
1438
|
+
statement: "The project stays inside the private educational preview boundary.",
|
|
1439
|
+
visibility: "visible",
|
|
1440
|
+
criterionIds: ["road-hopper-rally-safety"],
|
|
1441
|
+
completionRequired: true,
|
|
1442
|
+
aiRequired: false
|
|
1443
|
+
}
|
|
1444
|
+
],
|
|
1445
|
+
interactions: [
|
|
1446
|
+
{
|
|
1447
|
+
id: "road-hopper-rally-m1-start-control",
|
|
1448
|
+
description: "Start the private preview.",
|
|
1449
|
+
primaryMode: "pointer",
|
|
1450
|
+
alternativeIds: ["road-hopper-rally-m1-keyboard-start"]
|
|
1451
|
+
}
|
|
1452
|
+
],
|
|
1453
|
+
accessibilityAlternatives: [
|
|
1454
|
+
{
|
|
1455
|
+
id: "road-hopper-rally-m1-keyboard-start",
|
|
1456
|
+
modes: ["keyboard"],
|
|
1457
|
+
equivalentOutcome: true,
|
|
1458
|
+
description: "Start the same preview with Enter while the control has focus."
|
|
1459
|
+
}
|
|
1460
|
+
],
|
|
1461
|
+
evidenceRequirements: [
|
|
1462
|
+
{
|
|
1463
|
+
id: "road-hopper-rally-m1-assessment",
|
|
1464
|
+
goalIds: [
|
|
1465
|
+
"road-hopper-rally-m1-starts",
|
|
1466
|
+
"road-hopper-rally-m1-draws-road",
|
|
1467
|
+
"road-hopper-rally-m1-safe-preview"
|
|
1468
|
+
],
|
|
1469
|
+
kind: "assessment-result",
|
|
1470
|
+
retention: "entitlement",
|
|
1471
|
+
containsPersonalData: false
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
id: "road-hopper-rally-m1-explanation",
|
|
1475
|
+
goalIds: ["road-hopper-rally-m1-draws-road"],
|
|
1476
|
+
kind: "learner-explanation",
|
|
1477
|
+
retention: "attempt",
|
|
1478
|
+
containsPersonalData: false
|
|
1479
|
+
}
|
|
1480
|
+
],
|
|
1481
|
+
sideAdventures: [
|
|
1482
|
+
{
|
|
1483
|
+
id: "road-hopper-rally-m1-remix",
|
|
1484
|
+
prompt: "Remix the lane colours while keeping text or shape cues available.",
|
|
1485
|
+
completionRequired: false
|
|
1486
|
+
}
|
|
1487
|
+
],
|
|
1488
|
+
rewardBindings: [
|
|
1489
|
+
{
|
|
1490
|
+
id: "road-hopper-rally-m1-badge",
|
|
1491
|
+
badgeId: "road-hopper-rally-mission-complete",
|
|
1492
|
+
goalIds: [
|
|
1493
|
+
"road-hopper-rally-m1-starts",
|
|
1494
|
+
"road-hopper-rally-m1-draws-road",
|
|
1495
|
+
"road-hopper-rally-m1-safe-preview"
|
|
1496
|
+
],
|
|
1497
|
+
deterministic: true,
|
|
1498
|
+
random: false,
|
|
1499
|
+
tokenConvertible: false
|
|
1500
|
+
}
|
|
1501
|
+
]
|
|
1502
|
+
},
|
|
1503
|
+
facilitator: {
|
|
1504
|
+
artifacts: [
|
|
1505
|
+
{
|
|
1506
|
+
id: "road-hopper-rally-m1-answer-key",
|
|
1507
|
+
kind: "answer-key",
|
|
1508
|
+
audience: "facilitator",
|
|
1509
|
+
solutionBearing: true
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
id: "road-hopper-rally-m1-protected-tests",
|
|
1513
|
+
kind: "protected-test",
|
|
1514
|
+
audience: "facilitator",
|
|
1515
|
+
solutionBearing: true
|
|
1516
|
+
}
|
|
1517
|
+
],
|
|
1518
|
+
protectedGoals: [
|
|
1519
|
+
{
|
|
1520
|
+
id: "road-hopper-rally-m1-protected-edge",
|
|
1521
|
+
statement: "The drawing remains bounded when a protected coordinate edge case runs.",
|
|
1522
|
+
visibility: "protected",
|
|
1523
|
+
criterionIds: ["road-hopper-rally-edge-one"],
|
|
1524
|
+
completionRequired: false,
|
|
1525
|
+
aiRequired: false
|
|
1526
|
+
}
|
|
1527
|
+
],
|
|
1528
|
+
prompts: [
|
|
1529
|
+
"Ask the learner to predict one coordinate before offering a hint.",
|
|
1530
|
+
"Do not reveal protected expected values; point back to the visible goal."
|
|
1531
|
+
]
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// src/validation.ts
|
|
610
1536
|
var CANONICAL_TOKEN_SUBUNITS = /^(0|[1-9][0-9]*)$/u;
|
|
611
1537
|
function issue(code, message, path, moduleId) {
|
|
612
1538
|
return { code, message, path, ...moduleId ? { moduleId } : {} };
|
|
@@ -687,63 +1613,9 @@ function validateModule(module3, moduleIndex) {
|
|
|
687
1613
|
)
|
|
688
1614
|
);
|
|
689
1615
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
behaviour: 0,
|
|
694
|
-
resilience: 0,
|
|
695
|
-
safety: 0
|
|
696
|
-
};
|
|
697
|
-
let rubricTotal = 0;
|
|
698
|
-
for (const criterion of module3.assessment.criteria) {
|
|
699
|
-
rubricTotal += criterion.points;
|
|
700
|
-
dimensionTotals[criterion.dimension] += criterion.points;
|
|
701
|
-
if (criterionIds.has(criterion.id)) {
|
|
702
|
-
issues.push(
|
|
703
|
-
issue(
|
|
704
|
-
"duplicate-criterion-id",
|
|
705
|
-
`Duplicate assessment criterion ${criterion.id}.`,
|
|
706
|
-
`${base}.assessment.criteria`,
|
|
707
|
-
module3.id
|
|
708
|
-
)
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
criterionIds.add(criterion.id);
|
|
712
|
-
}
|
|
713
|
-
if (rubricTotal !== 100) {
|
|
714
|
-
issues.push(
|
|
715
|
-
issue(
|
|
716
|
-
"rubric-total",
|
|
717
|
-
`Assessment rubric totals ${rubricTotal}; expected 100.`,
|
|
718
|
-
`${base}.assessment.criteria`,
|
|
719
|
-
module3.id
|
|
720
|
-
)
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
for (const [dimension, expected] of Object.entries(DIMENSION_TOTALS)) {
|
|
724
|
-
if (dimensionTotals[dimension] !== expected) {
|
|
725
|
-
issues.push(
|
|
726
|
-
issue(
|
|
727
|
-
"rubric-dimension-total",
|
|
728
|
-
`${dimension} criteria total ${dimensionTotals[dimension]}; expected ${expected}.`,
|
|
729
|
-
`${base}.assessment.criteria`,
|
|
730
|
-
module3.id
|
|
731
|
-
)
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
if (!module3.assessment.criteria.some(
|
|
736
|
-
(criterion) => criterion.dimension === "safety" && criterion.mandatory
|
|
737
|
-
)) {
|
|
738
|
-
issues.push(
|
|
739
|
-
issue(
|
|
740
|
-
"missing-mandatory-safety",
|
|
741
|
-
"Every module requires a mandatory safety criterion.",
|
|
742
|
-
`${base}.assessment.criteria`,
|
|
743
|
-
module3.id
|
|
744
|
-
)
|
|
745
|
-
);
|
|
746
|
-
}
|
|
1616
|
+
issues.push(
|
|
1617
|
+
...validateAssessmentRubric(module3.assessment, `${base}.assessment`, module3.id)
|
|
1618
|
+
);
|
|
747
1619
|
if (module3.hardware.mode === "physical-first" && module3.hardware.items.length === 0) {
|
|
748
1620
|
issues.push(
|
|
749
1621
|
issue(
|
|
@@ -818,12 +1690,19 @@ ${summary}`);
|
|
|
818
1690
|
}
|
|
819
1691
|
// Annotate the CommonJS export names for ESM import in node:
|
|
820
1692
|
0 && (module.exports = {
|
|
1693
|
+
JUNIOR_CODER_MISSION_STAGE_ORDER_V1,
|
|
821
1694
|
JUNIOR_CODER_MODULE_PRICE_V1_1,
|
|
822
1695
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_CURRENT,
|
|
823
1696
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_V1,
|
|
824
1697
|
JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_1,
|
|
1698
|
+
MISSION_AUTHORING_CONTRACT_VERSION_V1,
|
|
1699
|
+
ROAD_HOPPER_RALLY_MISSION_ONE_AUTHORING_V1,
|
|
1700
|
+
ROBOT_MAZE_DASH_MISSION_ONE_AUTHORING_V1,
|
|
825
1701
|
assertValidLearningPath,
|
|
1702
|
+
assertValidMissionAuthoringBundle,
|
|
826
1703
|
calculateAssessment,
|
|
827
|
-
|
|
1704
|
+
validateAssessmentRubric,
|
|
1705
|
+
validateLearningPath,
|
|
1706
|
+
validateMissionAuthoringBundle
|
|
828
1707
|
});
|
|
829
1708
|
//# sourceMappingURL=index.cjs.map
|