@forwardimpact/pathway 0.25.15 → 0.25.21

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 (34) hide show
  1. package/bin/fit-pathway.js +62 -54
  2. package/package.json +1 -3
  3. package/src/commands/agent-io.js +120 -0
  4. package/src/commands/agent.js +266 -349
  5. package/src/commands/init.js +2 -2
  6. package/src/commands/job.js +237 -183
  7. package/src/components/comparison-radar.js +118 -103
  8. package/src/components/progression-table.js +244 -208
  9. package/src/formatters/index.js +0 -19
  10. package/src/formatters/interview/markdown.js +100 -88
  11. package/src/formatters/job/description.js +76 -75
  12. package/src/formatters/job/dom.js +113 -97
  13. package/src/formatters/level/dom.js +87 -102
  14. package/src/formatters/questions/markdown.js +37 -33
  15. package/src/formatters/questions/shared.js +142 -75
  16. package/src/formatters/skill/dom.js +102 -93
  17. package/src/lib/comparison-radar-chart.js +256 -0
  18. package/src/lib/radar-utils.js +199 -0
  19. package/src/lib/radar.js +25 -662
  20. package/src/pages/agent-builder-download.js +170 -0
  21. package/src/pages/agent-builder-preview.js +344 -0
  22. package/src/pages/agent-builder.js +6 -550
  23. package/src/pages/progress-comparison.js +110 -0
  24. package/src/pages/progress.js +11 -111
  25. package/src/pages/self-assessment-steps.js +494 -0
  26. package/src/pages/self-assessment.js +54 -504
  27. package/src/formatters/behaviour/microdata.js +0 -106
  28. package/src/formatters/discipline/microdata.js +0 -117
  29. package/src/formatters/driver/microdata.js +0 -91
  30. package/src/formatters/level/microdata.js +0 -141
  31. package/src/formatters/microdata-shared.js +0 -184
  32. package/src/formatters/skill/microdata.js +0 -151
  33. package/src/formatters/stage/microdata.js +0 -116
  34. package/src/formatters/track/microdata.js +0 -111
@@ -35,14 +35,98 @@ import { createJsonLdScript, levelToJsonLd } from "../json-ld.js";
35
35
  * @param {boolean} [options.showBackLink=true] - Whether to show back navigation link
36
36
  * @returns {HTMLElement}
37
37
  */
38
+ /**
39
+ * Create a proficiency row for the base skill table
40
+ * @param {string} label
41
+ * @param {string} badgeClass
42
+ * @param {string|undefined} proficiency
43
+ * @returns {HTMLElement}
44
+ */
45
+ function createProficiencyRow(label, badgeClass, proficiency) {
46
+ return tr(
47
+ {},
48
+ td({}, span({ className: `badge ${badgeClass}` }, label)),
49
+ td(
50
+ {},
51
+ proficiency
52
+ ? createLevelDots(
53
+ SKILL_PROFICIENCY_ORDER.indexOf(proficiency),
54
+ SKILL_PROFICIENCY_ORDER.length,
55
+ )
56
+ : span({ className: "text-muted" }, "—"),
57
+ ),
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Create the base skill proficiencies and behaviour maturity section
63
+ * @param {Object} view
64
+ * @returns {HTMLElement}
65
+ */
66
+ function createBaseProfileSection(view) {
67
+ const profs = view.baseSkillProficiencies || {};
68
+ return div(
69
+ { className: "detail-section" },
70
+ div(
71
+ { className: "content-columns" },
72
+ div(
73
+ { className: "column" },
74
+ heading2({ className: "section-title" }, "Base Skill Proficiencies"),
75
+ table(
76
+ { className: "level-table" },
77
+ thead({}, tr({}, th({}, "Type"), th({}, "Level"))),
78
+ tbody(
79
+ {},
80
+ createProficiencyRow("Primary", "badge-primary", profs.primary),
81
+ createProficiencyRow(
82
+ "Secondary",
83
+ "badge-secondary",
84
+ profs.secondary,
85
+ ),
86
+ createProficiencyRow("Broad", "badge-broad", profs.broad),
87
+ ),
88
+ ),
89
+ ),
90
+ div(
91
+ { className: "column" },
92
+ heading2({ className: "section-title" }, "Base Behaviour Maturity"),
93
+ view.baseBehaviourMaturity
94
+ ? table(
95
+ { className: "level-table" },
96
+ thead({}, tr({}, th({}, "Maturity"), th({}, "Level"))),
97
+ tbody(
98
+ {},
99
+ tr(
100
+ {},
101
+ td(
102
+ {},
103
+ view.baseBehaviourMaturity.charAt(0).toUpperCase() +
104
+ view.baseBehaviourMaturity.slice(1),
105
+ ),
106
+ td(
107
+ {},
108
+ createLevelDots(
109
+ BEHAVIOUR_MATURITY_ORDER.indexOf(
110
+ view.baseBehaviourMaturity,
111
+ ),
112
+ BEHAVIOUR_MATURITY_ORDER.length,
113
+ ),
114
+ ),
115
+ ),
116
+ ),
117
+ )
118
+ : p({ className: "text-muted" }, "—"),
119
+ ),
120
+ ),
121
+ );
122
+ }
123
+
38
124
  export function levelToDOM(level, { framework, showBackLink = true } = {}) {
39
125
  const view = prepareLevelDetail(level);
40
126
  const emoji = framework ? getConceptEmoji(framework, "level") : "📊";
41
127
  return div(
42
128
  { className: "detail-page level-detail" },
43
- // JSON-LD structured data
44
129
  createJsonLdScript(levelToJsonLd(level)),
45
- // Header
46
130
  div(
47
131
  { className: "page-header" },
48
132
  showBackLink ? createBackLink("/level", "← Back to Levels") : null,
@@ -68,7 +152,6 @@ export function levelToDOM(level, { framework, showBackLink = true } = {}) {
68
152
  : null,
69
153
  ),
70
154
 
71
- // Titles section
72
155
  view.professionalTitle || view.managementTitle
73
156
  ? div(
74
157
  { className: "detail-section" },
@@ -93,7 +176,6 @@ export function levelToDOM(level, { framework, showBackLink = true } = {}) {
93
176
  )
94
177
  : null,
95
178
 
96
- // Expectations
97
179
  view.expectations && Object.keys(view.expectations).length > 0
98
180
  ? div(
99
181
  { className: "detail-section" },
@@ -111,103 +193,6 @@ export function levelToDOM(level, { framework, showBackLink = true } = {}) {
111
193
  )
112
194
  : null,
113
195
 
114
- // Base Skill Proficiencies and Base Behaviour Maturity in two columns
115
- div(
116
- { className: "detail-section" },
117
- div(
118
- { className: "content-columns" },
119
- // Base Skill Proficiencies column
120
- div(
121
- { className: "column" },
122
- heading2({ className: "section-title" }, "Base Skill Proficiencies"),
123
- table(
124
- { className: "level-table" },
125
- thead({}, tr({}, th({}, "Type"), th({}, "Level"))),
126
- tbody(
127
- {},
128
- tr(
129
- {},
130
- td({}, span({ className: "badge badge-primary" }, "Primary")),
131
- td(
132
- {},
133
- view.baseSkillProficiencies?.primary
134
- ? createLevelDots(
135
- SKILL_PROFICIENCY_ORDER.indexOf(
136
- view.baseSkillProficiencies.primary,
137
- ),
138
- SKILL_PROFICIENCY_ORDER.length,
139
- )
140
- : span({ className: "text-muted" }, "—"),
141
- ),
142
- ),
143
- tr(
144
- {},
145
- td(
146
- {},
147
- span({ className: "badge badge-secondary" }, "Secondary"),
148
- ),
149
- td(
150
- {},
151
- view.baseSkillProficiencies?.secondary
152
- ? createLevelDots(
153
- SKILL_PROFICIENCY_ORDER.indexOf(
154
- view.baseSkillProficiencies.secondary,
155
- ),
156
- SKILL_PROFICIENCY_ORDER.length,
157
- )
158
- : span({ className: "text-muted" }, "—"),
159
- ),
160
- ),
161
- tr(
162
- {},
163
- td({}, span({ className: "badge badge-broad" }, "Broad")),
164
- td(
165
- {},
166
- view.baseSkillProficiencies?.broad
167
- ? createLevelDots(
168
- SKILL_PROFICIENCY_ORDER.indexOf(
169
- view.baseSkillProficiencies.broad,
170
- ),
171
- SKILL_PROFICIENCY_ORDER.length,
172
- )
173
- : span({ className: "text-muted" }, "—"),
174
- ),
175
- ),
176
- ),
177
- ),
178
- ),
179
- // Base Behaviour Maturity column
180
- div(
181
- { className: "column" },
182
- heading2({ className: "section-title" }, "Base Behaviour Maturity"),
183
- view.baseBehaviourMaturity
184
- ? table(
185
- { className: "level-table" },
186
- thead({}, tr({}, th({}, "Maturity"), th({}, "Level"))),
187
- tbody(
188
- {},
189
- tr(
190
- {},
191
- td(
192
- {},
193
- view.baseBehaviourMaturity.charAt(0).toUpperCase() +
194
- view.baseBehaviourMaturity.slice(1),
195
- ),
196
- td(
197
- {},
198
- createLevelDots(
199
- BEHAVIOUR_MATURITY_ORDER.indexOf(
200
- view.baseBehaviourMaturity,
201
- ),
202
- BEHAVIOUR_MATURITY_ORDER.length,
203
- ),
204
- ),
205
- ),
206
- ),
207
- )
208
- : p({ className: "text-muted" }, "—"),
209
- ),
210
- ),
211
- ),
196
+ createBaseProfileSection(view),
212
197
  );
213
198
  }
@@ -223,10 +223,43 @@ function formatTable(view) {
223
223
  }
224
224
 
225
225
  /**
226
- * Format single source deep dive
227
- * @param {Object} view - Questions view
228
- * @returns {string}
226
+ * Format a single question's detail lines
227
+ * @param {string[]} lines
228
+ * @param {Object} q - Flattened question
229
229
  */
230
+ function formatQuestionDetail(lines, q) {
231
+ lines.push(` • [${q.id}] ${q.text}`);
232
+ lines.push(` Duration: ${q.expectedDurationMinutes} min`);
233
+ if (q.context) {
234
+ lines.push(` Context: ${q.context}`);
235
+ }
236
+ if (q.simulationPrompts && q.simulationPrompts.length > 0) {
237
+ lines.push(" Steer the simulation:");
238
+ for (const prompt of q.simulationPrompts) {
239
+ lines.push(` - ${prompt}`);
240
+ }
241
+ }
242
+ if (q.decompositionPrompts && q.decompositionPrompts.length > 0) {
243
+ lines.push(" Guide candidate thinking:");
244
+ for (const prompt of q.decompositionPrompts) {
245
+ lines.push(` - ${prompt}`);
246
+ }
247
+ }
248
+ if (q.lookingFor.length > 0) {
249
+ lines.push(" Looking for:");
250
+ for (const item of q.lookingFor) {
251
+ lines.push(` - ${item}`);
252
+ }
253
+ }
254
+ if (q.followUps.length > 0) {
255
+ lines.push(" Follow-ups:");
256
+ for (const fu of q.followUps) {
257
+ lines.push(` → ${fu}`);
258
+ }
259
+ }
260
+ lines.push("");
261
+ }
262
+
230
263
  function formatSingleSource(view) {
231
264
  const lines = [];
232
265
  const { questions, stats } = view;
@@ -259,36 +292,7 @@ function formatSingleSource(view) {
259
292
 
260
293
  lines.push(level.toUpperCase());
261
294
  for (const q of byLevel[level]) {
262
- lines.push(` • [${q.id}] ${q.text}`);
263
- lines.push(` Duration: ${q.expectedDurationMinutes} min`);
264
- if (q.context) {
265
- lines.push(` Context: ${q.context}`);
266
- }
267
- if (q.simulationPrompts && q.simulationPrompts.length > 0) {
268
- lines.push(" Steer the simulation:");
269
- for (const prompt of q.simulationPrompts) {
270
- lines.push(` - ${prompt}`);
271
- }
272
- }
273
- if (q.decompositionPrompts && q.decompositionPrompts.length > 0) {
274
- lines.push(" Guide candidate thinking:");
275
- for (const prompt of q.decompositionPrompts) {
276
- lines.push(` - ${prompt}`);
277
- }
278
- }
279
- if (q.lookingFor.length > 0) {
280
- lines.push(" Looking for:");
281
- for (const item of q.lookingFor) {
282
- lines.push(` - ${item}`);
283
- }
284
- }
285
- if (q.followUps.length > 0) {
286
- lines.push(" Follow-ups:");
287
- for (const fu of q.followUps) {
288
- lines.push(` → ${fu}`);
289
- }
290
- }
291
- lines.push("");
295
+ formatQuestionDetail(lines, q);
292
296
  }
293
297
  }
294
298
 
@@ -111,6 +111,119 @@ function getSkillCapability(skillId, skills) {
111
111
  */
112
112
  const ROLE_TYPES = ["professionalQuestions", "managementQuestions"];
113
113
 
114
+ /**
115
+ * Check whether skill questions should be included given the current filter
116
+ * @param {string} skillId
117
+ * @param {Array} skills
118
+ * @param {QuestionsFilter} filter
119
+ * @returns {boolean}
120
+ */
121
+ function shouldIncludeSkill(skillId, skills, filter) {
122
+ if (filter.skills && !filter.skills.includes(skillId)) return false;
123
+ if (filter.behaviours) return false;
124
+ if (filter.maturity) return false;
125
+ if (filter.capability) {
126
+ const capability = getSkillCapability(skillId, skills);
127
+ if (capability !== filter.capability) return false;
128
+ }
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Check whether behaviour questions should be included given the current filter
134
+ * @param {string} behaviourId
135
+ * @param {QuestionsFilter} filter
136
+ * @returns {boolean}
137
+ */
138
+ function shouldIncludeBehaviour(behaviourId, filter) {
139
+ if (filter.behaviours && !filter.behaviours.includes(behaviourId))
140
+ return false;
141
+ if (filter.capability) return false;
142
+ if (filter.skills) return false;
143
+ if (filter.level) return false;
144
+ return true;
145
+ }
146
+
147
+ /**
148
+ * Flatten skill questions from a single skill entry
149
+ * @param {string} skillId
150
+ * @param {Object} roleTypes
151
+ * @param {Array} skills
152
+ * @param {QuestionsFilter} filter
153
+ * @param {FlattenedQuestion[]} out
154
+ */
155
+ function flattenSkillQuestions(skillId, roleTypes, skills, filter, out) {
156
+ const skillName = getSkillName(skillId, skills);
157
+
158
+ for (const roleType of ROLE_TYPES) {
159
+ const levels = roleTypes[roleType];
160
+ if (!levels) continue;
161
+
162
+ for (const [level, levelQuestions] of Object.entries(levels)) {
163
+ if (filter.level && level !== filter.level) continue;
164
+
165
+ for (const q of levelQuestions) {
166
+ out.push({
167
+ source: skillId,
168
+ sourceName: skillName,
169
+ sourceType: "skill",
170
+ level,
171
+ id: q.id,
172
+ text: q.text,
173
+ lookingFor: q.lookingFor || [],
174
+ expectedDurationMinutes: q.expectedDurationMinutes || 5,
175
+ followUps: q.followUps || [],
176
+ context: q.context || null,
177
+ decompositionPrompts: q.decompositionPrompts || [],
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Flatten behaviour questions from a single behaviour entry
186
+ * @param {string} behaviourId
187
+ * @param {Object} roleTypes
188
+ * @param {Array} behaviours
189
+ * @param {QuestionsFilter} filter
190
+ * @param {FlattenedQuestion[]} out
191
+ */
192
+ function flattenBehaviourQuestions(
193
+ behaviourId,
194
+ roleTypes,
195
+ behaviours,
196
+ filter,
197
+ out,
198
+ ) {
199
+ const behaviourName = getBehaviourName(behaviourId, behaviours);
200
+
201
+ for (const roleType of ROLE_TYPES) {
202
+ const maturities = roleTypes[roleType];
203
+ if (!maturities) continue;
204
+
205
+ for (const [maturity, maturityQuestions] of Object.entries(maturities)) {
206
+ if (filter.maturity && maturity !== filter.maturity) continue;
207
+
208
+ for (const q of maturityQuestions) {
209
+ out.push({
210
+ source: behaviourId,
211
+ sourceName: behaviourName,
212
+ sourceType: "behaviour",
213
+ level: maturity,
214
+ id: q.id,
215
+ text: q.text,
216
+ lookingFor: q.lookingFor || [],
217
+ expectedDurationMinutes: q.expectedDurationMinutes || 5,
218
+ followUps: q.followUps || [],
219
+ context: q.context || null,
220
+ simulationPrompts: q.simulationPrompts || [],
221
+ });
222
+ }
223
+ }
224
+ }
225
+ }
226
+
114
227
  /**
115
228
  * Flatten all questions from question bank
116
229
  * @param {Object} questionBank
@@ -122,84 +235,43 @@ const ROLE_TYPES = ["professionalQuestions", "managementQuestions"];
122
235
  export function flattenQuestions(questionBank, skills, behaviours, filter) {
123
236
  const questions = [];
124
237
 
125
- // Process skill questions
126
238
  for (const [skillId, roleTypes] of Object.entries(
127
239
  questionBank.skillProficiencies || {},
128
240
  )) {
129
- const skillName = getSkillName(skillId, skills);
130
- const capability = getSkillCapability(skillId, skills);
131
-
132
- if (filter.skills && !filter.skills.includes(skillId)) continue;
133
- if (filter.behaviours) continue;
134
- if (filter.capability && capability !== filter.capability) continue;
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
- }
159
- }
160
- }
241
+ if (!shouldIncludeSkill(skillId, skills, filter)) continue;
242
+ flattenSkillQuestions(skillId, roleTypes, skills, filter, questions);
161
243
  }
162
244
 
163
- // Process behaviour questions
164
245
  for (const [behaviourId, roleTypes] of Object.entries(
165
246
  questionBank.behaviourMaturities || {},
166
247
  )) {
167
- const behaviourName = getBehaviourName(behaviourId, behaviours);
168
-
169
- if (filter.behaviours && !filter.behaviours.includes(behaviourId)) continue;
170
- if (filter.capability) continue;
171
- if (filter.skills) continue;
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
- }
196
- }
197
- }
248
+ if (!shouldIncludeBehaviour(behaviourId, filter)) continue;
249
+ flattenBehaviourQuestions(
250
+ behaviourId,
251
+ roleTypes,
252
+ behaviours,
253
+ filter,
254
+ questions,
255
+ );
198
256
  }
199
257
 
200
258
  return questions;
201
259
  }
202
260
 
261
+ /**
262
+ * Count questions across role types for a given level/maturity
263
+ * @param {Object} roleTypes - Role type map
264
+ * @param {string} levelKey - Level or maturity key
265
+ * @returns {number}
266
+ */
267
+ function countQuestionsForLevel(roleTypes, levelKey) {
268
+ let count = 0;
269
+ for (const roleType of ROLE_TYPES) {
270
+ count += (roleTypes[roleType]?.[levelKey] || []).length;
271
+ }
272
+ return count;
273
+ }
274
+
203
275
  /**
204
276
  * Calculate question statistics
205
277
  * @param {FlattenedQuestion[]} questions
@@ -222,11 +294,7 @@ export function calculateStats(questions, questionBank) {
222
294
  )) {
223
295
  skillStats[skillId] = {};
224
296
  for (const level of SKILL_PROFICIENCIES) {
225
- let count = 0;
226
- for (const roleType of ROLE_TYPES) {
227
- count += (roleTypes[roleType]?.[level] || []).length;
228
- }
229
- skillStats[skillId][level] = count;
297
+ skillStats[skillId][level] = countQuestionsForLevel(roleTypes, level);
230
298
  }
231
299
  skillStats[skillId].total = Object.values(skillStats[skillId]).reduce(
232
300
  (a, b) => a + b,
@@ -240,11 +308,10 @@ export function calculateStats(questions, questionBank) {
240
308
  )) {
241
309
  behaviourStats[behaviourId] = {};
242
310
  for (const maturity of BEHAVIOUR_MATURITIES) {
243
- let count = 0;
244
- for (const roleType of ROLE_TYPES) {
245
- count += (roleTypes[roleType]?.[maturity] || []).length;
246
- }
247
- behaviourStats[behaviourId][maturity] = count;
311
+ behaviourStats[behaviourId][maturity] = countQuestionsForLevel(
312
+ roleTypes,
313
+ maturity,
314
+ );
248
315
  }
249
316
  behaviourStats[behaviourId].total = Object.values(
250
317
  behaviourStats[behaviourId],