@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
@@ -3,30 +3,20 @@
3
3
  * A step-by-step interface for users to assess their skills and behaviours
4
4
  */
5
5
 
6
- import {
7
- render,
8
- div,
9
- h1,
10
- h2,
11
- h3,
12
- h4,
13
- p,
14
- span,
15
- button,
16
- a,
17
- } from "../lib/render.js";
6
+ import { render, div, h1, p, span, button } from "../lib/render.js";
18
7
  import { getState } from "../lib/state.js";
19
- import { createBadge } from "../components/card.js";
20
- import { createDisciplineSelect } from "../lib/form-controls.js";
21
8
  import {
22
- SKILL_PROFICIENCY_ORDER,
23
- BEHAVIOUR_MATURITY_ORDER,
24
9
  groupSkillsByCapability,
25
10
  getCapabilityOrder,
26
11
  getCapabilityEmoji,
27
12
  getConceptEmoji,
28
13
  } from "@forwardimpact/map/levels";
29
- import { formatLevel } from "../lib/render.js";
14
+ import {
15
+ renderIntroStep,
16
+ renderSkillsStep,
17
+ renderBehavioursStep,
18
+ renderResultsPreview,
19
+ } from "./self-assessment-steps.js";
30
20
 
31
21
  /**
32
22
  * Assessment state stored in memory
@@ -59,6 +49,17 @@ export function getAssessmentState() {
59
49
  return assessmentState;
60
50
  }
61
51
 
52
+ /**
53
+ * Format capability name for display
54
+ * @param {string} capabilityId
55
+ * @param {Array} capabilities
56
+ * @returns {string}
57
+ */
58
+ function formatCapability(capabilityId, capabilities) {
59
+ const capability = capabilities.find((c) => c.id === capabilityId);
60
+ return capability?.name || capabilityId;
61
+ }
62
+
62
63
  /**
63
64
  * Get steps for the wizard
64
65
  * @param {Object} data - App data
@@ -79,7 +80,6 @@ function getWizardSteps(data) {
79
80
  },
80
81
  ];
81
82
 
82
- // Add a step for each non-empty skill capability
83
83
  for (const capability of getCapabilityOrder(data.capabilities)) {
84
84
  const skills = skillsByCapability[capability];
85
85
  if (skills && skills.length > 0) {
@@ -94,7 +94,6 @@ function getWizardSteps(data) {
94
94
  }
95
95
  }
96
96
 
97
- // Add behaviours step
98
97
  steps.push({
99
98
  id: "behaviours",
100
99
  name: "Behaviours",
@@ -103,7 +102,6 @@ function getWizardSteps(data) {
103
102
  items: data.behaviours,
104
103
  });
105
104
 
106
- // Add results step
107
105
  steps.push({
108
106
  id: "results",
109
107
  name: "Results",
@@ -114,36 +112,54 @@ function getWizardSteps(data) {
114
112
  return steps;
115
113
  }
116
114
 
117
- /**
118
- * Format capability name for display
119
- * @param {string} capabilityId
120
- * @param {Array} capabilities
121
- * @returns {string}
122
- */
123
- function formatCapability(capabilityId, capabilities) {
124
- const capability = capabilities.find((c) => c.id === capabilityId);
125
- return capability?.name || capabilityId;
126
- }
127
-
128
115
  /**
129
116
  * Calculate progress percentage
130
117
  * @param {Object} data - App data
131
118
  * @returns {number}
132
119
  */
133
120
  function calculateProgress(data) {
134
- const totalSkills = data.skills.length;
135
- const totalBehaviours = data.behaviours.length;
136
- const totalItems = totalSkills + totalBehaviours;
137
-
121
+ const totalItems = data.skills.length + data.behaviours.length;
138
122
  if (totalItems === 0) return 0;
139
123
 
140
- const assessedSkills = Object.keys(assessmentState.skills).length;
141
- const assessedBehaviours = Object.keys(assessmentState.behaviours).length;
142
- const assessedItems = assessedSkills + assessedBehaviours;
124
+ const assessedItems =
125
+ Object.keys(assessmentState.skills).length +
126
+ Object.keys(assessmentState.behaviours).length;
143
127
 
144
128
  return Math.round((assessedItems / totalItems) * 100);
145
129
  }
146
130
 
131
+ /**
132
+ * Render content for the current step
133
+ * @param {Object} step - Current step configuration
134
+ * @param {Object} data - App data
135
+ * @returns {HTMLElement}
136
+ */
137
+ function renderStepContent(step, data) {
138
+ switch (step.type) {
139
+ case "intro":
140
+ return renderIntroStep(data, assessmentState);
141
+ case "skills":
142
+ return renderSkillsStep(
143
+ step,
144
+ data,
145
+ assessmentState,
146
+ renderSelfAssessment,
147
+ formatCapability,
148
+ );
149
+ case "behaviours":
150
+ return renderBehavioursStep(
151
+ step,
152
+ data,
153
+ assessmentState,
154
+ renderSelfAssessment,
155
+ );
156
+ case "results":
157
+ return renderResultsPreview(data, assessmentState, calculateProgress);
158
+ default:
159
+ return div({}, "Unknown step");
160
+ }
161
+ }
162
+
147
163
  /**
148
164
  * Render the self-assessment wizard
149
165
  */
@@ -155,7 +171,6 @@ export function renderSelfAssessment() {
155
171
 
156
172
  const page = div(
157
173
  { className: "self-assessment-page" },
158
- // Header
159
174
  div(
160
175
  { className: "page-header" },
161
176
  h1({ className: "page-title" }, "Self-Assessment"),
@@ -164,17 +179,11 @@ export function renderSelfAssessment() {
164
179
  "Assess your skills and behaviours to find matching roles and identify development opportunities.",
165
180
  ),
166
181
  ),
167
-
168
- // Progress bar
169
182
  createProgressBar(data, steps, currentStep),
170
-
171
- // Step content
172
183
  div(
173
184
  { className: "assessment-content", id: "assessment-content" },
174
185
  renderStepContent(step, data),
175
186
  ),
176
-
177
- // Navigation buttons
178
187
  createNavigationButtons(steps, currentStep),
179
188
  );
180
189
 
@@ -193,7 +202,6 @@ function createProgressBar(data, steps, currentStep) {
193
202
 
194
203
  return div(
195
204
  { className: "assessment-progress" },
196
- // Progress percentage
197
205
  div(
198
206
  { className: "progress-header" },
199
207
  span({ className: "progress-label" }, `${progress}% Complete`),
@@ -203,12 +211,10 @@ function createProgressBar(data, steps, currentStep) {
203
211
  `${Object.keys(assessmentState.behaviours).length}/${data.behaviours.length} behaviours`,
204
212
  ),
205
213
  ),
206
- // Progress bar
207
214
  div(
208
215
  { className: "progress-bar" },
209
216
  div({ className: "progress-bar-fill", style: `width: ${progress}%` }),
210
217
  ),
211
- // Step indicators
212
218
  div(
213
219
  { className: "step-indicators" },
214
220
  ...steps.map((step, index) =>
@@ -216,7 +222,6 @@ function createProgressBar(data, steps, currentStep) {
216
222
  {
217
223
  className: `step-indicator ${index === currentStep ? "active" : ""} ${index < currentStep ? "completed" : ""}`,
218
224
  onClick: () => {
219
- // Allow jumping to any step except results (unless assessment is complete)
220
225
  if (index < steps.length - 1 || calculateProgress(data) >= 50) {
221
226
  assessmentState.currentStep = index;
222
227
  renderSelfAssessment();
@@ -231,461 +236,6 @@ function createProgressBar(data, steps, currentStep) {
231
236
  );
232
237
  }
233
238
 
234
- /**
235
- * Render content for the current step
236
- * @param {Object} step - Current step configuration
237
- * @param {Object} data - App data
238
- * @returns {HTMLElement}
239
- */
240
- function renderStepContent(step, data) {
241
- switch (step.type) {
242
- case "intro":
243
- return renderIntroStep(data);
244
- case "skills":
245
- return renderSkillsStep(step, data);
246
- case "behaviours":
247
- return renderBehavioursStep(step, data);
248
- case "results":
249
- return renderResultsPreview(data);
250
- default:
251
- return div({}, "Unknown step");
252
- }
253
- }
254
-
255
- /**
256
- * Render introduction step
257
- * @param {Object} data - App data
258
- * @returns {HTMLElement}
259
- */
260
- function renderIntroStep(data) {
261
- return div(
262
- { className: "assessment-step assessment-intro" },
263
- div(
264
- { className: "intro-card" },
265
- h2({}, "Welcome to the Self-Assessment"),
266
- p(
267
- {},
268
- "This assessment helps you understand your current skill proficiencies and behaviours, " +
269
- "then matches you with suitable roles in the organization.",
270
- ),
271
-
272
- div(
273
- { className: "intro-info" },
274
- div(
275
- { className: "info-item" },
276
- span(
277
- { className: "info-icon" },
278
- getConceptEmoji(data.framework, "skill"),
279
- ),
280
- div(
281
- {},
282
- h4({}, `${data.skills.length} Skills`),
283
- p({}, "Across " + data.capabilities.length + " capabilities"),
284
- ),
285
- ),
286
- div(
287
- { className: "info-item" },
288
- span(
289
- { className: "info-icon" },
290
- getConceptEmoji(data.framework, "behaviour"),
291
- ),
292
- div(
293
- {},
294
- h4({}, `${data.behaviours.length} Behaviours`),
295
- p({}, "Key mindsets and ways of working"),
296
- ),
297
- ),
298
- div(
299
- { className: "info-item" },
300
- span({ className: "info-icon" }, "⏱️"),
301
- div({}, h4({}, "10-15 Minutes"), p({}, "Complete at your own pace")),
302
- ),
303
- ),
304
-
305
- // Optional discipline filter
306
- div(
307
- { className: "discipline-filter" },
308
- h3({}, "Optional: Focus on a Discipline"),
309
- p(
310
- { className: "text-muted" },
311
- "Select a discipline to highlight which skills are most relevant for that role. " +
312
- "You can still assess all skills.",
313
- ),
314
- createDisciplineSelect({
315
- id: "discipline-filter-select",
316
- disciplines: data.disciplines,
317
- initialValue: assessmentState.discipline || "",
318
- placeholder: "Select discipline",
319
- onChange: (value) => {
320
- assessmentState.discipline = value || null;
321
- },
322
- getDisplayName: (d) => d.specialization,
323
- }),
324
- ),
325
-
326
- div(
327
- { className: "intro-tips" },
328
- h3({}, "Tips for Accurate Self-Assessment"),
329
- div(
330
- { className: "auto-grid-sm" },
331
- createTipCard(
332
- "🎯",
333
- "Be Honest",
334
- "Rate yourself where you genuinely are, not where you aspire to be.",
335
- ),
336
- createTipCard(
337
- "📚",
338
- "Read Descriptions",
339
- "Hover over levels to see detailed descriptions for each.",
340
- ),
341
- createTipCard(
342
- "⏭️",
343
- "Skip if Unsure",
344
- "You can leave items unrated and come back later.",
345
- ),
346
- createTipCard(
347
- "💾",
348
- "Auto-Saved",
349
- "Your progress is kept while you navigate between steps.",
350
- ),
351
- ),
352
- ),
353
- ),
354
- );
355
- }
356
-
357
- /**
358
- * Create a tip card
359
- * @param {string} icon
360
- * @param {string} title
361
- * @param {string} text
362
- * @returns {HTMLElement}
363
- */
364
- function createTipCard(icon, title, text) {
365
- return div(
366
- { className: "tip-card" },
367
- span({ className: "tip-icon" }, icon),
368
- h4({}, title),
369
- p({}, text),
370
- );
371
- }
372
-
373
- /**
374
- * Render skills assessment step
375
- * @param {Object} step - Step configuration with capability and items
376
- * @param {Object} data - App data
377
- * @returns {HTMLElement}
378
- */
379
- function renderSkillsStep(step, data) {
380
- const { capability, items } = step;
381
- const selectedDiscipline = assessmentState.discipline
382
- ? data.disciplines.find((d) => d.id === assessmentState.discipline)
383
- : null;
384
-
385
- // Determine skill relevance if a discipline is selected
386
- const getSkillRelevance = (skill) => {
387
- if (!selectedDiscipline) return null;
388
- if (selectedDiscipline.coreSkills?.includes(skill.id)) return "primary";
389
- if (selectedDiscipline.supportingSkills?.includes(skill.id))
390
- return "secondary";
391
- if (selectedDiscipline.broadSkills?.includes(skill.id)) return "broad";
392
- return null;
393
- };
394
-
395
- // Sort items: relevant skills first
396
- const sortedItems = [...items].sort((a, b) => {
397
- const relevanceA = getSkillRelevance(a);
398
- const relevanceB = getSkillRelevance(b);
399
- const order = { primary: 0, secondary: 1, broad: 2 };
400
-
401
- if (relevanceA && !relevanceB) return -1;
402
- if (!relevanceA && relevanceB) return 1;
403
- if (relevanceA && relevanceB) {
404
- return (order[relevanceA] ?? 3) - (order[relevanceB] ?? 3);
405
- }
406
- return a.name.localeCompare(b.name);
407
- });
408
-
409
- const assessedCount = items.filter(
410
- (item) => assessmentState.skills[item.id],
411
- ).length;
412
-
413
- return div(
414
- { className: "assessment-step" },
415
- div(
416
- { className: "step-header" },
417
- h2(
418
- {},
419
- span({ className: "step-header-icon" }, step.icon),
420
- ` ${formatCapability(capability, data.capabilities)} Skills`,
421
- ),
422
- span(
423
- { className: "step-progress" },
424
- `${assessedCount}/${items.length} rated`,
425
- ),
426
- ),
427
-
428
- selectedDiscipline &&
429
- div(
430
- { className: "discipline-context" },
431
- span({}, `Showing relevance for: `),
432
- span(
433
- { className: "discipline-name" },
434
- selectedDiscipline.specialization,
435
- ),
436
- ),
437
-
438
- div(
439
- { className: "assessment-items" },
440
- ...sortedItems.map((skill) =>
441
- createSkillAssessmentItem(skill, getSkillRelevance(skill)),
442
- ),
443
- ),
444
- );
445
- }
446
-
447
- /**
448
- * Create a skill assessment item
449
- * @param {Object} skill - Skill data
450
- * @param {string|null} relevance - Skill relevance for selected discipline
451
- * @returns {HTMLElement}
452
- */
453
- function createSkillAssessmentItem(skill, relevance) {
454
- const currentLevel = assessmentState.skills[skill.id];
455
-
456
- return div(
457
- {
458
- className: `assessment-item ${currentLevel ? "assessed" : ""} ${relevance ? `relevance-${relevance}` : ""}`,
459
- },
460
- div(
461
- { className: "assessment-item-header" },
462
- div(
463
- { className: "assessment-item-title" },
464
- a({ href: `#/skill/${skill.id}` }, skill.name),
465
- relevance && createBadge(relevance, relevance),
466
- ),
467
- currentLevel &&
468
- span({ className: "current-level-badge" }, formatLevel(currentLevel)),
469
- ),
470
-
471
- p({ className: "assessment-item-description" }, skill.description),
472
-
473
- div(
474
- { className: "level-selector" },
475
- ...SKILL_PROFICIENCY_ORDER.map((level, index) =>
476
- createLevelButton(skill, level, index, "skill"),
477
- ),
478
- // Clear button
479
- button(
480
- {
481
- className: "level-clear-btn",
482
- title: "Clear selection",
483
- onClick: () => {
484
- delete assessmentState.skills[skill.id];
485
- renderSelfAssessment();
486
- },
487
- },
488
- "✕",
489
- ),
490
- ),
491
- );
492
- }
493
-
494
- /**
495
- * Create a level selection button
496
- * @param {Object} item - Skill or behaviour
497
- * @param {string} level - Level value
498
- * @param {number} index - Level index
499
- * @param {string} type - 'skill' or 'behaviour'
500
- * @returns {HTMLElement}
501
- */
502
- function createLevelButton(item, level, index, type) {
503
- const stateKey = type === "skill" ? "skills" : "behaviours";
504
- const currentLevel = assessmentState[stateKey][item.id];
505
- const isSelected = currentLevel === level;
506
- const proficiencyDescriptions =
507
- type === "skill" ? item.proficiencyDescriptions : item.maturityDescriptions;
508
- const description = proficiencyDescriptions?.[level] || "";
509
-
510
- return button(
511
- {
512
- className: `level-btn level-${index + 1} ${isSelected ? "selected" : ""}`,
513
- title: `${formatLevel(level)}: ${description}`,
514
- onClick: () => {
515
- assessmentState[stateKey][item.id] = level;
516
- renderSelfAssessment();
517
- },
518
- },
519
- span({ className: "level-btn-number" }, String(index + 1)),
520
- span({ className: "level-btn-name" }, formatLevel(level)),
521
- );
522
- }
523
-
524
- /**
525
- * Render behaviours assessment step
526
- * @param {Object} step - Step configuration
527
- * @param {Object} data - App data
528
- * @returns {HTMLElement}
529
- */
530
- function renderBehavioursStep(step, data) {
531
- const { items } = step;
532
- const assessedCount = items.filter(
533
- (item) => assessmentState.behaviours[item.id],
534
- ).length;
535
-
536
- return div(
537
- { className: "assessment-step" },
538
- div(
539
- { className: "step-header" },
540
- h2(
541
- {},
542
- span(
543
- { className: "step-header-icon" },
544
- getConceptEmoji(data.framework, "behaviour"),
545
- ),
546
- " Behaviours",
547
- ),
548
- span(
549
- { className: "step-progress" },
550
- `${assessedCount}/${items.length} rated`,
551
- ),
552
- ),
553
-
554
- p(
555
- { className: "step-intro" },
556
- "Behaviours describe how you approach work—your mindsets and ways of working. " +
557
- "These are equally important as technical skills.",
558
- ),
559
-
560
- div(
561
- { className: "assessment-items" },
562
- ...items.map((behaviour) => createBehaviourAssessmentItem(behaviour)),
563
- ),
564
- );
565
- }
566
-
567
- /**
568
- * Create a behaviour assessment item
569
- * @param {Object} behaviour - Behaviour data
570
- * @returns {HTMLElement}
571
- */
572
- function createBehaviourAssessmentItem(behaviour) {
573
- const currentLevel = assessmentState.behaviours[behaviour.id];
574
-
575
- return div(
576
- { className: `assessment-item ${currentLevel ? "assessed" : ""}` },
577
- div(
578
- { className: "assessment-item-header" },
579
- div(
580
- { className: "assessment-item-title" },
581
- a({ href: `#/behaviour/${behaviour.id}` }, behaviour.name),
582
- ),
583
- currentLevel &&
584
- span({ className: "current-level-badge" }, formatLevel(currentLevel)),
585
- ),
586
-
587
- p({ className: "assessment-item-description" }, behaviour.description),
588
-
589
- div(
590
- { className: "level-selector" },
591
- ...BEHAVIOUR_MATURITY_ORDER.map((level, index) =>
592
- createLevelButton(behaviour, level, index, "behaviour"),
593
- ),
594
- // Clear button
595
- button(
596
- {
597
- className: "level-clear-btn",
598
- title: "Clear selection",
599
- onClick: () => {
600
- delete assessmentState.behaviours[behaviour.id];
601
- renderSelfAssessment();
602
- },
603
- },
604
- "✕",
605
- ),
606
- ),
607
- );
608
- }
609
-
610
- /**
611
- * Render results preview before navigating to full results
612
- * @param {Object} data - App data
613
- * @returns {HTMLElement}
614
- */
615
- function renderResultsPreview(data) {
616
- const progress = calculateProgress(data);
617
- const skillCount = Object.keys(assessmentState.skills).length;
618
- const behaviourCount = Object.keys(assessmentState.behaviours).length;
619
-
620
- if (progress < 20) {
621
- return div(
622
- { className: "assessment-step results-preview" },
623
- div(
624
- { className: "results-incomplete" },
625
- h2({}, "Complete More of the Assessment"),
626
- p(
627
- {},
628
- "Please complete at least 20% of the assessment to see job matches. " +
629
- `You've currently assessed ${skillCount} skills and ${behaviourCount} behaviours.`,
630
- ),
631
- div(
632
- { className: "progress-summary" },
633
- div(
634
- { className: "progress-bar large" },
635
- div({
636
- className: "progress-bar-fill",
637
- style: `width: ${progress}%`,
638
- }),
639
- ),
640
- span({}, `${progress}% complete`),
641
- ),
642
- ),
643
- );
644
- }
645
-
646
- return div(
647
- { className: "assessment-step results-preview" },
648
- div(
649
- { className: "results-ready" },
650
- h2({}, "🎉 Assessment Complete!"),
651
- p({}, "Great work! You're ready to see your job matches."),
652
-
653
- div(
654
- { className: "results-summary" },
655
- div(
656
- { className: "summary-stat" },
657
- span({ className: "summary-value" }, String(skillCount)),
658
- span({ className: "summary-label" }, "Skills Assessed"),
659
- ),
660
- div(
661
- { className: "summary-stat" },
662
- span({ className: "summary-value" }, String(behaviourCount)),
663
- span({ className: "summary-label" }, "Behaviours Assessed"),
664
- ),
665
- div(
666
- { className: "summary-stat" },
667
- span({ className: "summary-value" }, `${progress}%`),
668
- span({ className: "summary-label" }, "Complete"),
669
- ),
670
- ),
671
-
672
- div(
673
- { className: "results-actions" },
674
- button(
675
- {
676
- className: "btn btn-primary btn-lg",
677
- onClick: () => {
678
- // Navigate to results page
679
- window.location.hash = "/self-assessment/results";
680
- },
681
- },
682
- "View My Job Matches →",
683
- ),
684
- ),
685
- ),
686
- );
687
- }
688
-
689
239
  /**
690
240
  * Create navigation buttons for the wizard
691
241
  * @param {Array} steps - Wizard steps