@forwardimpact/pathway 0.1.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.
Files changed (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. package/package.json +68 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Directory Index Generator
3
+ *
4
+ * Generates _index.yaml files for browser-based directory discovery.
5
+ * These files list all entity files in a directory.
6
+ */
7
+
8
+ import { readdir, writeFile } from "fs/promises";
9
+ import { join, basename } from "path";
10
+ import { stringify as stringifyYaml } from "yaml";
11
+
12
+ /**
13
+ * Generate _index.yaml for a directory
14
+ * @param {string} dir - Directory path
15
+ * @returns {Promise<string[]>} List of file IDs included
16
+ */
17
+ export async function generateDirIndex(dir) {
18
+ const files = await readdir(dir);
19
+ const yamlFiles = files.filter(
20
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
21
+ );
22
+
23
+ const fileIds = yamlFiles.map((f) => basename(f, ".yaml")).sort();
24
+
25
+ const content = stringifyYaml(
26
+ {
27
+ // Auto-generated index for browser loading
28
+ // Do not edit manually - regenerate with: npx pathway --generate-index
29
+ files: fileIds,
30
+ },
31
+ { lineWidth: 0 },
32
+ );
33
+
34
+ // Add header comment
35
+ const output = `# Auto-generated index for browser loading
36
+ # Do not edit manually - regenerate with: npx pathway --generate-index
37
+ ${content}`;
38
+
39
+ await writeFile(join(dir, "_index.yaml"), output, "utf-8");
40
+
41
+ return fileIds;
42
+ }
43
+
44
+ /**
45
+ * Generate all index files for the data directory
46
+ * @param {string} dataDir - Path to the data directory
47
+ * @returns {Promise<Object>} Summary of generated indexes
48
+ */
49
+ export async function generateAllIndexes(dataDir) {
50
+ const directories = [
51
+ "skills",
52
+ "behaviours",
53
+ "disciplines",
54
+ "tracks",
55
+ "capabilities",
56
+ ];
57
+
58
+ const results = {};
59
+
60
+ for (const dir of directories) {
61
+ const fullPath = join(dataDir, dir);
62
+ try {
63
+ const files = await generateDirIndex(fullPath);
64
+ results[dir] = files;
65
+ } catch (err) {
66
+ results[dir] = { error: err.message };
67
+ }
68
+ }
69
+
70
+ return results;
71
+ }
@@ -0,0 +1,539 @@
1
+ /**
2
+ * Engineering Pathway Interview Question Generation
3
+ *
4
+ * This module provides pure functions for generating interview questions
5
+ * based on job definitions and question banks.
6
+ */
7
+
8
+ import {
9
+ getSkillLevelIndex,
10
+ getBehaviourMaturityIndex,
11
+ SKILL_LEVEL_ORDER,
12
+ Capability,
13
+ } from "./levels.js";
14
+
15
+ /**
16
+ * Default question time estimate if not specified
17
+ */
18
+ const DEFAULT_QUESTION_MINUTES = 5;
19
+
20
+ /**
21
+ * Get questions from the question bank for a specific skill and level
22
+ * @param {import('./levels.js').QuestionBank} questionBank - The question bank
23
+ * @param {string} skillId - The skill ID
24
+ * @param {string} level - The skill level
25
+ * @returns {import('./levels.js').Question[]} Array of questions
26
+ */
27
+ function getSkillQuestions(questionBank, skillId, level) {
28
+ return questionBank.skillLevels?.[skillId]?.[level] || [];
29
+ }
30
+
31
+ /**
32
+ * Get questions from the question bank for a specific behaviour and maturity
33
+ * @param {import('./levels.js').QuestionBank} questionBank - The question bank
34
+ * @param {string} behaviourId - The behaviour ID
35
+ * @param {string} maturity - The maturity level
36
+ * @returns {import('./levels.js').Question[]} Array of questions
37
+ */
38
+ function getBehaviourQuestions(questionBank, behaviourId, maturity) {
39
+ return questionBank.behaviourMaturities?.[behaviourId]?.[maturity] || [];
40
+ }
41
+
42
+ /**
43
+ * Calculate priority for a skill question
44
+ * @param {import('./levels.js').SkillMatrixEntry} skill - The skill entry
45
+ * @param {boolean} includeBelowLevel - Whether this is a below-level question
46
+ * @returns {number} Priority score (higher = more important)
47
+ */
48
+ function calculateSkillPriority(skill, includeBelowLevel = false) {
49
+ let priority = 0;
50
+
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
+ }
59
+
60
+ // AI skills get a boost for "AI-era focus"
61
+ if (skill.capability === Capability.AI) {
62
+ priority += 15;
63
+ }
64
+
65
+ // Delivery skills are core technical skills
66
+ if (skill.capability === Capability.DELIVERY) {
67
+ priority += 5;
68
+ }
69
+
70
+ // Higher skill level = higher priority
71
+ priority += getSkillLevelIndex(skill.level) * 2;
72
+
73
+ // Below-level questions have lower priority
74
+ if (includeBelowLevel) {
75
+ priority -= 5;
76
+ }
77
+
78
+ return priority;
79
+ }
80
+
81
+ /**
82
+ * Calculate priority for a behaviour question
83
+ * @param {import('./levels.js').BehaviourProfileEntry} behaviour - The behaviour entry
84
+ * @returns {number} Priority score (higher = more important)
85
+ */
86
+ function calculateBehaviourPriority(behaviour) {
87
+ let priority = 15;
88
+
89
+ // Higher maturity level = higher priority
90
+ priority += getBehaviourMaturityIndex(behaviour.maturity) * 3;
91
+
92
+ return priority;
93
+ }
94
+
95
+ /**
96
+ * Select a random question from an array (or first if deterministic)
97
+ * @param {import('./levels.js').Question[]} questions - Array of questions
98
+ * @param {boolean} deterministic - If true, always select first question
99
+ * @returns {import('./levels.js').Question|null} Selected question or null
100
+ */
101
+ function selectQuestion(questions, deterministic = false) {
102
+ if (!questions || questions.length === 0) {
103
+ return null;
104
+ }
105
+ if (deterministic) {
106
+ return questions[0];
107
+ }
108
+ return questions[Math.floor(Math.random() * questions.length)];
109
+ }
110
+
111
+ /**
112
+ * Derive interview questions for a job
113
+ * @param {Object} params
114
+ * @param {import('./levels.js').JobDefinition} params.job - The job definition
115
+ * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
116
+ * @param {Object} [params.options] - Generation options
117
+ * @param {boolean} [params.options.includeBelowLevel=true] - Include one question from level below
118
+ * @param {boolean} [params.options.deterministic=false] - Use deterministic selection
119
+ * @param {number} [params.options.maxQuestionsPerSkill=2] - Max questions per skill
120
+ * @param {number} [params.options.maxQuestionsPerBehaviour=2] - Max questions per behaviour
121
+ * @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)
123
+ * @returns {import('./levels.js').InterviewGuide}
124
+ */
125
+ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
126
+ const {
127
+ includeBelowLevel = true,
128
+ deterministic = false,
129
+ maxQuestionsPerSkill = 2,
130
+ maxQuestionsPerBehaviour = 2,
131
+ targetMinutes = 60,
132
+ skillBehaviourRatio = 0.6,
133
+ } = options;
134
+
135
+ const allSkillQuestions = [];
136
+ const allBehaviourQuestions = [];
137
+ const coveredSkills = new Set();
138
+ const coveredBehaviours = new Set();
139
+
140
+ // Generate all potential skill questions with priority
141
+ for (const skill of job.skillMatrix) {
142
+ const targetLevel = skill.level;
143
+ const targetLevelIndex = getSkillLevelIndex(targetLevel);
144
+
145
+ // Get questions at target level
146
+ const targetQuestions = getSkillQuestions(
147
+ questionBank,
148
+ skill.skillId,
149
+ targetLevel,
150
+ );
151
+ let questionsAdded = 0;
152
+
153
+ // Add question(s) at target level
154
+ for (const question of targetQuestions) {
155
+ if (questionsAdded >= maxQuestionsPerSkill) break;
156
+
157
+ allSkillQuestions.push({
158
+ question,
159
+ targetId: skill.skillId,
160
+ targetName: skill.skillName,
161
+ targetType: "skill",
162
+ targetLevel,
163
+ priority: calculateSkillPriority(skill, false),
164
+ });
165
+ questionsAdded++;
166
+ }
167
+
168
+ // Optionally add question from level below
169
+ if (
170
+ includeBelowLevel &&
171
+ targetLevelIndex > 0 &&
172
+ questionsAdded < maxQuestionsPerSkill
173
+ ) {
174
+ const belowLevel = SKILL_LEVEL_ORDER[targetLevelIndex - 1];
175
+ const belowQuestions = getSkillQuestions(
176
+ questionBank,
177
+ skill.skillId,
178
+ belowLevel,
179
+ );
180
+
181
+ const belowQuestion = selectQuestion(belowQuestions, deterministic);
182
+ if (belowQuestion) {
183
+ allSkillQuestions.push({
184
+ question: belowQuestion,
185
+ targetId: skill.skillId,
186
+ targetName: skill.skillName,
187
+ targetType: "skill",
188
+ targetLevel: belowLevel,
189
+ priority: calculateSkillPriority(skill, true),
190
+ });
191
+ }
192
+ }
193
+ }
194
+
195
+ // Generate all potential behaviour questions with priority
196
+ for (const behaviour of job.behaviourProfile) {
197
+ const targetMaturity = behaviour.maturity;
198
+ const questions = getBehaviourQuestions(
199
+ questionBank,
200
+ behaviour.behaviourId,
201
+ targetMaturity,
202
+ );
203
+ let questionsAdded = 0;
204
+
205
+ for (const question of questions) {
206
+ if (questionsAdded >= maxQuestionsPerBehaviour) break;
207
+
208
+ allBehaviourQuestions.push({
209
+ question,
210
+ targetId: behaviour.behaviourId,
211
+ targetName: behaviour.behaviourName,
212
+ targetType: "behaviour",
213
+ targetLevel: targetMaturity,
214
+ priority: calculateBehaviourPriority(behaviour),
215
+ });
216
+ questionsAdded++;
217
+ }
218
+ }
219
+
220
+ // Sort both lists by priority (highest first)
221
+ allSkillQuestions.sort((a, b) => b.priority - a.priority);
222
+ allBehaviourQuestions.sort((a, b) => b.priority - a.priority);
223
+
224
+ // Calculate time budgets
225
+ const skillTimeBudget = targetMinutes * skillBehaviourRatio;
226
+ const behaviourTimeBudget = targetMinutes * (1 - skillBehaviourRatio);
227
+
228
+ // Select skill questions within budget, prioritizing coverage diversity
229
+ // First pass: one question per skill (highest priority first)
230
+ const selectedQuestions = [];
231
+ const selectedSkillIds = new Set();
232
+ let skillMinutes = 0;
233
+
234
+ for (const q of allSkillQuestions) {
235
+ if (selectedSkillIds.has(q.targetId)) continue; // Skip if we already have this skill
236
+ const questionTime =
237
+ q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES;
238
+ if (skillMinutes + questionTime <= skillTimeBudget + 5) {
239
+ selectedQuestions.push(q);
240
+ selectedSkillIds.add(q.targetId);
241
+ coveredSkills.add(q.targetId);
242
+ skillMinutes += questionTime;
243
+ }
244
+ }
245
+
246
+ // Second pass: add more questions if time allows
247
+ for (const q of allSkillQuestions) {
248
+ if (selectedQuestions.includes(q)) continue; // Skip already selected
249
+ const questionTime =
250
+ q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES;
251
+ if (skillMinutes + questionTime <= skillTimeBudget + 5) {
252
+ selectedQuestions.push(q);
253
+ coveredSkills.add(q.targetId);
254
+ skillMinutes += questionTime;
255
+ }
256
+ }
257
+
258
+ // Select behaviour questions within budget, prioritizing coverage diversity
259
+ // First pass: one question per behaviour (highest priority first)
260
+ const selectedBehaviourIds = new Set();
261
+ let behaviourMinutes = 0;
262
+
263
+ for (const q of allBehaviourQuestions) {
264
+ if (selectedBehaviourIds.has(q.targetId)) continue; // Skip if we already have this behaviour
265
+ const questionTime =
266
+ q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES;
267
+ if (behaviourMinutes + questionTime <= behaviourTimeBudget + 5) {
268
+ selectedQuestions.push(q);
269
+ selectedBehaviourIds.add(q.targetId);
270
+ coveredBehaviours.add(q.targetId);
271
+ behaviourMinutes += questionTime;
272
+ }
273
+ }
274
+
275
+ // Second pass: add more behaviour questions if time allows
276
+ for (const q of allBehaviourQuestions) {
277
+ if (selectedQuestions.includes(q)) continue; // Skip already selected
278
+ const questionTime =
279
+ q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES;
280
+ if (behaviourMinutes + questionTime <= behaviourTimeBudget + 5) {
281
+ selectedQuestions.push(q);
282
+ coveredBehaviours.add(q.targetId);
283
+ behaviourMinutes += questionTime;
284
+ }
285
+ }
286
+
287
+ // Re-sort selected questions by priority
288
+ selectedQuestions.sort((a, b) => b.priority - a.priority);
289
+
290
+ // Calculate total time
291
+ const expectedDurationMinutes = selectedQuestions.reduce(
292
+ (sum, q) =>
293
+ sum + (q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES),
294
+ 0,
295
+ );
296
+
297
+ return {
298
+ job,
299
+ questions: selectedQuestions,
300
+ expectedDurationMinutes,
301
+ coverage: {
302
+ skills: Array.from(coveredSkills),
303
+ behaviours: Array.from(coveredBehaviours),
304
+ },
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Derive a short/screening interview within a time budget
310
+ * @param {Object} params
311
+ * @param {import('./levels.js').JobDefinition} params.job - The job definition
312
+ * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
313
+ * @param {number} [params.targetMinutes=20] - Target interview length in minutes
314
+ * @returns {import('./levels.js').InterviewGuide}
315
+ */
316
+ export function deriveShortInterview({
317
+ job,
318
+ questionBank,
319
+ targetMinutes = 20,
320
+ }) {
321
+ // First get all potential questions with priority
322
+ const fullInterview = deriveInterviewQuestions({
323
+ job,
324
+ questionBank,
325
+ options: {
326
+ includeBelowLevel: false, // Skip below-level for short interviews
327
+ maxQuestionsPerSkill: 1,
328
+ maxQuestionsPerBehaviour: 1,
329
+ },
330
+ });
331
+
332
+ // Select questions until we hit the time budget
333
+ const selectedQuestions = [];
334
+ let totalMinutes = 0;
335
+ const coveredSkills = new Set();
336
+ const coveredBehaviours = new Set();
337
+
338
+ // Ensure we have at least some skill and behaviour coverage
339
+ // by alternating between skill and behaviour questions
340
+ const skillQuestions = fullInterview.questions.filter(
341
+ (q) => q.targetType === "skill",
342
+ );
343
+ const behaviourQuestions = fullInterview.questions.filter(
344
+ (q) => q.targetType === "behaviour",
345
+ );
346
+
347
+ let skillIndex = 0;
348
+ let behaviourIndex = 0;
349
+ let preferSkill = true;
350
+
351
+ while (totalMinutes < targetMinutes) {
352
+ let nextQuestion = null;
353
+
354
+ if (preferSkill && skillIndex < skillQuestions.length) {
355
+ nextQuestion = skillQuestions[skillIndex++];
356
+ } else if (!preferSkill && behaviourIndex < behaviourQuestions.length) {
357
+ nextQuestion = behaviourQuestions[behaviourIndex++];
358
+ } else if (skillIndex < skillQuestions.length) {
359
+ nextQuestion = skillQuestions[skillIndex++];
360
+ } else if (behaviourIndex < behaviourQuestions.length) {
361
+ nextQuestion = behaviourQuestions[behaviourIndex++];
362
+ } else {
363
+ break; // No more questions
364
+ }
365
+
366
+ const questionTime =
367
+ nextQuestion.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES;
368
+
369
+ // Don't exceed budget by too much
370
+ if (totalMinutes + questionTime > targetMinutes + 5) {
371
+ break;
372
+ }
373
+
374
+ selectedQuestions.push(nextQuestion);
375
+ totalMinutes += questionTime;
376
+
377
+ if (nextQuestion.targetType === "skill") {
378
+ coveredSkills.add(nextQuestion.targetId);
379
+ } else {
380
+ coveredBehaviours.add(nextQuestion.targetId);
381
+ }
382
+
383
+ preferSkill = !preferSkill;
384
+ }
385
+
386
+ // Re-sort selected questions by priority
387
+ selectedQuestions.sort((a, b) => b.priority - a.priority);
388
+
389
+ return {
390
+ job,
391
+ questions: selectedQuestions,
392
+ expectedDurationMinutes: totalMinutes,
393
+ coverage: {
394
+ skills: Array.from(coveredSkills),
395
+ behaviours: Array.from(coveredBehaviours),
396
+ },
397
+ };
398
+ }
399
+
400
+ /**
401
+ * Derive behaviour-focused interview questions
402
+ * @param {Object} params
403
+ * @param {import('./levels.js').JobDefinition} params.job - The job definition
404
+ * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
405
+ * @returns {import('./levels.js').InterviewGuide}
406
+ */
407
+ export function deriveBehaviourQuestions({ job, questionBank }) {
408
+ const interviewQuestions = [];
409
+ const coveredBehaviours = new Set();
410
+
411
+ // Focus only on behaviours, with more depth
412
+ for (const behaviour of job.behaviourProfile) {
413
+ const targetMaturity = behaviour.maturity;
414
+
415
+ // Get questions at target maturity
416
+ const targetQuestions = getBehaviourQuestions(
417
+ questionBank,
418
+ behaviour.behaviourId,
419
+ targetMaturity,
420
+ );
421
+
422
+ for (const question of targetQuestions) {
423
+ interviewQuestions.push({
424
+ question,
425
+ targetId: behaviour.behaviourId,
426
+ targetName: behaviour.behaviourName,
427
+ targetType: "behaviour",
428
+ targetLevel: targetMaturity,
429
+ priority: calculateBehaviourPriority(behaviour),
430
+ });
431
+ coveredBehaviours.add(behaviour.behaviourId);
432
+ }
433
+ }
434
+
435
+ // Sort by priority
436
+ interviewQuestions.sort((a, b) => b.priority - a.priority);
437
+
438
+ // Calculate total time
439
+ const expectedDurationMinutes = interviewQuestions.reduce(
440
+ (sum, q) =>
441
+ sum + (q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES),
442
+ 0,
443
+ );
444
+
445
+ return {
446
+ job,
447
+ questions: interviewQuestions,
448
+ expectedDurationMinutes,
449
+ coverage: {
450
+ skills: [],
451
+ behaviours: Array.from(coveredBehaviours),
452
+ },
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Generate a focused interview for specific skills/behaviours
458
+ * @param {Object} params
459
+ * @param {import('./levels.js').JobDefinition} params.job - The job definition
460
+ * @param {import('./levels.js').QuestionBank} params.questionBank - The question bank
461
+ * @param {string[]} [params.focusSkills] - Skill IDs to focus on
462
+ * @param {string[]} [params.focusBehaviours] - Behaviour IDs to focus on
463
+ * @returns {import('./levels.js').InterviewGuide}
464
+ */
465
+ export function deriveFocusedInterview({
466
+ job,
467
+ questionBank,
468
+ focusSkills = [],
469
+ focusBehaviours = [],
470
+ }) {
471
+ const interviewQuestions = [];
472
+ const coveredSkills = new Set();
473
+ const coveredBehaviours = new Set();
474
+
475
+ // Focus skills
476
+ const focusSkillSet = new Set(focusSkills);
477
+ for (const skill of job.skillMatrix) {
478
+ if (!focusSkillSet.has(skill.skillId)) continue;
479
+
480
+ const questions = getSkillQuestions(
481
+ questionBank,
482
+ skill.skillId,
483
+ skill.level,
484
+ );
485
+ for (const question of questions) {
486
+ interviewQuestions.push({
487
+ question,
488
+ targetId: skill.skillId,
489
+ targetName: skill.skillName,
490
+ targetType: "skill",
491
+ targetLevel: skill.level,
492
+ priority: calculateSkillPriority(skill) + 10, // Boost for focus
493
+ });
494
+ coveredSkills.add(skill.skillId);
495
+ }
496
+ }
497
+
498
+ // Focus behaviours
499
+ const focusBehaviourSet = new Set(focusBehaviours);
500
+ for (const behaviour of job.behaviourProfile) {
501
+ if (!focusBehaviourSet.has(behaviour.behaviourId)) continue;
502
+
503
+ const questions = getBehaviourQuestions(
504
+ questionBank,
505
+ behaviour.behaviourId,
506
+ behaviour.maturity,
507
+ );
508
+ for (const question of questions) {
509
+ interviewQuestions.push({
510
+ question,
511
+ targetId: behaviour.behaviourId,
512
+ targetName: behaviour.behaviourName,
513
+ targetType: "behaviour",
514
+ targetLevel: behaviour.maturity,
515
+ priority: calculateBehaviourPriority(behaviour) + 10, // Boost for focus
516
+ });
517
+ coveredBehaviours.add(behaviour.behaviourId);
518
+ }
519
+ }
520
+
521
+ // Sort by priority
522
+ interviewQuestions.sort((a, b) => b.priority - a.priority);
523
+
524
+ const expectedDurationMinutes = interviewQuestions.reduce(
525
+ (sum, q) =>
526
+ sum + (q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES),
527
+ 0,
528
+ );
529
+
530
+ return {
531
+ job,
532
+ questions: interviewQuestions,
533
+ expectedDurationMinutes,
534
+ coverage: {
535
+ skills: Array.from(coveredSkills),
536
+ behaviours: Array.from(coveredBehaviours),
537
+ },
538
+ };
539
+ }