@forwardimpact/pathway 0.12.0 → 0.13.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.
@@ -18,7 +18,7 @@
18
18
  * stage [<id>] Show stages
19
19
  * tool [<name>] Show tools
20
20
  * job [<discipline> <grade>] [--track=TRACK] Generate job definition
21
- * interview <discipline> <grade> [--track=TRACK] [--type=TYPE] Generate interview
21
+ * interview <discipline> <grade> [--track=TRACK] [--type=mission|decomposition|stakeholder] Generate interview
22
22
  * progress <discipline> <grade> [--track=TRACK] [--compare=GRADE] Career progression
23
23
  * questions [options] Browse interview questions
24
24
  * agent [<discipline> <track>] [--output=PATH] Generate AI agent
@@ -167,13 +167,14 @@ INTERVIEW COMMAND
167
167
  Generate interview question sets based on job requirements.
168
168
 
169
169
  Usage:
170
- npx fit-pathway interview <discipline> <grade>
171
- npx fit-pathway interview <d> <g> --track=<track>
172
- npx fit-pathway interview <d> <g> --type=<type>
170
+ npx fit-pathway interview <discipline> <grade> All types
171
+ npx fit-pathway interview <d> <g> --track=<track> With track
172
+ npx fit-pathway interview <d> <g> --track=<t> --type=<type> Single type
173
173
 
174
174
  Options:
175
175
  --track=TRACK Track specialization
176
- --type=TYPE Interview type: full (default), short
176
+ --type=TYPE Interview type: mission, decomposition, stakeholder
177
+ (omit for all types)
177
178
 
178
179
  ────────────────────────────────────────────────────────────────────────────────
179
180
  PROGRESS COMMAND
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Career progression web app and CLI for exploring roles and generating agents",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -40,8 +40,8 @@
40
40
  "./commands": "./src/commands/index.js"
41
41
  },
42
42
  "dependencies": {
43
- "@forwardimpact/schema": "^0.5.0",
44
- "@forwardimpact/model": "^0.6.0",
43
+ "@forwardimpact/schema": "^0.6.0",
44
+ "@forwardimpact/model": "^0.7.0",
45
45
  "mustache": "^4.2.0",
46
46
  "simple-icons": "^16.7.0",
47
47
  "yaml": "^2.3.4"
@@ -4,9 +4,9 @@
4
4
  * Generates and displays interview questions in the terminal.
5
5
  *
6
6
  * Usage:
7
- * npx pathway interview <discipline> <grade> # Interview for trackless job
8
- * npx pathway interview <discipline> <grade> --track=<track> # Interview with track
9
- * npx pathway interview <discipline> <grade> --track=<track> --type=short
7
+ * npx fit-pathway interview <discipline> <grade> # All interview types
8
+ * npx fit-pathway interview <discipline> <grade> --track=<track> # With track
9
+ * npx fit-pathway interview <discipline> <grade> --track=<track> --type=mission # Single type
10
10
  */
11
11
 
12
12
  import { createCompositeCommand } from "./command-factory.js";
@@ -16,8 +16,10 @@ import {
16
16
  } from "../formatters/interview/shared.js";
17
17
  import { interviewToMarkdown } from "../formatters/interview/markdown.js";
18
18
 
19
+ const VALID_TYPES = Object.keys(INTERVIEW_TYPES);
20
+
19
21
  /**
20
- * Format interview output
22
+ * Format a single interview type as markdown
21
23
  * @param {Object} view - Presenter view
22
24
  * @param {Object} options - Options including framework
23
25
  */
@@ -25,15 +27,31 @@ function formatInterview(view, options) {
25
27
  console.log(interviewToMarkdown(view, { framework: options.framework }));
26
28
  }
27
29
 
30
+ /**
31
+ * Format all interview types as markdown with separators
32
+ * @param {Array<Object>} views - Array of presenter views
33
+ * @param {Object} options - Options including framework
34
+ */
35
+ function formatAllInterviews(views, options) {
36
+ for (let i = 0; i < views.length; i++) {
37
+ if (i > 0) {
38
+ console.log("\n" + "─".repeat(80) + "\n");
39
+ }
40
+ console.log(
41
+ interviewToMarkdown(views[i], { framework: options.framework }),
42
+ );
43
+ }
44
+ }
45
+
28
46
  export const runInterviewCommand = createCompositeCommand({
29
47
  commandName: "interview",
30
48
  requiredArgs: ["discipline_id", "grade_id"],
31
49
  findEntities: (data, args, options) => {
32
- const interviewType = options.type || "full";
50
+ const interviewType = options.type === "full" ? null : options.type;
33
51
 
34
- if (!INTERVIEW_TYPES[interviewType]) {
52
+ if (interviewType && !INTERVIEW_TYPES[interviewType]) {
35
53
  console.error(`Unknown interview type: ${interviewType}`);
36
- console.error("Available types: full, short, behaviour");
54
+ console.error(`Available types: ${VALID_TYPES.join(", ")}`);
37
55
  process.exit(1);
38
56
  }
39
57
 
@@ -58,18 +76,38 @@ export const runInterviewCommand = createCompositeCommand({
58
76
  }
59
77
  return null;
60
78
  },
61
- presenter: (entities, data, _options) =>
62
- prepareInterviewDetail({
79
+ presenter: (entities, data, _options) => {
80
+ const params = {
63
81
  discipline: entities.discipline,
64
82
  grade: entities.grade,
65
83
  track: entities.track,
66
84
  skills: data.skills,
67
85
  behaviours: data.behaviours,
68
86
  questions: data.questions,
69
- interviewType: entities.interviewType,
70
- }),
71
- formatter: (view, options, data) =>
72
- formatInterview(view, { ...options, framework: data.framework }),
87
+ };
88
+
89
+ // Single type: return one view
90
+ if (entities.interviewType) {
91
+ return prepareInterviewDetail({
92
+ ...params,
93
+ interviewType: entities.interviewType,
94
+ });
95
+ }
96
+
97
+ // All types: return array of views
98
+ return VALID_TYPES.map((type) =>
99
+ prepareInterviewDetail({ ...params, interviewType: type }),
100
+ ).filter(Boolean);
101
+ },
102
+ formatter: (view, options, data) => {
103
+ const opts = { ...options, framework: data.framework };
104
+
105
+ if (Array.isArray(view)) {
106
+ formatAllInterviews(view, opts);
107
+ } else {
108
+ formatInterview(view, opts);
109
+ }
110
+ },
73
111
  usageExample:
74
- "npx pathway interview software_engineering L4 --track=platform --type=short",
112
+ "npx fit-pathway interview software_engineering J090 --track=platform --type=mission",
75
113
  });
@@ -56,12 +56,15 @@ function showQuestionsSummary(data) {
56
56
  "practitioner",
57
57
  "expert",
58
58
  ];
59
+ const roleTypes = ["professionalQuestions", "managementQuestions"];
59
60
  const skillRows = skillLevels.map((level) => {
60
61
  let count = 0;
61
62
  for (const skill of skills) {
62
- const sq = questions.skills?.[skill.id];
63
- if (sq?.[level]) {
64
- count += sq[level].length;
63
+ const sq = questions.skillLevels?.[skill.id];
64
+ if (sq) {
65
+ for (const roleType of roleTypes) {
66
+ count += (sq[roleType]?.[level] || []).length;
67
+ }
65
68
  }
66
69
  }
67
70
  return [level, count];
@@ -81,9 +84,11 @@ function showQuestionsSummary(data) {
81
84
  const behaviourRows = maturities.map((maturity) => {
82
85
  let count = 0;
83
86
  for (const behaviour of behaviours) {
84
- const bq = questions.behaviours?.[behaviour.id];
85
- if (bq?.[maturity]) {
86
- count += bq[maturity].length;
87
+ const bq = questions.behaviourMaturities?.[behaviour.id];
88
+ if (bq) {
89
+ for (const roleType of roleTypes) {
90
+ count += (bq[roleType]?.[maturity] || []).length;
91
+ }
87
92
  }
88
93
  }
89
94
  return [maturity.replace(/_/g, " "), count];
@@ -138,10 +143,8 @@ export async function runQuestionsCommand({
138
143
  behaviours: data.behaviours,
139
144
  filter,
140
145
  });
141
- for (const section of view.sections) {
142
- for (const q of section.questions) {
143
- console.log(q.id);
144
- }
146
+ for (const q of view.questions) {
147
+ console.log(q.id);
145
148
  }
146
149
  return;
147
150
  }
@@ -94,24 +94,34 @@
94
94
  }
95
95
 
96
96
  /* Capability badges - each capability ID maps to its own distinct color */
97
+ .badge-ai {
98
+ background: var(--color-cap-ai-light);
99
+ color: var(--color-cap-ai);
100
+ }
101
+
102
+ .badge-business {
103
+ background: var(--color-cap-business-light);
104
+ color: var(--color-cap-business);
105
+ }
106
+
107
+ .badge-data {
108
+ background: var(--color-cap-data-light);
109
+ color: var(--color-cap-data);
110
+ }
111
+
97
112
  .badge-delivery {
98
113
  background: var(--color-cap-delivery-light);
99
114
  color: var(--color-cap-delivery);
100
115
  }
101
116
 
102
- .badge-scale {
103
- background: var(--color-cap-scale-light);
104
- color: var(--color-cap-scale);
105
- }
106
-
107
- .badge-reliability {
108
- background: var(--color-cap-reliability-light);
109
- color: var(--color-cap-reliability);
117
+ .badge-documentation {
118
+ background: var(--color-cap-documentation-light);
119
+ color: var(--color-cap-documentation);
110
120
  }
111
121
 
112
- .badge-business {
113
- background: var(--color-cap-business-light);
114
- color: var(--color-cap-business);
122
+ .badge-ml {
123
+ background: var(--color-cap-ml-light);
124
+ color: var(--color-cap-ml);
115
125
  }
116
126
 
117
127
  .badge-people {
@@ -119,6 +129,26 @@
119
129
  color: var(--color-cap-people);
120
130
  }
121
131
 
132
+ .badge-process {
133
+ background: var(--color-cap-process-light);
134
+ color: var(--color-cap-process);
135
+ }
136
+
137
+ .badge-product {
138
+ background: var(--color-cap-product-light);
139
+ color: var(--color-cap-product);
140
+ }
141
+
142
+ .badge-reliability {
143
+ background: var(--color-cap-reliability-light);
144
+ color: var(--color-cap-reliability);
145
+ }
146
+
147
+ .badge-scale {
148
+ background: var(--color-cap-scale-light);
149
+ color: var(--color-cap-scale);
150
+ }
151
+
122
152
  /* Tool badge */
123
153
  .badge-tool {
124
154
  background: var(--color-bg);
@@ -43,22 +43,34 @@
43
43
  --color-maturity-5: #c4b5fd;
44
44
 
45
45
  /* --------------------------------------------------------------------------
46
- Colors - Capabilities (text)
46
+ Colors - Capabilities (text) - matches data/capabilities/*.yaml IDs
47
47
  -------------------------------------------------------------------------- */
48
- --color-cap-delivery: #dc2626; /* red - speed and efficiency */
49
- --color-cap-scale: #1d4ed8; /* blue - infrastructure and systems */
50
- --color-cap-reliability: #0891b2; /* cyan - stability and operations */
51
- --color-cap-business: #b45309; /* amber - strategy and outcomes */
52
- --color-cap-people: #be185d; /* pink - collaboration and leadership */
48
+ --color-cap-ai: #7c3aed; /* violet */
49
+ --color-cap-business: #b45309; /* amber */
50
+ --color-cap-data: #0f766e; /* teal */
51
+ --color-cap-delivery: #dc2626; /* red */
52
+ --color-cap-documentation: #15803d; /* green */
53
+ --color-cap-ml: #9333ea; /* purple */
54
+ --color-cap-people: #be185d; /* pink */
55
+ --color-cap-process: #c2410c; /* orange */
56
+ --color-cap-product: #4f46e5; /* indigo */
57
+ --color-cap-reliability: #0891b2; /* cyan */
58
+ --color-cap-scale: #1d4ed8; /* blue */
53
59
 
54
60
  /* --------------------------------------------------------------------------
55
61
  Colors - Capabilities (backgrounds)
56
62
  -------------------------------------------------------------------------- */
57
- --color-cap-delivery-light: #fecaca; /* red */
58
- --color-cap-scale-light: #dbeafe; /* blue */
59
- --color-cap-reliability-light: #cffafe; /* cyan */
63
+ --color-cap-ai-light: #ede9fe; /* violet */
60
64
  --color-cap-business-light: #fef3c7; /* amber */
65
+ --color-cap-data-light: #ccfbf1; /* teal */
66
+ --color-cap-delivery-light: #fecaca; /* red */
67
+ --color-cap-documentation-light: #dcfce7; /* green */
68
+ --color-cap-ml-light: #f3e8ff; /* purple */
61
69
  --color-cap-people-light: #fce7f3; /* pink */
70
+ --color-cap-process-light: #ffedd5; /* orange */
71
+ --color-cap-product-light: #e0e7ff; /* indigo */
72
+ --color-cap-reliability-light: #cffafe; /* cyan */
73
+ --color-cap-scale-light: #dbeafe; /* blue */
62
74
 
63
75
  /* --------------------------------------------------------------------------
64
76
  Colors - Neutrals
@@ -27,6 +27,9 @@ export function interviewToMarkdown(view, { framework } = {}) {
27
27
  // Group sections by type
28
28
  const skillSections = view.sections.filter((s) => s.type === "skill");
29
29
  const behaviourSections = view.sections.filter((s) => s.type === "behaviour");
30
+ const capabilitySections = view.sections.filter(
31
+ (s) => s.type === "capability",
32
+ );
30
33
 
31
34
  // Skill questions
32
35
  if (skillSections.length > 0) {
@@ -36,27 +39,83 @@ export function interviewToMarkdown(view, { framework } = {}) {
36
39
  for (const q of section.questions) {
37
40
  lines.push(`**Q**: ${q.question}`);
38
41
  if (q.followUps.length > 0) {
42
+ lines.push("", "**Follow-ups:**");
39
43
  for (const followUp of q.followUps) {
40
44
  lines.push(` → ${followUp}`);
41
45
  }
42
46
  }
47
+ if (q.lookingFor && q.lookingFor.length > 0) {
48
+ lines.push("", "**What to look for:**");
49
+ for (const item of q.lookingFor) {
50
+ lines.push(`- ${item}`);
51
+ }
52
+ }
43
53
  lines.push("");
44
54
  }
45
55
  }
46
56
  }
47
57
 
48
- // Behaviour questions
58
+ // Capability decomposition questions
59
+ if (capabilitySections.length > 0) {
60
+ lines.push(`## 🧩 Decomposition Questions`, "");
61
+ for (const section of capabilitySections) {
62
+ lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
63
+ for (const q of section.questions) {
64
+ lines.push(`**Scenario**: ${q.question}`);
65
+ if (q.context) {
66
+ lines.push(`> ${q.context}`);
67
+ }
68
+ if (q.decompositionPrompts && q.decompositionPrompts.length > 0) {
69
+ lines.push("", "**Guide the candidate through:**");
70
+ for (const prompt of q.decompositionPrompts) {
71
+ lines.push(`- ${prompt}`);
72
+ }
73
+ }
74
+ if (q.followUps.length > 0) {
75
+ lines.push("", "**Follow-ups:**");
76
+ for (const followUp of q.followUps) {
77
+ lines.push(` → ${followUp}`);
78
+ }
79
+ }
80
+ if (q.lookingFor && q.lookingFor.length > 0) {
81
+ lines.push("", "**What to look for:**");
82
+ for (const item of q.lookingFor) {
83
+ lines.push(`- ${item}`);
84
+ }
85
+ }
86
+ lines.push("");
87
+ }
88
+ }
89
+ }
90
+
91
+ // Behaviour stakeholder simulation questions
49
92
  if (behaviourSections.length > 0) {
50
- lines.push(`## ${behaviourEmoji} Behaviour Questions`, "");
93
+ lines.push(`## ${behaviourEmoji} Stakeholder Simulation`, "");
51
94
  for (const section of behaviourSections) {
52
95
  lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
53
96
  for (const q of section.questions) {
54
- lines.push(`**Q**: ${q.question}`);
97
+ lines.push(`**Scenario**: ${q.question}`);
98
+ if (q.context) {
99
+ lines.push(`> ${q.context}`);
100
+ }
101
+ if (q.simulationPrompts && q.simulationPrompts.length > 0) {
102
+ lines.push("", "**Steer the simulation:**");
103
+ for (const prompt of q.simulationPrompts) {
104
+ lines.push(`- ${prompt}`);
105
+ }
106
+ }
55
107
  if (q.followUps.length > 0) {
108
+ lines.push("", "**Follow-ups:**");
56
109
  for (const followUp of q.followUps) {
57
110
  lines.push(` → ${followUp}`);
58
111
  }
59
112
  }
113
+ if (q.lookingFor && q.lookingFor.length > 0) {
114
+ lines.push("", "**What to look for:**");
115
+ for (const item of q.lookingFor) {
116
+ lines.push(`- ${item}`);
117
+ }
118
+ }
60
119
  lines.push("");
61
120
  }
62
121
  }
@@ -10,9 +10,9 @@ import {
10
10
  getDisciplineSkillIds,
11
11
  } from "@forwardimpact/model/derivation";
12
12
  import {
13
- deriveInterviewQuestions,
14
- deriveShortInterview,
15
- deriveBehaviourQuestions,
13
+ deriveMissionFitInterview,
14
+ deriveDecompositionInterview,
15
+ deriveStakeholderInterview,
16
16
  } from "@forwardimpact/model/interview";
17
17
  import { getOrCreateJob } from "@forwardimpact/model/job-cache";
18
18
 
@@ -20,26 +20,35 @@ import { getOrCreateJob } from "@forwardimpact/model/job-cache";
20
20
  * Interview type configurations
21
21
  */
22
22
  export const INTERVIEW_TYPES = {
23
- short: {
24
- id: "short",
25
- name: "Screening",
26
- description: "20-minute screening interview",
27
- icon: "⏱️",
28
- expectedDurationMinutes: 20,
29
- },
30
- behaviour: {
31
- id: "behaviour",
32
- name: "Behavioural",
33
- description: "Focus on behaviours and mindsets",
34
- icon: "🧠",
23
+ mission: {
24
+ id: "mission",
25
+ name: "Mission Fit",
26
+ description:
27
+ "Assess technical skills and alignment with role expectations. Focus on depth of knowledge, problem-solving approach, and ability to articulate technical decisions.",
28
+ icon: "🎯",
35
29
  expectedDurationMinutes: 45,
30
+ panel: "Recruiting Manager + 1 Senior Engineer",
31
+ questionTypes: ["skill"],
32
+ },
33
+ decomposition: {
34
+ id: "decomposition",
35
+ name: "Decomposition",
36
+ description:
37
+ "Evaluate how candidates break down ambiguous problems into actionable components. Inspired by Palantir's technique—focus on structured thinking, trade-off analysis, and communication under uncertainty.",
38
+ icon: "🧩",
39
+ expectedDurationMinutes: 60,
40
+ panel: "2 Senior Engineers",
41
+ questionTypes: ["capability"],
36
42
  },
37
- full: {
38
- id: "full",
39
- name: "Full Interview",
40
- description: "Comprehensive interview covering all skills and behaviours",
41
- icon: "📋",
42
- expectedDurationMinutes: 90,
43
+ stakeholder: {
44
+ id: "stakeholder",
45
+ name: "Stakeholder Simulation",
46
+ description:
47
+ "Simulate real-world stakeholder interactions through behaviour-focused assessment. Focus on communication style, influence, and ability to navigate competing priorities.",
48
+ icon: "👥",
49
+ expectedDurationMinutes: 60,
50
+ panel: "3-4 Stakeholders",
51
+ questionTypes: ["behaviour"],
43
52
  },
44
53
  };
45
54
 
@@ -67,14 +76,27 @@ function groupQuestionsIntoSections(questions) {
67
76
  };
68
77
  }
69
78
 
70
- sections[id].questions.push({
79
+ const questionEntry = {
71
80
  skillOrBehaviourId: id,
72
81
  skillOrBehaviourName: name,
73
82
  type,
74
83
  level,
75
84
  question: q.question.text,
76
85
  followUps: q.question.followUps || [],
77
- });
86
+ lookingFor: q.question.lookingFor || [],
87
+ };
88
+
89
+ if (q.question.decompositionPrompts) {
90
+ questionEntry.decompositionPrompts = q.question.decompositionPrompts;
91
+ }
92
+ if (q.question.simulationPrompts) {
93
+ questionEntry.simulationPrompts = q.question.simulationPrompts;
94
+ }
95
+ if (q.question.context) {
96
+ questionEntry.context = q.question.context;
97
+ }
98
+
99
+ sections[id].questions.push(questionEntry);
78
100
  }
79
101
 
80
102
  return Object.values(sections);
@@ -83,7 +105,7 @@ function groupQuestionsIntoSections(questions) {
83
105
  /**
84
106
  * @typedef {Object} InterviewDetailView
85
107
  * @property {string} title
86
- * @property {string} interviewType - 'full', 'short', or 'behaviour'
108
+ * @property {string} interviewType - 'mission', 'decomposition', or 'stakeholder'
87
109
  * @property {string} disciplineId
88
110
  * @property {string} disciplineName
89
111
  * @property {string} gradeId
@@ -104,7 +126,7 @@ function groupQuestionsIntoSections(questions) {
104
126
  * @param {Array} params.skills
105
127
  * @param {Array} params.behaviours
106
128
  * @param {Array} params.questions
107
- * @param {string} [params.interviewType='full']
129
+ * @param {string} [params.interviewType='mission']
108
130
  * @returns {InterviewDetailView|null}
109
131
  */
110
132
  export function prepareInterviewDetail({
@@ -114,7 +136,7 @@ export function prepareInterviewDetail({
114
136
  skills,
115
137
  behaviours,
116
138
  questions,
117
- interviewType = "full",
139
+ interviewType = "mission",
118
140
  }) {
119
141
  if (!discipline || !grade) return null;
120
142
 
@@ -130,18 +152,26 @@ export function prepareInterviewDetail({
130
152
 
131
153
  let interviewGuide;
132
154
  switch (interviewType) {
133
- case "short":
134
- interviewGuide = deriveShortInterview({ job, questionBank: questions });
155
+ case "mission":
156
+ interviewGuide = deriveMissionFitInterview({
157
+ job,
158
+ questionBank: questions,
159
+ });
135
160
  break;
136
- case "behaviour":
137
- interviewGuide = deriveBehaviourQuestions({
161
+ case "decomposition":
162
+ interviewGuide = deriveDecompositionInterview({
163
+ job,
164
+ questionBank: questions,
165
+ });
166
+ break;
167
+ case "stakeholder":
168
+ interviewGuide = deriveStakeholderInterview({
138
169
  job,
139
170
  questionBank: questions,
140
171
  });
141
172
  break;
142
- case "full":
143
173
  default:
144
- interviewGuide = deriveInterviewQuestions({
174
+ interviewGuide = deriveMissionFitInterview({
145
175
  job,
146
176
  questionBank: questions,
147
177
  });
@@ -151,19 +181,27 @@ export function prepareInterviewDetail({
151
181
  // Extract the questions array from the interview guide
152
182
  const rawQuestions = interviewGuide.questions || [];
153
183
 
154
- // Separate skill and behaviour questions based on targetType
184
+ // Separate questions by type
155
185
  const skillQuestions = rawQuestions.filter((q) => q.targetType === "skill");
156
186
  const behaviourQuestions = rawQuestions.filter(
157
187
  (q) => q.targetType === "behaviour",
158
188
  );
189
+ const capabilityQuestions = rawQuestions.filter(
190
+ (q) => q.targetType === "capability",
191
+ );
159
192
 
160
193
  const skillSections = groupQuestionsIntoSections(skillQuestions);
161
194
  const behaviourSections = groupQuestionsIntoSections(behaviourQuestions);
195
+ const capabilitySections = groupQuestionsIntoSections(capabilityQuestions);
162
196
 
163
- const allSections = [...skillSections, ...behaviourSections];
197
+ const allSections = [
198
+ ...skillSections,
199
+ ...behaviourSections,
200
+ ...capabilitySections,
201
+ ];
164
202
  const totalQuestions = rawQuestions.length;
165
203
 
166
- const typeConfig = INTERVIEW_TYPES[interviewType] || INTERVIEW_TYPES.full;
204
+ const typeConfig = INTERVIEW_TYPES[interviewType] || INTERVIEW_TYPES.mission;
167
205
 
168
206
  return {
169
207
  title: job.title,
@@ -293,24 +331,19 @@ export function prepareAllInterviews({
293
331
  if (!job) return null;
294
332
 
295
333
  // Generate all interview types
296
- const shortInterview = deriveShortInterview({
334
+ const missionInterview = deriveMissionFitInterview({
297
335
  job,
298
336
  questionBank: questions,
299
- targetMinutes: 20,
300
337
  });
301
338
 
302
- const behaviourInterview = deriveBehaviourQuestions({
339
+ const decompositionInterview = deriveDecompositionInterview({
303
340
  job,
304
341
  questionBank: questions,
305
342
  });
306
343
 
307
- const fullInterview = deriveInterviewQuestions({
344
+ const stakeholderInterview = deriveStakeholderInterview({
308
345
  job,
309
346
  questionBank: questions,
310
- options: {
311
- targetMinutes: 60,
312
- skillBehaviourRatio: 0.6,
313
- },
314
347
  });
315
348
 
316
349
  return {
@@ -321,17 +354,21 @@ export function prepareAllInterviews({
321
354
  trackId: track?.id || null,
322
355
  trackName: track?.name || null,
323
356
  interviews: {
324
- short: {
325
- ...shortInterview,
326
- type: "short",
327
- typeInfo: INTERVIEW_TYPES.short,
357
+ mission: {
358
+ ...missionInterview,
359
+ type: "mission",
360
+ typeInfo: INTERVIEW_TYPES.mission,
361
+ },
362
+ decomposition: {
363
+ ...decompositionInterview,
364
+ type: "decomposition",
365
+ typeInfo: INTERVIEW_TYPES.decomposition,
328
366
  },
329
- behaviour: {
330
- ...behaviourInterview,
331
- type: "behaviour",
332
- typeInfo: INTERVIEW_TYPES.behaviour,
367
+ stakeholder: {
368
+ ...stakeholderInterview,
369
+ type: "stakeholder",
370
+ typeInfo: INTERVIEW_TYPES.stakeholder,
333
371
  },
334
- full: { ...fullInterview, type: "full", typeInfo: INTERVIEW_TYPES.full },
335
372
  },
336
373
  };
337
374
  }
@@ -259,6 +259,21 @@ function formatSingleSource(view) {
259
259
  for (const q of byLevel[level]) {
260
260
  lines.push(` • [${q.id}] ${q.text}`);
261
261
  lines.push(` Duration: ${q.expectedDurationMinutes} min`);
262
+ if (q.context) {
263
+ lines.push(` Context: ${q.context}`);
264
+ }
265
+ if (q.simulationPrompts && q.simulationPrompts.length > 0) {
266
+ lines.push(" Steer the simulation:");
267
+ for (const prompt of q.simulationPrompts) {
268
+ lines.push(` - ${prompt}`);
269
+ }
270
+ }
271
+ if (q.decompositionPrompts && q.decompositionPrompts.length > 0) {
272
+ lines.push(" Guide candidate thinking:");
273
+ for (const prompt of q.decompositionPrompts) {
274
+ lines.push(` - ${prompt}`);
275
+ }
276
+ }
262
277
  if (q.lookingFor.length > 0) {
263
278
  lines.push(" Looking for:");
264
279
  for (const item of q.lookingFor) {
@@ -46,6 +46,9 @@ export const BEHAVIOUR_MATURITIES = [
46
46
  * @property {string[]} lookingFor - Expected answer indicators
47
47
  * @property {number} expectedDurationMinutes - Time estimate
48
48
  * @property {string[]} [followUps] - Follow-up questions
49
+ * @property {string} [context] - Scenario context
50
+ * @property {string[]} [simulationPrompts] - Simulation steering prompts
51
+ * @property {string[]} [decompositionPrompts] - Decomposition guidance prompts
49
52
  */
50
53
 
51
54
  /**
@@ -103,6 +106,11 @@ function getSkillCapability(skillId, skills) {
103
106
  return skill ? skill.capability : null;
104
107
  }
105
108
 
109
+ /**
110
+ * Role type keys in question YAML files
111
+ */
112
+ const ROLE_TYPES = ["professionalQuestions", "managementQuestions"];
113
+
106
114
  /**
107
115
  * Flatten all questions from question bank
108
116
  * @param {Object} questionBank
@@ -115,78 +123,76 @@ export function flattenQuestions(questionBank, skills, behaviours, filter) {
115
123
  const questions = [];
116
124
 
117
125
  // Process skill questions
118
- for (const [skillId, levels] of Object.entries(
126
+ for (const [skillId, roleTypes] of Object.entries(
119
127
  questionBank.skillLevels || {},
120
128
  )) {
121
129
  const skillName = getSkillName(skillId, skills);
122
130
  const capability = getSkillCapability(skillId, skills);
123
131
 
124
- // Filter by skill IDs
125
132
  if (filter.skills && !filter.skills.includes(skillId)) continue;
126
-
127
- // Skip skills if filtering by specific behaviours
128
133
  if (filter.behaviours) continue;
129
-
130
- // Filter by capability
131
134
  if (filter.capability && capability !== filter.capability) continue;
132
-
133
- for (const [level, levelQuestions] of Object.entries(levels)) {
134
- // Filter by level
135
- if (filter.level && level !== filter.level) continue;
136
-
137
- // Skip if filtering by maturity (behaviour-only filter)
138
- if (filter.maturity) continue;
139
-
140
- for (const q of levelQuestions) {
141
- questions.push({
142
- source: skillId,
143
- sourceName: skillName,
144
- sourceType: "skill",
145
- level,
146
- id: q.id,
147
- text: q.text,
148
- lookingFor: q.lookingFor || [],
149
- expectedDurationMinutes: q.expectedDurationMinutes || 5,
150
- followUps: q.followUps || [],
151
- });
135
+ if (filter.maturity) continue;
136
+
137
+ for (const roleType of ROLE_TYPES) {
138
+ const levels = roleTypes[roleType];
139
+ if (!levels) continue;
140
+
141
+ for (const [level, levelQuestions] of Object.entries(levels)) {
142
+ if (filter.level && level !== filter.level) continue;
143
+
144
+ for (const q of levelQuestions) {
145
+ questions.push({
146
+ source: skillId,
147
+ sourceName: skillName,
148
+ sourceType: "skill",
149
+ level,
150
+ id: q.id,
151
+ text: q.text,
152
+ lookingFor: q.lookingFor || [],
153
+ expectedDurationMinutes: q.expectedDurationMinutes || 5,
154
+ followUps: q.followUps || [],
155
+ context: q.context || null,
156
+ decompositionPrompts: q.decompositionPrompts || [],
157
+ });
158
+ }
152
159
  }
153
160
  }
154
161
  }
155
162
 
156
163
  // Process behaviour questions
157
- for (const [behaviourId, maturities] of Object.entries(
164
+ for (const [behaviourId, roleTypes] of Object.entries(
158
165
  questionBank.behaviourMaturities || {},
159
166
  )) {
160
167
  const behaviourName = getBehaviourName(behaviourId, behaviours);
161
168
 
162
- // Filter by behaviour IDs
163
169
  if (filter.behaviours && !filter.behaviours.includes(behaviourId)) continue;
164
-
165
- // Skip behaviours if filtering by capability (skill-only filter)
166
170
  if (filter.capability) continue;
167
-
168
- // Skip behaviours if filtering by specific skills
169
171
  if (filter.skills) continue;
170
-
171
- for (const [maturity, maturityQuestions] of Object.entries(maturities)) {
172
- // Filter by maturity
173
- if (filter.maturity && maturity !== filter.maturity) continue;
174
-
175
- // Skip if filtering by level (skill-only filter)
176
- if (filter.level) continue;
177
-
178
- for (const q of maturityQuestions) {
179
- questions.push({
180
- source: behaviourId,
181
- sourceName: behaviourName,
182
- sourceType: "behaviour",
183
- level: maturity,
184
- id: q.id,
185
- text: q.text,
186
- lookingFor: q.lookingFor || [],
187
- expectedDurationMinutes: q.expectedDurationMinutes || 5,
188
- followUps: q.followUps || [],
189
- });
172
+ if (filter.level) continue;
173
+
174
+ for (const roleType of ROLE_TYPES) {
175
+ const maturities = roleTypes[roleType];
176
+ if (!maturities) continue;
177
+
178
+ for (const [maturity, maturityQuestions] of Object.entries(maturities)) {
179
+ if (filter.maturity && maturity !== filter.maturity) continue;
180
+
181
+ for (const q of maturityQuestions) {
182
+ questions.push({
183
+ source: behaviourId,
184
+ sourceName: behaviourName,
185
+ sourceType: "behaviour",
186
+ level: maturity,
187
+ id: q.id,
188
+ text: q.text,
189
+ lookingFor: q.lookingFor || [],
190
+ expectedDurationMinutes: q.expectedDurationMinutes || 5,
191
+ followUps: q.followUps || [],
192
+ context: q.context || null,
193
+ simulationPrompts: q.simulationPrompts || [],
194
+ });
195
+ }
190
196
  }
191
197
  }
192
198
  }
@@ -211,12 +217,16 @@ export function calculateStats(questions, questionBank) {
211
217
 
212
218
  // Calculate full stats for skills and behaviours
213
219
  const skillStats = {};
214
- for (const [skillId, levels] of Object.entries(
220
+ for (const [skillId, roleTypes] of Object.entries(
215
221
  questionBank.skillLevels || {},
216
222
  )) {
217
223
  skillStats[skillId] = {};
218
224
  for (const level of SKILL_LEVELS) {
219
- skillStats[skillId][level] = (levels[level] || []).length;
225
+ let count = 0;
226
+ for (const roleType of ROLE_TYPES) {
227
+ count += (roleTypes[roleType]?.[level] || []).length;
228
+ }
229
+ skillStats[skillId][level] = count;
220
230
  }
221
231
  skillStats[skillId].total = Object.values(skillStats[skillId]).reduce(
222
232
  (a, b) => a + b,
@@ -225,14 +235,16 @@ export function calculateStats(questions, questionBank) {
225
235
  }
226
236
 
227
237
  const behaviourStats = {};
228
- for (const [behaviourId, maturities] of Object.entries(
238
+ for (const [behaviourId, roleTypes] of Object.entries(
229
239
  questionBank.behaviourMaturities || {},
230
240
  )) {
231
241
  behaviourStats[behaviourId] = {};
232
242
  for (const maturity of BEHAVIOUR_MATURITIES) {
233
- behaviourStats[behaviourId][maturity] = (
234
- maturities[maturity] || []
235
- ).length;
243
+ let count = 0;
244
+ for (const roleType of ROLE_TYPES) {
245
+ count += (roleTypes[roleType]?.[maturity] || []).length;
246
+ }
247
+ behaviourStats[behaviourId][maturity] = count;
236
248
  }
237
249
  behaviourStats[behaviourId].total = Object.values(
238
250
  behaviourStats[behaviourId],
@@ -237,35 +237,52 @@ async function loadCapabilitiesFromDir(capabilitiesDir) {
237
237
  }
238
238
 
239
239
  /**
240
- * Load questions from folder structure using skill/behaviour IDs
240
+ * Load questions from folder structure using skill/behaviour/capability IDs
241
241
  * @param {string} questionsDir - Path to questions directory
242
242
  * @param {Array} skills - Skills array (with id property)
243
243
  * @param {Array} behaviours - Behaviours array (with id property)
244
+ * @param {Array} capabilities - Capabilities array (with id property)
244
245
  * @returns {Promise<Object>}
245
246
  */
246
- async function loadQuestionFolder(questionsDir, skills, behaviours) {
247
- const [skillEntries, behaviourEntries] = await Promise.all([
248
- Promise.all(
249
- skills.map(async (skill) => {
250
- const content = await tryLoadYamlFile(
251
- `${questionsDir}/skills/${skill.id}.yaml`,
252
- );
253
- return [skill.id, content || {}];
254
- }),
255
- ),
256
- Promise.all(
257
- behaviours.map(async (behaviour) => {
258
- const content = await tryLoadYamlFile(
259
- `${questionsDir}/behaviours/${behaviour.id}.yaml`,
260
- );
261
- return [behaviour.id, content || {}];
262
- }),
263
- ),
264
- ]);
247
+ async function loadQuestionFolder(
248
+ questionsDir,
249
+ skills,
250
+ behaviours,
251
+ capabilities,
252
+ ) {
253
+ const [skillEntries, behaviourEntries, capabilityEntries] = await Promise.all(
254
+ [
255
+ Promise.all(
256
+ skills.map(async (skill) => {
257
+ const content = await tryLoadYamlFile(
258
+ `${questionsDir}/skills/${skill.id}.yaml`,
259
+ );
260
+ return [skill.id, content || {}];
261
+ }),
262
+ ),
263
+ Promise.all(
264
+ behaviours.map(async (behaviour) => {
265
+ const content = await tryLoadYamlFile(
266
+ `${questionsDir}/behaviours/${behaviour.id}.yaml`,
267
+ );
268
+ return [behaviour.id, content || {}];
269
+ }),
270
+ ),
271
+ Promise.all(
272
+ capabilities.map(async (capability) => {
273
+ const content = await tryLoadYamlFile(
274
+ `${questionsDir}/capabilities/${capability.id}.yaml`,
275
+ );
276
+ return [capability.id, content || {}];
277
+ }),
278
+ ),
279
+ ],
280
+ );
265
281
 
266
282
  return {
267
283
  skillLevels: Object.fromEntries(skillEntries),
268
284
  behaviourMaturities: Object.fromEntries(behaviourEntries),
285
+ capabilityLevels: Object.fromEntries(capabilityEntries),
269
286
  };
270
287
  }
271
288
 
@@ -293,11 +310,12 @@ export async function loadAllData(dataDir = "./data") {
293
310
  loadYamlFile(`${dataDir}/framework.yaml`),
294
311
  ]);
295
312
 
296
- // Load questions using skill/behaviour IDs
313
+ // Load questions using skill/behaviour/capability IDs
297
314
  const questions = await loadQuestionFolder(
298
315
  `${dataDir}/questions`,
299
316
  skills,
300
317
  behaviours,
318
+ capabilities,
301
319
  );
302
320
 
303
321
  return {
@@ -81,8 +81,8 @@ export function renderInterviewDetail(params) {
81
81
  return;
82
82
  }
83
83
 
84
- // State for current interview type (default to first: Screening)
85
- let currentType = "short";
84
+ // State for current interview type (default to first: Mission Fit)
85
+ let currentType = "mission";
86
86
 
87
87
  const page = div(
88
88
  { className: "interview-detail-page" },
@@ -194,23 +194,32 @@ function createInterviewSummary(interview) {
194
194
  { className: "interview-summary-header" },
195
195
  h2({}, `${typeInfo.icon} ${typeInfo.name}`),
196
196
  p({ className: "text-muted" }, typeInfo.description),
197
+ typeInfo.panel
198
+ ? p({ className: "text-muted" }, `Panel: ${typeInfo.panel}`)
199
+ : null,
197
200
  ),
198
201
  div(
199
202
  { className: "interview-summary-stats" },
200
203
  createBadge(`${interview.questions.length} questions`, "default"),
201
204
  createBadge(`~${interview.expectedDurationMinutes} minutes`, "secondary"),
202
- interview.coverage.skills.length > 0
205
+ interview.coverage.skills?.length > 0
203
206
  ? createBadge(
204
207
  `${interview.coverage.skills.length} skills covered`,
205
208
  "primary",
206
209
  )
207
210
  : null,
208
- interview.coverage.behaviours.length > 0
211
+ interview.coverage.behaviours?.length > 0
209
212
  ? createBadge(
210
213
  `${interview.coverage.behaviours.length} behaviours covered`,
211
214
  "primary",
212
215
  )
213
216
  : null,
217
+ interview.coverage.capabilities?.length > 0
218
+ ? createBadge(
219
+ `${interview.coverage.capabilities.length} capabilities covered`,
220
+ "primary",
221
+ )
222
+ : null,
214
223
  ),
215
224
  );
216
225
  }
@@ -226,6 +235,9 @@ function createQuestionsDisplay(interview, framework) {
226
235
  const behaviourQuestions = interview.questions.filter(
227
236
  (q) => q.targetType === "behaviour",
228
237
  );
238
+ const capabilityQuestions = interview.questions.filter(
239
+ (q) => q.targetType === "capability",
240
+ );
229
241
 
230
242
  const sections = [];
231
243
 
@@ -241,12 +253,21 @@ function createQuestionsDisplay(interview, framework) {
241
253
  if (behaviourQuestions.length > 0) {
242
254
  sections.push(
243
255
  createDetailSection({
244
- title: `${getConceptEmoji(framework, "behaviour")} Behaviour Questions (${behaviourQuestions.length})`,
256
+ title: `${getConceptEmoji(framework, "behaviour")} Stakeholder Simulation (${behaviourQuestions.length})`,
245
257
  content: createQuestionsList(behaviourQuestions),
246
258
  }),
247
259
  );
248
260
  }
249
261
 
262
+ if (capabilityQuestions.length > 0) {
263
+ sections.push(
264
+ createDetailSection({
265
+ title: `${getConceptEmoji(framework, "capability") || "🧩"} Decomposition Questions (${capabilityQuestions.length})`,
266
+ content: createQuestionsList(capabilityQuestions),
267
+ }),
268
+ );
269
+ }
270
+
250
271
  if (sections.length === 0) {
251
272
  return div(
252
273
  { className: "card" },
@@ -262,6 +283,7 @@ function createQuestionsDisplay(interview, framework) {
262
283
 
263
284
  /**
264
285
  * Create questions list
286
+ * @param {Array} questions - Questions to display
265
287
  */
266
288
  function createQuestionsList(questions) {
267
289
  return div(
@@ -272,10 +294,41 @@ function createQuestionsList(questions) {
272
294
 
273
295
  /**
274
296
  * Create question card
297
+ * @param {Object} questionEntry - Question entry
298
+ * @param {number} number - Question number
275
299
  */
276
300
  function createQuestionCard(questionEntry, number) {
277
301
  const { question, targetName, targetLevel } = questionEntry;
278
302
 
303
+ const contextSection = question.context
304
+ ? div(
305
+ { className: "question-context" },
306
+ h4({}, "Context:"),
307
+ p({}, question.context),
308
+ )
309
+ : null;
310
+
311
+ const decompositionPromptsList =
312
+ question.decompositionPrompts && question.decompositionPrompts.length > 0
313
+ ? div(
314
+ { className: "question-prompts" },
315
+ h4({}, "Guide candidate thinking:"),
316
+ ul(
317
+ {},
318
+ ...question.decompositionPrompts.map((prompt) => li({}, prompt)),
319
+ ),
320
+ )
321
+ : null;
322
+
323
+ const simulationPromptsList =
324
+ question.simulationPrompts && question.simulationPrompts.length > 0
325
+ ? div(
326
+ { className: "question-prompts" },
327
+ h4({}, "Steer the simulation:"),
328
+ ul({}, ...question.simulationPrompts.map((prompt) => li({}, prompt))),
329
+ )
330
+ : null;
331
+
279
332
  const followUpsList =
280
333
  question.followUps && question.followUps.length > 0
281
334
  ? div(
@@ -309,6 +362,9 @@ function createQuestionCard(questionEntry, number) {
309
362
  ),
310
363
  ),
311
364
  div({ className: "question-text" }, question.text),
365
+ contextSection,
366
+ decompositionPromptsList,
367
+ simulationPromptsList,
312
368
  followUpsList,
313
369
  lookingForList,
314
370
  );