@plasius/learning 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -532,6 +532,15 @@ var modules = [
532
532
  missionTitles: ["Build the Control Panel", "Simulate Telemetry", "Add Stop Confirmations", "Mission Control Challenge"]
533
533
  })
534
534
  ];
535
+ var JUNIOR_CODER_MODULE_PRICE_V1_1 = Object.freeze({
536
+ tokenSubunits: "50000",
537
+ referencePrice: Object.freeze({
538
+ currency: "GBP",
539
+ minorUnits: "500",
540
+ basis: "nominal-reference",
541
+ cashRedemptionAllowed: false
542
+ })
543
+ });
535
544
  var JUNIOR_CODER_ROBOT_RESCUE_PATH_V1 = {
536
545
  id: "junior-coder.robot-rescue-arcade",
537
546
  slug: "robot-rescue-arcade",
@@ -543,14 +552,734 @@ var JUNIOR_CODER_ROBOT_RESCUE_PATH_V1 = {
543
552
  featureFlag: "learning.junior-coder.catalog.enabled",
544
553
  modules
545
554
  };
555
+ var JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_1 = {
556
+ ...JUNIOR_CODER_ROBOT_RESCUE_PATH_V1,
557
+ version: "1.1.0",
558
+ modules: JUNIOR_CODER_ROBOT_RESCUE_PATH_V1.modules.map((entry) => ({
559
+ ...entry,
560
+ version: "1.1.0",
561
+ contentRevision: "2026-07-28.1",
562
+ pricing: {
563
+ ...entry.pricing,
564
+ tokenSubunits: JUNIOR_CODER_MODULE_PRICE_V1_1.tokenSubunits,
565
+ referencePrice: JUNIOR_CODER_MODULE_PRICE_V1_1.referencePrice
566
+ }
567
+ }))
568
+ };
569
+ var JUNIOR_CODER_ROBOT_RESCUE_PATH_CURRENT = JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_1;
546
570
 
547
- // src/validation.ts
571
+ // src/contracts.ts
572
+ var MISSION_AUTHORING_CONTRACT_VERSION_V1 = "1.0.0";
573
+
574
+ // src/rubric-validation.ts
548
575
  var DIMENSION_TOTALS = {
549
576
  structure: 20,
550
577
  behaviour: 50,
551
578
  resilience: 20,
552
579
  safety: 10
553
580
  };
581
+ function rubricIssue(code, message, path, moduleId) {
582
+ return { code, message, path, ...moduleId ? { moduleId } : {} };
583
+ }
584
+ function validateAssessmentRubric(rubric2, path = "assessment", moduleId) {
585
+ const issues = [];
586
+ const criterionIds = /* @__PURE__ */ new Set();
587
+ const dimensionTotals = {
588
+ structure: 0,
589
+ behaviour: 0,
590
+ resilience: 0,
591
+ safety: 0
592
+ };
593
+ let rubricTotal = 0;
594
+ for (const criterion of rubric2.criteria) {
595
+ rubricTotal += criterion.points;
596
+ dimensionTotals[criterion.dimension] += criterion.points;
597
+ if (criterionIds.has(criterion.id)) {
598
+ issues.push(
599
+ rubricIssue(
600
+ "duplicate-criterion-id",
601
+ `Duplicate assessment criterion ${criterion.id}.`,
602
+ `${path}.criteria`,
603
+ moduleId
604
+ )
605
+ );
606
+ }
607
+ criterionIds.add(criterion.id);
608
+ }
609
+ if (rubricTotal !== 100) {
610
+ issues.push(
611
+ rubricIssue(
612
+ "rubric-total",
613
+ `Assessment rubric totals ${rubricTotal}; expected 100.`,
614
+ `${path}.criteria`,
615
+ moduleId
616
+ )
617
+ );
618
+ }
619
+ for (const [dimension, expected] of Object.entries(DIMENSION_TOTALS)) {
620
+ if (dimensionTotals[dimension] !== expected) {
621
+ issues.push(
622
+ rubricIssue(
623
+ "rubric-dimension-total",
624
+ `${dimension} criteria total ${dimensionTotals[dimension]}; expected ${expected}.`,
625
+ `${path}.criteria`,
626
+ moduleId
627
+ )
628
+ );
629
+ }
630
+ }
631
+ if (!rubric2.criteria.some(
632
+ (criterion) => criterion.dimension === "safety" && criterion.mandatory
633
+ )) {
634
+ issues.push(
635
+ rubricIssue(
636
+ "missing-mandatory-safety",
637
+ "Every module requires a mandatory safety criterion.",
638
+ `${path}.criteria`,
639
+ moduleId
640
+ )
641
+ );
642
+ }
643
+ return issues;
644
+ }
645
+
646
+ // src/mission-authoring.ts
647
+ var JUNIOR_CODER_MISSION_STAGE_ORDER_V1 = [
648
+ "learn",
649
+ "predict",
650
+ "build",
651
+ "run",
652
+ "assess",
653
+ "inspect",
654
+ "fix",
655
+ "explain",
656
+ "reward"
657
+ ];
658
+ var LEARNER_STARTER_KINDS = /* @__PURE__ */ new Set([
659
+ "starter-code",
660
+ "starter-assets",
661
+ "sample-data"
662
+ ]);
663
+ var LEARNER_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
664
+ "facilitator-note",
665
+ "answer-key",
666
+ "protected-test"
667
+ ]);
668
+ var SINGLE_MODE_REQUIRES_ALTERNATIVE = /* @__PURE__ */ new Set([
669
+ "pointer",
670
+ "drag",
671
+ "audio",
672
+ "colour",
673
+ "motion"
674
+ ]);
675
+ function authoringIssue(code, message, path) {
676
+ return { code, message, path };
677
+ }
678
+ function reportDuplicateIds(ids, path) {
679
+ const seen = /* @__PURE__ */ new Set();
680
+ const issues = [];
681
+ for (const id of ids) {
682
+ if (seen.has(id)) {
683
+ issues.push(
684
+ authoringIssue("duplicate-id", `Duplicate authored ID ${id}.`, path)
685
+ );
686
+ }
687
+ seen.add(id);
688
+ }
689
+ return issues;
690
+ }
691
+ function validateMissionAuthoringBundle(bundle, module2) {
692
+ const issues = [];
693
+ if (bundle.version !== MISSION_AUTHORING_CONTRACT_VERSION_V1) {
694
+ issues.push(
695
+ authoringIssue(
696
+ "bundle-version-mismatch",
697
+ `Unsupported mission authoring version ${bundle.version}.`,
698
+ "version"
699
+ )
700
+ );
701
+ }
702
+ if (bundle.moduleId !== module2.id || bundle.moduleVersion !== module2.version) {
703
+ issues.push(
704
+ authoringIssue(
705
+ "module-reference-mismatch",
706
+ `Bundle ${bundle.moduleId}@${bundle.moduleVersion} does not match ${module2.id}@${module2.version}.`,
707
+ "moduleId"
708
+ )
709
+ );
710
+ }
711
+ if (!module2.missions.some((mission2) => mission2.id === bundle.missionId)) {
712
+ issues.push(
713
+ authoringIssue(
714
+ "mission-reference-mismatch",
715
+ `Mission ${bundle.missionId} does not exist in module ${module2.id}.`,
716
+ "missionId"
717
+ )
718
+ );
719
+ }
720
+ const learner = bundle.learner;
721
+ const facilitator = bundle.facilitator;
722
+ if (learner.estimatedMinutes < 15 || learner.estimatedMinutes > 25) {
723
+ issues.push(
724
+ authoringIssue(
725
+ "invalid-duration",
726
+ "A mission must last between 15 and 25 minutes.",
727
+ "learner.estimatedMinutes"
728
+ )
729
+ );
730
+ }
731
+ const stageKinds = learner.stages.map((stage) => stage.kind);
732
+ for (const requiredStage of JUNIOR_CODER_MISSION_STAGE_ORDER_V1) {
733
+ const count = stageKinds.filter((stage) => stage === requiredStage).length;
734
+ if (count === 0) {
735
+ issues.push(
736
+ authoringIssue(
737
+ "missing-stage",
738
+ `Mission stage ${requiredStage} is required.`,
739
+ "learner.stages"
740
+ )
741
+ );
742
+ } else if (count > 1) {
743
+ issues.push(
744
+ authoringIssue(
745
+ "duplicate-stage",
746
+ `Mission stage ${requiredStage} appears more than once.`,
747
+ "learner.stages"
748
+ )
749
+ );
750
+ }
751
+ }
752
+ if (stageKinds.length === JUNIOR_CODER_MISSION_STAGE_ORDER_V1.length && stageKinds.some(
753
+ (stage, index) => stage !== JUNIOR_CODER_MISSION_STAGE_ORDER_V1[index]
754
+ )) {
755
+ issues.push(
756
+ authoringIssue(
757
+ "stage-order",
758
+ "Mission stages must follow the canonical learner journey.",
759
+ "learner.stages"
760
+ )
761
+ );
762
+ }
763
+ if (learner.readinessChecks.length === 0) {
764
+ issues.push(
765
+ authoringIssue(
766
+ "missing-readiness-check",
767
+ "At least one unscored readiness check is required.",
768
+ "learner.readinessChecks"
769
+ )
770
+ );
771
+ }
772
+ if (learner.readinessChecks.some((check) => check.scored !== false)) {
773
+ issues.push(
774
+ authoringIssue(
775
+ "scored-readiness-check",
776
+ "Readiness checks must not affect the deterministic score.",
777
+ "learner.readinessChecks"
778
+ )
779
+ );
780
+ }
781
+ issues.push(
782
+ ...reportDuplicateIds(
783
+ learner.readinessChecks.map((check) => check.id),
784
+ "learner.readinessChecks"
785
+ )
786
+ );
787
+ const learnerArtifactIds = new Set(learner.artifacts.map((artifact) => artifact.id));
788
+ if (!learner.artifacts.some((artifact) => LEARNER_STARTER_KINDS.has(artifact.kind))) {
789
+ issues.push(
790
+ authoringIssue(
791
+ "missing-starter-artifact",
792
+ "At least one learner-safe starter artifact is required.",
793
+ "learner.artifacts"
794
+ )
795
+ );
796
+ }
797
+ if (learner.artifacts.some(
798
+ (artifact) => artifact.audience !== "learner" || artifact.solutionBearing || LEARNER_FORBIDDEN_KINDS.has(artifact.kind)
799
+ )) {
800
+ issues.push(
801
+ authoringIssue(
802
+ "learner-artifact-leak",
803
+ "Learner artifacts cannot contain facilitator or solution-bearing content.",
804
+ "learner.artifacts"
805
+ )
806
+ );
807
+ }
808
+ if (facilitator.artifacts.some((artifact) => artifact.audience !== "facilitator")) {
809
+ issues.push(
810
+ authoringIssue(
811
+ "facilitator-artifact-leak",
812
+ "Facilitator artifacts must remain in the facilitator projection.",
813
+ "facilitator.artifacts"
814
+ )
815
+ );
816
+ }
817
+ issues.push(
818
+ ...reportDuplicateIds(
819
+ [...learner.artifacts, ...facilitator.artifacts].map((artifact) => artifact.id),
820
+ "artifacts"
821
+ )
822
+ );
823
+ for (const [stageIndex, stage] of learner.stages.entries()) {
824
+ for (const artifactId of stage.artifactIds) {
825
+ if (!learnerArtifactIds.has(artifactId)) {
826
+ issues.push(
827
+ authoringIssue(
828
+ "unknown-artifact",
829
+ `Stage references unknown learner artifact ${artifactId}.`,
830
+ `learner.stages[${stageIndex}].artifactIds`
831
+ )
832
+ );
833
+ }
834
+ }
835
+ }
836
+ if (learner.goals.length === 0) {
837
+ issues.push(
838
+ authoringIssue(
839
+ "missing-visible-goal",
840
+ "At least one visible learner goal is required.",
841
+ "learner.goals"
842
+ )
843
+ );
844
+ }
845
+ if (facilitator.protectedGoals.length === 0) {
846
+ issues.push(
847
+ authoringIssue(
848
+ "missing-protected-goal",
849
+ "At least one protected facilitator goal is required.",
850
+ "facilitator.protectedGoals"
851
+ )
852
+ );
853
+ }
854
+ const allGoals = [...learner.goals, ...facilitator.protectedGoals];
855
+ const learnerGoalIds = new Set(learner.goals.map((goal) => goal.id));
856
+ const seenGoalIds = /* @__PURE__ */ new Set();
857
+ for (const goal of allGoals) {
858
+ if (seenGoalIds.has(goal.id)) {
859
+ issues.push(
860
+ authoringIssue(
861
+ "duplicate-goal-id",
862
+ `Duplicate goal ID ${goal.id}.`,
863
+ "goals"
864
+ )
865
+ );
866
+ }
867
+ seenGoalIds.add(goal.id);
868
+ }
869
+ if (learner.goals.some((goal) => goal.visibility !== "visible") || facilitator.protectedGoals.some(
870
+ (goal) => goal.visibility !== "protected" || goal.completionRequired
871
+ )) {
872
+ issues.push(
873
+ authoringIssue(
874
+ "invalid-goal-projection",
875
+ "Visible goals belong to learners and protected goals to facilitators.",
876
+ "goals"
877
+ )
878
+ );
879
+ }
880
+ const criterionById = new Map(
881
+ module2.assessment.criteria.map((criterion) => [criterion.id, criterion])
882
+ );
883
+ for (const goal of allGoals) {
884
+ if (goal.criterionIds.length === 0) {
885
+ issues.push(
886
+ authoringIssue(
887
+ "unknown-criterion",
888
+ `Goal ${goal.id} must reference a deterministic criterion.`,
889
+ "goals"
890
+ )
891
+ );
892
+ }
893
+ for (const criterionId of goal.criterionIds) {
894
+ const criterion = criterionById.get(criterionId);
895
+ if (!criterion) {
896
+ issues.push(
897
+ authoringIssue(
898
+ "unknown-criterion",
899
+ `Goal ${goal.id} references unknown criterion ${criterionId}.`,
900
+ "goals"
901
+ )
902
+ );
903
+ } else if (criterion.visibility !== goal.visibility) {
904
+ issues.push(
905
+ authoringIssue(
906
+ "criterion-visibility-mismatch",
907
+ `Goal ${goal.id} cannot expose a ${criterion.visibility} criterion as ${goal.visibility}.`,
908
+ "goals"
909
+ )
910
+ );
911
+ }
912
+ }
913
+ if (goal.completionRequired && goal.aiRequired) {
914
+ issues.push(
915
+ authoringIssue(
916
+ "ai-dependent-completion",
917
+ `Completion goal ${goal.id} cannot require AI.`,
918
+ "goals"
919
+ )
920
+ );
921
+ }
922
+ }
923
+ for (const rubricIssue2 of validateAssessmentRubric(module2.assessment)) {
924
+ if (rubricIssue2.code === "rubric-total" || rubricIssue2.code === "rubric-dimension-total" || rubricIssue2.code === "duplicate-criterion-id" || rubricIssue2.code === "missing-mandatory-safety") {
925
+ issues.push(
926
+ authoringIssue(rubricIssue2.code, rubricIssue2.message, rubricIssue2.path)
927
+ );
928
+ }
929
+ }
930
+ const alternativeById = new Map(
931
+ learner.accessibilityAlternatives.map((alternative) => [alternative.id, alternative])
932
+ );
933
+ issues.push(
934
+ ...reportDuplicateIds(
935
+ learner.interactions.map((interaction) => interaction.id),
936
+ "learner.interactions"
937
+ ),
938
+ ...reportDuplicateIds(
939
+ learner.accessibilityAlternatives.map((alternative) => alternative.id),
940
+ "learner.accessibilityAlternatives"
941
+ )
942
+ );
943
+ for (const [interactionIndex, interaction] of learner.interactions.entries()) {
944
+ if (SINGLE_MODE_REQUIRES_ALTERNATIVE.has(interaction.primaryMode) && interaction.alternativeIds.length === 0) {
945
+ issues.push(
946
+ authoringIssue(
947
+ "inaccessible-interaction",
948
+ `Interaction ${interaction.id} requires an equivalent alternative.`,
949
+ `learner.interactions[${interactionIndex}]`
950
+ )
951
+ );
952
+ }
953
+ for (const alternativeId of interaction.alternativeIds) {
954
+ const alternative = alternativeById.get(alternativeId);
955
+ if (!alternative) {
956
+ issues.push(
957
+ authoringIssue(
958
+ "unknown-accessibility-alternative",
959
+ `Interaction ${interaction.id} references unknown alternative ${alternativeId}.`,
960
+ `learner.interactions[${interactionIndex}].alternativeIds`
961
+ )
962
+ );
963
+ } else if (alternative.equivalentOutcome !== true || alternative.modes.length === 0 || alternative.modes.every((mode) => mode === interaction.primaryMode)) {
964
+ issues.push(
965
+ authoringIssue(
966
+ "non-equivalent-accessibility-alternative",
967
+ `Alternative ${alternativeId} must provide an equivalent outcome through another mode.`,
968
+ "learner.accessibilityAlternatives"
969
+ )
970
+ );
971
+ }
972
+ }
973
+ }
974
+ if (learner.evidenceRequirements.length === 0) {
975
+ issues.push(
976
+ authoringIssue(
977
+ "missing-evidence",
978
+ "At least one evidence requirement is required.",
979
+ "learner.evidenceRequirements"
980
+ )
981
+ );
982
+ }
983
+ issues.push(
984
+ ...reportDuplicateIds(
985
+ learner.evidenceRequirements.map((evidence) => evidence.id),
986
+ "learner.evidenceRequirements"
987
+ )
988
+ );
989
+ for (const [evidenceIndex, evidence] of learner.evidenceRequirements.entries()) {
990
+ if (evidence.containsPersonalData !== false) {
991
+ issues.push(
992
+ authoringIssue(
993
+ "personal-data-evidence",
994
+ "Mission evidence cannot request personal data.",
995
+ `learner.evidenceRequirements[${evidenceIndex}]`
996
+ )
997
+ );
998
+ }
999
+ for (const goalId of evidence.goalIds) {
1000
+ if (!learnerGoalIds.has(goalId)) {
1001
+ issues.push(
1002
+ authoringIssue(
1003
+ "unknown-evidence-goal",
1004
+ `Evidence references unknown goal ${goalId}.`,
1005
+ `learner.evidenceRequirements[${evidenceIndex}].goalIds`
1006
+ )
1007
+ );
1008
+ }
1009
+ }
1010
+ }
1011
+ for (const goal of learner.goals.filter((entry) => entry.completionRequired)) {
1012
+ if (!learner.evidenceRequirements.some((evidence) => evidence.goalIds.includes(goal.id))) {
1013
+ issues.push(
1014
+ authoringIssue(
1015
+ "missing-evidence",
1016
+ `Completion goal ${goal.id} requires deterministic evidence.`,
1017
+ "learner.evidenceRequirements"
1018
+ )
1019
+ );
1020
+ }
1021
+ }
1022
+ const mandatorySafetyGoals = learner.goals.filter(
1023
+ (goal) => goal.completionRequired && goal.criterionIds.some((criterionId) => {
1024
+ const criterion = criterionById.get(criterionId);
1025
+ return criterion?.dimension === "safety" && criterion.mandatory;
1026
+ })
1027
+ );
1028
+ if (mandatorySafetyGoals.length === 0 || !mandatorySafetyGoals.some(
1029
+ (goal) => learner.evidenceRequirements.some((evidence) => evidence.goalIds.includes(goal.id))
1030
+ )) {
1031
+ issues.push(
1032
+ authoringIssue(
1033
+ "missing-safety-evidence",
1034
+ "A completion-required goal must evidence a mandatory safety criterion.",
1035
+ "learner.evidenceRequirements"
1036
+ )
1037
+ );
1038
+ }
1039
+ issues.push(
1040
+ ...reportDuplicateIds(
1041
+ learner.sideAdventures.map((adventure) => adventure.id),
1042
+ "learner.sideAdventures"
1043
+ )
1044
+ );
1045
+ if (learner.sideAdventures.length === 0) {
1046
+ issues.push(
1047
+ authoringIssue(
1048
+ "missing-side-adventure",
1049
+ "At least one optional side adventure is required.",
1050
+ "learner.sideAdventures"
1051
+ )
1052
+ );
1053
+ }
1054
+ if (learner.sideAdventures.some((adventure) => adventure.completionRequired !== false)) {
1055
+ issues.push(
1056
+ authoringIssue(
1057
+ "mandatory-side-adventure",
1058
+ "Side adventures must remain optional.",
1059
+ "learner.sideAdventures"
1060
+ )
1061
+ );
1062
+ }
1063
+ const badgeIds = new Set(module2.badges.map((badge) => badge.id));
1064
+ issues.push(
1065
+ ...reportDuplicateIds(
1066
+ learner.rewardBindings.map((reward) => reward.id),
1067
+ "learner.rewardBindings"
1068
+ )
1069
+ );
1070
+ for (const [rewardIndex, reward] of learner.rewardBindings.entries()) {
1071
+ 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));
1072
+ if (rewardInvalid) {
1073
+ issues.push(
1074
+ authoringIssue(
1075
+ "invalid-reward",
1076
+ `Reward ${reward.id} must be deterministic, evidence-bound and non-convertible.`,
1077
+ `learner.rewardBindings[${rewardIndex}]`
1078
+ )
1079
+ );
1080
+ }
1081
+ }
1082
+ return issues;
1083
+ }
1084
+ function assertValidMissionAuthoringBundle(bundle, module2) {
1085
+ const issues = validateMissionAuthoringBundle(bundle, module2);
1086
+ if (issues.length === 0) return;
1087
+ const summary = issues.map((entry) => `${entry.code} at ${entry.path}: ${entry.message}`).join("\n");
1088
+ throw new Error(`Invalid mission authoring bundle:
1089
+ ${summary}`);
1090
+ }
1091
+ var ROAD_HOPPER_RALLY_MISSION_ONE_AUTHORING_V1 = {
1092
+ version: MISSION_AUTHORING_CONTRACT_VERSION_V1,
1093
+ moduleId: "junior-coder.road-hopper-rally",
1094
+ moduleVersion: "1.1.0",
1095
+ missionId: "road-hopper-rally-mission-1",
1096
+ learner: {
1097
+ estimatedMinutes: 20,
1098
+ stages: [
1099
+ {
1100
+ kind: "learn",
1101
+ instruction: "Find the x and y coordinates that place a rescue marker on the road.",
1102
+ artifactIds: ["road-hopper-rally-m1-art"]
1103
+ },
1104
+ {
1105
+ kind: "predict",
1106
+ instruction: "Predict where the marker will appear before you run the starter project.",
1107
+ artifactIds: []
1108
+ },
1109
+ {
1110
+ kind: "build",
1111
+ instruction: "Complete the two road-lane drawing commands in the starter code.",
1112
+ artifactIds: ["road-hopper-rally-m1-code"]
1113
+ },
1114
+ {
1115
+ kind: "run",
1116
+ instruction: "Run the private preview and use the keyboard start control.",
1117
+ artifactIds: ["road-hopper-rally-m1-code"]
1118
+ },
1119
+ {
1120
+ kind: "assess",
1121
+ instruction: "Run the visible and protected deterministic mission checks.",
1122
+ artifactIds: []
1123
+ },
1124
+ {
1125
+ kind: "inspect",
1126
+ instruction: "Compare the highlighted line with the goal that did not pass.",
1127
+ artifactIds: []
1128
+ },
1129
+ {
1130
+ kind: "fix",
1131
+ instruction: "Change one coordinate or drawing command, then run the checks again.",
1132
+ artifactIds: ["road-hopper-rally-m1-code"]
1133
+ },
1134
+ {
1135
+ kind: "explain",
1136
+ instruction: "Explain how your coordinates changed the road on screen.",
1137
+ artifactIds: []
1138
+ },
1139
+ {
1140
+ kind: "reward",
1141
+ instruction: "Collect the evidence-bound badge when the mission score and safety check pass.",
1142
+ artifactIds: []
1143
+ }
1144
+ ],
1145
+ readinessChecks: [
1146
+ {
1147
+ id: "road-hopper-rally-m1-predict-coordinate",
1148
+ prompt: "Point to the pair of numbers that controls horizontal and vertical position.",
1149
+ scored: false
1150
+ }
1151
+ ],
1152
+ artifacts: [
1153
+ {
1154
+ id: "road-hopper-rally-m1-code",
1155
+ kind: "starter-code",
1156
+ audience: "learner",
1157
+ solutionBearing: false
1158
+ },
1159
+ {
1160
+ id: "road-hopper-rally-m1-art",
1161
+ kind: "starter-assets",
1162
+ audience: "learner",
1163
+ solutionBearing: false
1164
+ }
1165
+ ],
1166
+ goals: [
1167
+ {
1168
+ id: "road-hopper-rally-m1-starts",
1169
+ statement: "The starter project is structurally valid and starts.",
1170
+ visibility: "visible",
1171
+ criterionIds: ["road-hopper-rally-build"],
1172
+ completionRequired: true,
1173
+ aiRequired: false
1174
+ },
1175
+ {
1176
+ id: "road-hopper-rally-m1-draws-road",
1177
+ statement: "Two original road lanes appear at the expected coordinates.",
1178
+ visibility: "visible",
1179
+ criterionIds: ["road-hopper-rally-goal-one"],
1180
+ completionRequired: true,
1181
+ aiRequired: false
1182
+ },
1183
+ {
1184
+ id: "road-hopper-rally-m1-safe-preview",
1185
+ statement: "The project stays inside the private educational preview boundary.",
1186
+ visibility: "visible",
1187
+ criterionIds: ["road-hopper-rally-safety"],
1188
+ completionRequired: true,
1189
+ aiRequired: false
1190
+ }
1191
+ ],
1192
+ interactions: [
1193
+ {
1194
+ id: "road-hopper-rally-m1-start-control",
1195
+ description: "Start the private preview.",
1196
+ primaryMode: "pointer",
1197
+ alternativeIds: ["road-hopper-rally-m1-keyboard-start"]
1198
+ }
1199
+ ],
1200
+ accessibilityAlternatives: [
1201
+ {
1202
+ id: "road-hopper-rally-m1-keyboard-start",
1203
+ modes: ["keyboard"],
1204
+ equivalentOutcome: true,
1205
+ description: "Start the same preview with Enter while the control has focus."
1206
+ }
1207
+ ],
1208
+ evidenceRequirements: [
1209
+ {
1210
+ id: "road-hopper-rally-m1-assessment",
1211
+ goalIds: [
1212
+ "road-hopper-rally-m1-starts",
1213
+ "road-hopper-rally-m1-draws-road",
1214
+ "road-hopper-rally-m1-safe-preview"
1215
+ ],
1216
+ kind: "assessment-result",
1217
+ retention: "entitlement",
1218
+ containsPersonalData: false
1219
+ },
1220
+ {
1221
+ id: "road-hopper-rally-m1-explanation",
1222
+ goalIds: ["road-hopper-rally-m1-draws-road"],
1223
+ kind: "learner-explanation",
1224
+ retention: "attempt",
1225
+ containsPersonalData: false
1226
+ }
1227
+ ],
1228
+ sideAdventures: [
1229
+ {
1230
+ id: "road-hopper-rally-m1-remix",
1231
+ prompt: "Remix the lane colours while keeping text or shape cues available.",
1232
+ completionRequired: false
1233
+ }
1234
+ ],
1235
+ rewardBindings: [
1236
+ {
1237
+ id: "road-hopper-rally-m1-badge",
1238
+ badgeId: "road-hopper-rally-mission-complete",
1239
+ goalIds: [
1240
+ "road-hopper-rally-m1-starts",
1241
+ "road-hopper-rally-m1-draws-road",
1242
+ "road-hopper-rally-m1-safe-preview"
1243
+ ],
1244
+ deterministic: true,
1245
+ random: false,
1246
+ tokenConvertible: false
1247
+ }
1248
+ ]
1249
+ },
1250
+ facilitator: {
1251
+ artifacts: [
1252
+ {
1253
+ id: "road-hopper-rally-m1-answer-key",
1254
+ kind: "answer-key",
1255
+ audience: "facilitator",
1256
+ solutionBearing: true
1257
+ },
1258
+ {
1259
+ id: "road-hopper-rally-m1-protected-tests",
1260
+ kind: "protected-test",
1261
+ audience: "facilitator",
1262
+ solutionBearing: true
1263
+ }
1264
+ ],
1265
+ protectedGoals: [
1266
+ {
1267
+ id: "road-hopper-rally-m1-protected-edge",
1268
+ statement: "The drawing remains bounded when a protected coordinate edge case runs.",
1269
+ visibility: "protected",
1270
+ criterionIds: ["road-hopper-rally-edge-one"],
1271
+ completionRequired: false,
1272
+ aiRequired: false
1273
+ }
1274
+ ],
1275
+ prompts: [
1276
+ "Ask the learner to predict one coordinate before offering a hint.",
1277
+ "Do not reveal protected expected values; point back to the visible goal."
1278
+ ]
1279
+ }
1280
+ };
1281
+
1282
+ // src/validation.ts
554
1283
  var CANONICAL_TOKEN_SUBUNITS = /^(0|[1-9][0-9]*)$/u;
555
1284
  function issue(code, message, path, moduleId) {
556
1285
  return { code, message, path, ...moduleId ? { moduleId } : {} };
@@ -620,63 +1349,20 @@ function validateModule(module2, moduleIndex) {
620
1349
  )
621
1350
  );
622
1351
  }
623
- const criterionIds = /* @__PURE__ */ new Set();
624
- const dimensionTotals = {
625
- structure: 0,
626
- behaviour: 0,
627
- resilience: 0,
628
- safety: 0
629
- };
630
- let rubricTotal = 0;
631
- for (const criterion of module2.assessment.criteria) {
632
- rubricTotal += criterion.points;
633
- dimensionTotals[criterion.dimension] += criterion.points;
634
- if (criterionIds.has(criterion.id)) {
635
- issues.push(
636
- issue(
637
- "duplicate-criterion-id",
638
- `Duplicate assessment criterion ${criterion.id}.`,
639
- `${base}.assessment.criteria`,
640
- module2.id
641
- )
642
- );
643
- }
644
- criterionIds.add(criterion.id);
645
- }
646
- if (rubricTotal !== 100) {
647
- issues.push(
648
- issue(
649
- "rubric-total",
650
- `Assessment rubric totals ${rubricTotal}; expected 100.`,
651
- `${base}.assessment.criteria`,
652
- module2.id
653
- )
654
- );
655
- }
656
- for (const [dimension, expected] of Object.entries(DIMENSION_TOTALS)) {
657
- if (dimensionTotals[dimension] !== expected) {
658
- issues.push(
659
- issue(
660
- "rubric-dimension-total",
661
- `${dimension} criteria total ${dimensionTotals[dimension]}; expected ${expected}.`,
662
- `${base}.assessment.criteria`,
663
- module2.id
664
- )
665
- );
666
- }
667
- }
668
- if (!module2.assessment.criteria.some(
669
- (criterion) => criterion.dimension === "safety" && criterion.mandatory
670
- )) {
1352
+ const referencePrice = module2.pricing.referencePrice;
1353
+ if (referencePrice && (referencePrice.currency !== "GBP" || !CANONICAL_TOKEN_SUBUNITS.test(referencePrice.minorUnits) || referencePrice.basis !== "nominal-reference" || referencePrice.cashRedemptionAllowed !== false)) {
671
1354
  issues.push(
672
1355
  issue(
673
- "missing-mandatory-safety",
674
- "Every module requires a mandatory safety criterion.",
675
- `${base}.assessment.criteria`,
1356
+ "invalid-reference-price",
1357
+ "Reference prices must use canonical GBP minor units, the nominal reference basis and prohibit cash redemption.",
1358
+ `${base}.pricing.referencePrice`,
676
1359
  module2.id
677
1360
  )
678
1361
  );
679
1362
  }
1363
+ issues.push(
1364
+ ...validateAssessmentRubric(module2.assessment, `${base}.assessment`, module2.id)
1365
+ );
680
1366
  if (module2.hardware.mode === "physical-first" && module2.hardware.items.length === 0) {
681
1367
  issues.push(
682
1368
  issue(
@@ -750,9 +1436,18 @@ function assertValidLearningPath(path) {
750
1436
  ${summary}`);
751
1437
  }
752
1438
  export {
1439
+ JUNIOR_CODER_MISSION_STAGE_ORDER_V1,
1440
+ JUNIOR_CODER_MODULE_PRICE_V1_1,
1441
+ JUNIOR_CODER_ROBOT_RESCUE_PATH_CURRENT,
753
1442
  JUNIOR_CODER_ROBOT_RESCUE_PATH_V1,
1443
+ JUNIOR_CODER_ROBOT_RESCUE_PATH_V1_1,
1444
+ MISSION_AUTHORING_CONTRACT_VERSION_V1,
1445
+ ROAD_HOPPER_RALLY_MISSION_ONE_AUTHORING_V1,
754
1446
  assertValidLearningPath,
1447
+ assertValidMissionAuthoringBundle,
755
1448
  calculateAssessment,
756
- validateLearningPath
1449
+ validateAssessmentRubric,
1450
+ validateLearningPath,
1451
+ validateMissionAuthoringBundle
757
1452
  };
758
1453
  //# sourceMappingURL=index.js.map