@forwardimpact/model 0.5.0 → 0.7.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.
@@ -12,20 +12,46 @@ import {
12
12
  Capability,
13
13
  } from "@forwardimpact/schema/levels";
14
14
 
15
+ import {
16
+ WEIGHT_SKILL_TYPE,
17
+ WEIGHT_CAPABILITY_BOOST,
18
+ WEIGHT_BEHAVIOUR_BASE,
19
+ WEIGHT_BEHAVIOUR_MATURITY,
20
+ WEIGHT_SKILL_LEVEL,
21
+ WEIGHT_BELOW_LEVEL_PENALTY,
22
+ RATIO_SKILL_BEHAVIOUR,
23
+ } from "./policies/thresholds.js";
24
+
15
25
  /**
16
26
  * Default question time estimate if not specified
17
27
  */
18
28
  const DEFAULT_QUESTION_MINUTES = 5;
19
29
 
30
+ /**
31
+ * Default decomposition question time estimate
32
+ */
33
+ const DEFAULT_DECOMPOSITION_MINUTES = 15;
34
+
35
+ /**
36
+ * Default stakeholder simulation question time estimate
37
+ */
38
+ const DEFAULT_SIMULATION_MINUTES = 20;
39
+
20
40
  /**
21
41
  * Get questions from the question bank for a specific skill and level
22
42
  * @param {import('./levels.js').QuestionBank} questionBank - The question bank
23
43
  * @param {string} skillId - The skill ID
24
44
  * @param {string} level - The skill level
45
+ * @param {string} [roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
25
46
  * @returns {import('./levels.js').Question[]} Array of questions
26
47
  */
27
- function getSkillQuestions(questionBank, skillId, level) {
28
- return questionBank.skillLevels?.[skillId]?.[level] || [];
48
+ function getSkillQuestions(
49
+ questionBank,
50
+ skillId,
51
+ level,
52
+ roleType = "professionalQuestions",
53
+ ) {
54
+ return questionBank.skillLevels?.[skillId]?.[roleType]?.[level] || [];
29
55
  }
30
56
 
31
57
  /**
@@ -33,10 +59,64 @@ function getSkillQuestions(questionBank, skillId, level) {
33
59
  * @param {import('./levels.js').QuestionBank} questionBank - The question bank
34
60
  * @param {string} behaviourId - The behaviour ID
35
61
  * @param {string} maturity - The maturity level
62
+ * @param {string} [roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
36
63
  * @returns {import('./levels.js').Question[]} Array of questions
37
64
  */
38
- function getBehaviourQuestions(questionBank, behaviourId, maturity) {
39
- return questionBank.behaviourMaturities?.[behaviourId]?.[maturity] || [];
65
+ function getBehaviourQuestions(
66
+ questionBank,
67
+ behaviourId,
68
+ maturity,
69
+ roleType = "professionalQuestions",
70
+ ) {
71
+ return (
72
+ questionBank.behaviourMaturities?.[behaviourId]?.[roleType]?.[maturity] ||
73
+ []
74
+ );
75
+ }
76
+
77
+ /**
78
+ * Get decomposition questions from the question bank for a specific capability and level
79
+ * @param {import('./levels.js').QuestionBank} questionBank - The question bank
80
+ * @param {string} capabilityId - The capability ID
81
+ * @param {string} level - The skill level (capabilities use same levels as skills)
82
+ * @param {string} [roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
83
+ * @returns {import('./levels.js').Question[]} Array of questions
84
+ */
85
+ function getCapabilityQuestions(
86
+ questionBank,
87
+ capabilityId,
88
+ level,
89
+ roleType = "professionalQuestions",
90
+ ) {
91
+ return (
92
+ questionBank.capabilityLevels?.[capabilityId]?.[roleType]?.[level] || []
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Derive capability levels from a job's skill matrix
98
+ * Uses the maximum skill level in each capability.
99
+ * @param {import('./levels.js').JobDefinition} job - The job definition
100
+ * @returns {Map<string, {capabilityId: string, level: string, levelIndex: number}>} Map of capability to level info
101
+ */
102
+ function deriveCapabilityLevels(job) {
103
+ const capabilityLevels = new Map();
104
+
105
+ for (const skill of job.skillMatrix) {
106
+ const capabilityId = skill.capability;
107
+ const levelIndex = getSkillLevelIndex(skill.level);
108
+
109
+ const existing = capabilityLevels.get(capabilityId);
110
+ if (!existing || levelIndex > existing.levelIndex) {
111
+ capabilityLevels.set(capabilityId, {
112
+ capabilityId,
113
+ level: skill.level,
114
+ levelIndex,
115
+ });
116
+ }
117
+ }
118
+
119
+ return capabilityLevels;
40
120
  }
41
121
 
42
122
  /**
@@ -48,31 +128,25 @@ function getBehaviourQuestions(questionBank, behaviourId, maturity) {
48
128
  function calculateSkillPriority(skill, includeBelowLevel = false) {
49
129
  let priority = 0;
50
130
 
51
- // Primary skills are highest priority
52
- if (skill.type === "primary") {
53
- priority += 30;
54
- } else if (skill.type === "secondary") {
55
- priority += 20;
56
- } else {
57
- priority += 10;
58
- }
131
+ // Skill type priority from policy weights
132
+ priority += WEIGHT_SKILL_TYPE[skill.type] || WEIGHT_SKILL_TYPE.broad;
59
133
 
60
134
  // AI skills get a boost for "AI-era focus"
61
135
  if (skill.capability === Capability.AI) {
62
- priority += 15;
136
+ priority += WEIGHT_CAPABILITY_BOOST.ai;
63
137
  }
64
138
 
65
139
  // Delivery skills are core technical skills
66
140
  if (skill.capability === Capability.DELIVERY) {
67
- priority += 5;
141
+ priority += WEIGHT_CAPABILITY_BOOST.delivery;
68
142
  }
69
143
 
70
144
  // Higher skill level = higher priority
71
- priority += getSkillLevelIndex(skill.level) * 2;
145
+ priority += getSkillLevelIndex(skill.level) * WEIGHT_SKILL_LEVEL;
72
146
 
73
147
  // Below-level questions have lower priority
74
148
  if (includeBelowLevel) {
75
- priority -= 5;
149
+ priority += WEIGHT_BELOW_LEVEL_PENALTY;
76
150
  }
77
151
 
78
152
  return priority;
@@ -84,10 +158,35 @@ function calculateSkillPriority(skill, includeBelowLevel = false) {
84
158
  * @returns {number} Priority score (higher = more important)
85
159
  */
86
160
  function calculateBehaviourPriority(behaviour) {
87
- let priority = 15;
161
+ let priority = WEIGHT_BEHAVIOUR_BASE;
88
162
 
89
163
  // Higher maturity level = higher priority
90
- priority += getBehaviourMaturityIndex(behaviour.maturity) * 3;
164
+ priority +=
165
+ getBehaviourMaturityIndex(behaviour.maturity) * WEIGHT_BEHAVIOUR_MATURITY;
166
+
167
+ return priority;
168
+ }
169
+
170
+ /**
171
+ * Calculate priority for a capability decomposition question
172
+ * @param {string} capabilityId - The capability ID
173
+ * @param {number} levelIndex - The skill level index
174
+ * @returns {number} Priority score (higher = more important)
175
+ */
176
+ function calculateCapabilityPriority(capabilityId, levelIndex) {
177
+ let priority = 0;
178
+
179
+ // Delivery and scale capabilities are typically more important for decomposition
180
+ if (capabilityId === Capability.DELIVERY) {
181
+ priority += 10;
182
+ } else if (capabilityId === Capability.SCALE) {
183
+ priority += 8;
184
+ } else if (capabilityId === Capability.RELIABILITY) {
185
+ priority += 6;
186
+ }
187
+
188
+ // Higher level = higher priority
189
+ priority += levelIndex * WEIGHT_SKILL_LEVEL;
91
190
 
92
191
  return priority;
93
192
  }
@@ -119,7 +218,8 @@ function selectQuestion(questions, deterministic = false) {
119
218
  * @param {number} [params.options.maxQuestionsPerSkill=2] - Max questions per skill
120
219
  * @param {number} [params.options.maxQuestionsPerBehaviour=2] - Max questions per behaviour
121
220
  * @param {number} [params.options.targetMinutes=60] - Target interview length in minutes
122
- * @param {number} [params.options.skillBehaviourRatio=0.6] - Ratio of time for skills vs behaviours (0.6 = 60% skills, 40% behaviours)
221
+ * @param {number} [params.options.skillBehaviourRatio=RATIO_SKILL_BEHAVIOUR] - Ratio of time for skills vs behaviours
222
+ * @param {string} [params.options.roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
123
223
  * @returns {import('./levels.js').InterviewGuide}
124
224
  */
125
225
  export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
@@ -129,7 +229,8 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
129
229
  maxQuestionsPerSkill = 2,
130
230
  maxQuestionsPerBehaviour = 2,
131
231
  targetMinutes = 60,
132
- skillBehaviourRatio = 0.6,
232
+ skillBehaviourRatio = RATIO_SKILL_BEHAVIOUR,
233
+ roleType = "professionalQuestions",
133
234
  } = options;
134
235
 
135
236
  const allSkillQuestions = [];
@@ -147,6 +248,7 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
147
248
  questionBank,
148
249
  skill.skillId,
149
250
  targetLevel,
251
+ roleType,
150
252
  );
151
253
  let questionsAdded = 0;
152
254
 
@@ -176,6 +278,7 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
176
278
  questionBank,
177
279
  skill.skillId,
178
280
  belowLevel,
281
+ roleType,
179
282
  );
180
283
 
181
284
  const belowQuestion = selectQuestion(belowQuestions, deterministic);
@@ -199,6 +302,8 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
199
302
  questionBank,
200
303
  behaviour.behaviourId,
201
304
  targetMaturity,
305
+ roleType,
306
+ targetMaturity,
202
307
  );
203
308
  let questionsAdded = 0;
204
309
 
@@ -402,9 +507,14 @@ export function deriveShortInterview({
402
507
  * @param {Object} params
403
508
  * @param {import('./levels.js').JobDefinition} params.job - The job definition
404
509
  * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
510
+ * @param {string} [params.roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
405
511
  * @returns {import('./levels.js').InterviewGuide}
406
512
  */
407
- export function deriveBehaviourQuestions({ job, questionBank }) {
513
+ export function deriveBehaviourQuestions({
514
+ job,
515
+ questionBank,
516
+ roleType = "professionalQuestions",
517
+ }) {
408
518
  const interviewQuestions = [];
409
519
  const coveredBehaviours = new Set();
410
520
 
@@ -417,6 +527,7 @@ export function deriveBehaviourQuestions({ job, questionBank }) {
417
527
  questionBank,
418
528
  behaviour.behaviourId,
419
529
  targetMaturity,
530
+ roleType,
420
531
  );
421
532
 
422
533
  for (const question of targetQuestions) {
@@ -460,6 +571,7 @@ export function deriveBehaviourQuestions({ job, questionBank }) {
460
571
  * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
461
572
  * @param {string[]} [params.focusSkills] - Skill IDs to focus on
462
573
  * @param {string[]} [params.focusBehaviours] - Behaviour IDs to focus on
574
+ * @param {string} [params.roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
463
575
  * @returns {import('./levels.js').InterviewGuide}
464
576
  */
465
577
  export function deriveFocusedInterview({
@@ -467,6 +579,7 @@ export function deriveFocusedInterview({
467
579
  questionBank,
468
580
  focusSkills = [],
469
581
  focusBehaviours = [],
582
+ roleType = "professionalQuestions",
470
583
  }) {
471
584
  const interviewQuestions = [];
472
585
  const coveredSkills = new Set();
@@ -481,6 +594,7 @@ export function deriveFocusedInterview({
481
594
  questionBank,
482
595
  skill.skillId,
483
596
  skill.level,
597
+ roleType,
484
598
  );
485
599
  for (const question of questions) {
486
600
  interviewQuestions.push({
@@ -504,6 +618,7 @@ export function deriveFocusedInterview({
504
618
  questionBank,
505
619
  behaviour.behaviourId,
506
620
  behaviour.maturity,
621
+ roleType,
507
622
  );
508
623
  for (const question of questions) {
509
624
  interviewQuestions.push({
@@ -537,3 +652,317 @@ export function deriveFocusedInterview({
537
652
  },
538
653
  };
539
654
  }
655
+
656
+ /**
657
+ * Derive Mission Fit interview questions (skill-focused)
658
+ *
659
+ * 45-minute interview with Recruiting Manager + 1 Senior Engineer
660
+ * Focuses on skill questions to assess technical capability and fit.
661
+ *
662
+ * @param {Object} params
663
+ * @param {import('./levels.js').JobDefinition} params.job - The job definition
664
+ * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
665
+ * @param {number} [params.targetMinutes=45] - Target interview length in minutes
666
+ * @param {string} [params.roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
667
+ * @returns {import('./levels.js').InterviewGuide}
668
+ */
669
+ export function deriveMissionFitInterview({
670
+ job,
671
+ questionBank,
672
+ targetMinutes = 45,
673
+ roleType = "professionalQuestions",
674
+ }) {
675
+ const allSkillQuestions = [];
676
+ const coveredSkills = new Set();
677
+
678
+ // Generate all potential skill questions with priority
679
+ for (const skill of job.skillMatrix) {
680
+ const targetLevel = skill.level;
681
+ const targetLevelIndex = getSkillLevelIndex(targetLevel);
682
+
683
+ // Get questions at target level
684
+ const targetQuestions = getSkillQuestions(
685
+ questionBank,
686
+ skill.skillId,
687
+ targetLevel,
688
+ roleType,
689
+ );
690
+
691
+ for (const question of targetQuestions) {
692
+ allSkillQuestions.push({
693
+ question,
694
+ targetId: skill.skillId,
695
+ targetName: skill.skillName,
696
+ targetType: "skill",
697
+ targetLevel,
698
+ priority: calculateSkillPriority(skill, false),
699
+ });
700
+ }
701
+
702
+ // Also add question from level below for depth
703
+ if (targetLevelIndex > 0) {
704
+ const belowLevel = SKILL_LEVEL_ORDER[targetLevelIndex - 1];
705
+ const belowQuestions = getSkillQuestions(
706
+ questionBank,
707
+ skill.skillId,
708
+ belowLevel,
709
+ roleType,
710
+ );
711
+
712
+ for (const question of belowQuestions) {
713
+ allSkillQuestions.push({
714
+ question,
715
+ targetId: skill.skillId,
716
+ targetName: skill.skillName,
717
+ targetType: "skill",
718
+ targetLevel: belowLevel,
719
+ priority: calculateSkillPriority(skill, true),
720
+ });
721
+ }
722
+ }
723
+ }
724
+
725
+ // Sort by priority (highest first)
726
+ allSkillQuestions.sort((a, b) => b.priority - a.priority);
727
+
728
+ // Select questions within budget, prioritizing coverage diversity
729
+ const selectedQuestions = [];
730
+ const selectedSkillIds = new Set();
731
+ let totalMinutes = 0;
732
+
733
+ // First pass: one question per skill (highest priority first)
734
+ for (const q of allSkillQuestions) {
735
+ if (selectedSkillIds.has(q.targetId)) continue;
736
+ const questionTime =
737
+ q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES;
738
+ if (totalMinutes + questionTime <= targetMinutes + 5) {
739
+ selectedQuestions.push(q);
740
+ selectedSkillIds.add(q.targetId);
741
+ coveredSkills.add(q.targetId);
742
+ totalMinutes += questionTime;
743
+ }
744
+ }
745
+
746
+ // Second pass: add more questions if time allows
747
+ for (const q of allSkillQuestions) {
748
+ if (selectedQuestions.includes(q)) continue;
749
+ const questionTime =
750
+ q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES;
751
+ if (totalMinutes + questionTime <= targetMinutes + 5) {
752
+ selectedQuestions.push(q);
753
+ coveredSkills.add(q.targetId);
754
+ totalMinutes += questionTime;
755
+ }
756
+ }
757
+
758
+ // Re-sort by priority
759
+ selectedQuestions.sort((a, b) => b.priority - a.priority);
760
+
761
+ return {
762
+ job,
763
+ questions: selectedQuestions,
764
+ expectedDurationMinutes: totalMinutes,
765
+ coverage: {
766
+ skills: Array.from(coveredSkills),
767
+ behaviours: [],
768
+ capabilities: [],
769
+ },
770
+ };
771
+ }
772
+
773
+ /**
774
+ * Derive Decomposition interview questions (capability-focused)
775
+ *
776
+ * 60-minute interview with 2 Senior Engineers
777
+ * Focuses on capability decomposition questions inspired by Palantir's technique.
778
+ * Capabilities are selected based on the job's skill matrix.
779
+ *
780
+ * @param {Object} params
781
+ * @param {import('./levels.js').JobDefinition} params.job - The job definition
782
+ * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
783
+ * @param {number} [params.targetMinutes=60] - Target interview length in minutes
784
+ * @param {string} [params.roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
785
+ * @returns {import('./levels.js').InterviewGuide}
786
+ */
787
+ export function deriveDecompositionInterview({
788
+ job,
789
+ questionBank,
790
+ targetMinutes = 60,
791
+ roleType = "professionalQuestions",
792
+ }) {
793
+ const allCapabilityQuestions = [];
794
+ const coveredCapabilities = new Set();
795
+
796
+ // Derive capability levels from the job's skill matrix
797
+ const capabilityLevels = deriveCapabilityLevels(job);
798
+
799
+ // Generate capability questions based on derived levels
800
+ for (const [capabilityId, levelInfo] of capabilityLevels) {
801
+ const { level, levelIndex } = levelInfo;
802
+
803
+ // Get questions at the derived level
804
+ const questions = getCapabilityQuestions(
805
+ questionBank,
806
+ capabilityId,
807
+ level,
808
+ roleType,
809
+ );
810
+
811
+ for (const question of questions) {
812
+ allCapabilityQuestions.push({
813
+ question,
814
+ targetId: capabilityId,
815
+ targetName: capabilityId, // Capability name can be enhanced if needed
816
+ targetType: "capability",
817
+ targetLevel: level,
818
+ priority: calculateCapabilityPriority(capabilityId, levelIndex),
819
+ });
820
+ }
821
+
822
+ // Also try level below if available
823
+ if (levelIndex > 0) {
824
+ const belowLevel = SKILL_LEVEL_ORDER[levelIndex - 1];
825
+ const belowQuestions = getCapabilityQuestions(
826
+ questionBank,
827
+ capabilityId,
828
+ belowLevel,
829
+ roleType,
830
+ );
831
+
832
+ for (const question of belowQuestions) {
833
+ allCapabilityQuestions.push({
834
+ question,
835
+ targetId: capabilityId,
836
+ targetName: capabilityId,
837
+ targetType: "capability",
838
+ targetLevel: belowLevel,
839
+ priority: calculateCapabilityPriority(capabilityId, levelIndex - 1),
840
+ });
841
+ }
842
+ }
843
+ }
844
+
845
+ // Sort by priority (highest first)
846
+ allCapabilityQuestions.sort((a, b) => b.priority - a.priority);
847
+
848
+ // Select questions within budget, prioritizing coverage diversity
849
+ const selectedQuestions = [];
850
+ const selectedCapabilityIds = new Set();
851
+ let totalMinutes = 0;
852
+
853
+ // First pass: one question per capability (highest priority first)
854
+ for (const q of allCapabilityQuestions) {
855
+ if (selectedCapabilityIds.has(q.targetId)) continue;
856
+ const questionTime =
857
+ q.question.expectedDurationMinutes || DEFAULT_DECOMPOSITION_MINUTES;
858
+ if (totalMinutes + questionTime <= targetMinutes + 5) {
859
+ selectedQuestions.push(q);
860
+ selectedCapabilityIds.add(q.targetId);
861
+ coveredCapabilities.add(q.targetId);
862
+ totalMinutes += questionTime;
863
+ }
864
+ }
865
+
866
+ // Second pass: add more questions if time allows
867
+ for (const q of allCapabilityQuestions) {
868
+ if (selectedQuestions.includes(q)) continue;
869
+ const questionTime =
870
+ q.question.expectedDurationMinutes || DEFAULT_DECOMPOSITION_MINUTES;
871
+ if (totalMinutes + questionTime <= targetMinutes + 5) {
872
+ selectedQuestions.push(q);
873
+ coveredCapabilities.add(q.targetId);
874
+ totalMinutes += questionTime;
875
+ }
876
+ }
877
+
878
+ // Re-sort by priority
879
+ selectedQuestions.sort((a, b) => b.priority - a.priority);
880
+
881
+ return {
882
+ job,
883
+ questions: selectedQuestions,
884
+ expectedDurationMinutes: totalMinutes,
885
+ coverage: {
886
+ skills: [],
887
+ behaviours: [],
888
+ capabilities: Array.from(coveredCapabilities),
889
+ },
890
+ };
891
+ }
892
+
893
+ /**
894
+ * Derive Stakeholder Simulation interview questions (behaviour-focused)
895
+ *
896
+ * 60-minute interview with 3-4 stakeholders.
897
+ * Selects the highest-maturity behaviours for the role and picks one chunky
898
+ * simulation question per behaviour. For most jobs this means 2-3 behaviours
899
+ * with ~20-minute simulation scenarios each.
900
+ *
901
+ * @param {Object} params
902
+ * @param {import('./levels.js').JobDefinition} params.job - The job definition
903
+ * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
904
+ * @param {number} [params.targetMinutes=60] - Target interview length in minutes
905
+ * @param {string} [params.roleType='professionalQuestions'] - Role type ('professionalQuestions' or 'managementQuestions')
906
+ * @returns {import('./levels.js').InterviewGuide}
907
+ */
908
+ export function deriveStakeholderInterview({
909
+ job,
910
+ questionBank,
911
+ targetMinutes = 60,
912
+ roleType = "professionalQuestions",
913
+ }) {
914
+ const coveredBehaviours = new Set();
915
+
916
+ // Sort behaviours by maturity (highest first) to prioritize the most emphasized
917
+ const sortedBehaviours = [...job.behaviourProfile].sort(
918
+ (a, b) =>
919
+ getBehaviourMaturityIndex(b.maturity) -
920
+ getBehaviourMaturityIndex(a.maturity),
921
+ );
922
+
923
+ // Select one question per behaviour, highest maturity first, within budget
924
+ const selectedQuestions = [];
925
+ let totalMinutes = 0;
926
+
927
+ for (const behaviour of sortedBehaviours) {
928
+ const targetMaturity = behaviour.maturity;
929
+
930
+ // Get questions at target maturity
931
+ const questions = getBehaviourQuestions(
932
+ questionBank,
933
+ behaviour.behaviourId,
934
+ targetMaturity,
935
+ roleType,
936
+ );
937
+
938
+ // Pick the first available question (1 per behaviour)
939
+ const question = questions[0];
940
+ if (!question) continue;
941
+
942
+ const questionTime =
943
+ question.expectedDurationMinutes || DEFAULT_SIMULATION_MINUTES;
944
+ if (totalMinutes + questionTime > targetMinutes + 5) break;
945
+
946
+ selectedQuestions.push({
947
+ question,
948
+ targetId: behaviour.behaviourId,
949
+ targetName: behaviour.behaviourName,
950
+ targetType: "behaviour",
951
+ targetLevel: targetMaturity,
952
+ priority: calculateBehaviourPriority(behaviour),
953
+ });
954
+ coveredBehaviours.add(behaviour.behaviourId);
955
+ totalMinutes += questionTime;
956
+ }
957
+
958
+ return {
959
+ job,
960
+ questions: selectedQuestions,
961
+ expectedDurationMinutes: totalMinutes,
962
+ coverage: {
963
+ skills: [],
964
+ behaviours: Array.from(coveredBehaviours),
965
+ capabilities: [],
966
+ },
967
+ };
968
+ }
@@ -11,13 +11,13 @@ import { deriveJob } from "./derivation.js";
11
11
  const cache = new Map();
12
12
 
13
13
  /**
14
- * Create a consistent cache key from job parameters
14
+ * Build a consistent cache key from job parameters
15
15
  * @param {string} disciplineId
16
16
  * @param {string} gradeId
17
17
  * @param {string} [trackId] - Optional track ID
18
18
  * @returns {string}
19
19
  */
20
- export function makeJobKey(disciplineId, gradeId, trackId = null) {
20
+ export function buildJobKey(disciplineId, gradeId, trackId = null) {
21
21
  if (trackId) {
22
22
  return `${disciplineId}_${gradeId}_${trackId}`;
23
23
  }
@@ -43,7 +43,7 @@ export function getOrCreateJob({
43
43
  behaviours,
44
44
  capabilities,
45
45
  }) {
46
- const key = makeJobKey(discipline.id, grade.id, track?.id);
46
+ const key = buildJobKey(discipline.id, grade.id, track?.id);
47
47
 
48
48
  if (!cache.has(key)) {
49
49
  const job = deriveJob({
@@ -66,7 +66,7 @@ export function getOrCreateJob({
66
66
  /**
67
67
  * Clear all cached jobs
68
68
  */
69
- export function clearJobCache() {
69
+ export function clearCache() {
70
70
  cache.clear();
71
71
  }
72
72
 
@@ -76,14 +76,14 @@ export function clearJobCache() {
76
76
  * @param {string} gradeId
77
77
  * @param {string} [trackId] - Optional track ID
78
78
  */
79
- export function invalidateJob(disciplineId, gradeId, trackId = null) {
80
- cache.delete(makeJobKey(disciplineId, gradeId, trackId));
79
+ export function invalidateCachedJob(disciplineId, gradeId, trackId = null) {
80
+ cache.delete(buildJobKey(disciplineId, gradeId, trackId));
81
81
  }
82
82
 
83
83
  /**
84
84
  * Get the number of cached jobs (for testing/debugging)
85
85
  * @returns {number}
86
86
  */
87
- export function getCacheSize() {
87
+ export function getCachedJobCount() {
88
88
  return cache.size;
89
89
  }