@forwardimpact/pathway 0.4.0 → 0.6.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 (245) hide show
  1. package/bin/{pathway.js → fit-pathway.js} +65 -153
  2. package/package.json +18 -41
  3. package/{app → src}/commands/agent.js +5 -2
  4. package/{app → src}/commands/behaviour.js +1 -1
  5. package/{app → src}/commands/command-factory.js +2 -2
  6. package/{app → src}/commands/discipline.js +1 -1
  7. package/{app → src}/commands/driver.js +2 -2
  8. package/{app → src}/commands/grade.js +2 -2
  9. package/{app → src}/commands/job.js +3 -3
  10. package/{app → src}/commands/serve.js +26 -4
  11. package/{app → src}/commands/site.js +24 -4
  12. package/{app → src}/commands/skill.js +3 -3
  13. package/{app → src}/commands/stage.js +1 -1
  14. package/{app → src}/commands/track.js +2 -2
  15. package/{app → src}/components/card.js +11 -1
  16. package/{app → src}/components/checklist.js +1 -1
  17. package/src/components/code-display.js +153 -0
  18. package/{app → src}/components/comparison-radar.js +1 -1
  19. package/{app → src}/components/detail.js +1 -1
  20. package/src/components/markdown-textarea.js +153 -0
  21. package/{app → src}/components/skill-matrix.js +1 -1
  22. package/{app → src}/css/bundles/app.css +14 -0
  23. package/{app → src}/css/components/badges.css +15 -8
  24. package/{app → src}/css/components/forms.css +23 -13
  25. package/{app → src}/css/components/surfaces.css +49 -3
  26. package/{app → src}/css/components/typography.css +1 -2
  27. package/{app → src}/css/pages/agent-builder.css +11 -102
  28. package/{app → src}/css/pages/detail.css +11 -1
  29. package/{app → src}/css/tokens.css +3 -0
  30. package/{app → src}/formatters/agent/dom.js +26 -71
  31. package/{app → src}/formatters/agent/profile.js +11 -6
  32. package/{app → src}/formatters/behaviour/dom.js +1 -1
  33. package/{app → src}/formatters/discipline/dom.js +1 -1
  34. package/{app → src}/formatters/driver/dom.js +1 -1
  35. package/{app → src}/formatters/grade/dom.js +7 -7
  36. package/{app → src}/formatters/grade/markdown.js +1 -1
  37. package/{app → src}/formatters/interview/dom.js +1 -1
  38. package/{app → src}/formatters/interview/markdown.js +1 -1
  39. package/{app → src}/formatters/interview/shared.js +3 -3
  40. package/{app → src}/formatters/job/description.js +1 -1
  41. package/{app → src}/formatters/job/dom.js +3 -3
  42. package/{app → src}/formatters/job/markdown.js +1 -1
  43. package/{app → src}/formatters/json-ld.js +1 -1
  44. package/{app → src}/formatters/progress/shared.js +3 -3
  45. package/{app → src}/formatters/skill/dom.js +69 -57
  46. package/{app → src}/formatters/skill/markdown.js +1 -1
  47. package/{app → src}/formatters/skill/shared.js +5 -3
  48. package/{app → src}/formatters/stage/microdata.js +2 -2
  49. package/{app → src}/formatters/stage/shared.js +3 -3
  50. package/{app → src}/formatters/tool/shared.js +6 -0
  51. package/{app → src}/formatters/track/dom.js +1 -1
  52. package/{app → src}/formatters/track/markdown.js +1 -1
  53. package/{app → src}/formatters/track/shared.js +4 -1
  54. package/{app → src}/handout-main.js +16 -12
  55. package/src/handout.html +43 -0
  56. package/{app → src}/index.html +23 -2
  57. package/{app → src}/lib/card-mappers.js +28 -1
  58. package/{app → src}/lib/job-cache.js +1 -1
  59. package/{app → src}/lib/render.js +1 -1
  60. package/{app → src}/pages/agent-builder.js +120 -76
  61. package/{app → src}/pages/assessment-results.js +1 -1
  62. package/{app → src}/pages/interview.js +1 -1
  63. package/{app → src}/pages/job-builder.js +1 -1
  64. package/{app → src}/pages/job.js +1 -1
  65. package/{app → src}/pages/landing.js +5 -2
  66. package/{app → src}/pages/self-assessment.js +1 -1
  67. package/{app → src}/pages/skill.js +1 -1
  68. package/{app → src}/pages/stage.js +5 -5
  69. package/{app → src}/pages/tool.js +1 -1
  70. package/{app → src}/slide-main.js +2 -2
  71. package/{app → src}/slides/chapter.js +8 -8
  72. package/{app → src}/slides/index.js +3 -3
  73. package/{app → src}/slides/job.js +1 -1
  74. package/{app → src}/slides/overview.js +9 -9
  75. package/{app → src}/slides/skill.js +1 -0
  76. package/{app → src}/slides.html +16 -1
  77. package/templates/agent.template.md +44 -13
  78. package/templates/job.template.md +14 -20
  79. package/templates/skill.template.md +20 -23
  80. package/LICENSE +0 -201
  81. package/README.md +0 -104
  82. package/app/components/markdown-textarea.js +0 -132
  83. package/app/handout.html +0 -28
  84. package/app/model/agent.js +0 -738
  85. package/app/model/checklist.js +0 -103
  86. package/app/model/derivation.js +0 -766
  87. package/app/model/index-generator.js +0 -65
  88. package/app/model/interview.js +0 -539
  89. package/app/model/job.js +0 -228
  90. package/app/model/levels.js +0 -601
  91. package/app/model/loader.js +0 -599
  92. package/app/model/matching.js +0 -888
  93. package/app/model/modifiers.js +0 -158
  94. package/app/model/profile.js +0 -259
  95. package/app/model/progression.js +0 -507
  96. package/app/model/schema-validation.js +0 -438
  97. package/app/model/validation.js +0 -2130
  98. package/examples/agents/.claude/skills/architecture-design/SKILL.md +0 -130
  99. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +0 -131
  100. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +0 -108
  101. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +0 -142
  102. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +0 -134
  103. package/examples/agents/.claude/skills/sre-practices/SKILL.md +0 -163
  104. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +0 -164
  105. package/examples/agents/.github/agents/se-platform-code.agent.md +0 -132
  106. package/examples/agents/.github/agents/se-platform-plan.agent.md +0 -131
  107. package/examples/agents/.github/agents/se-platform-review.agent.md +0 -136
  108. package/examples/agents/.vscode/settings.json +0 -8
  109. package/examples/behaviours/_index.yaml +0 -8
  110. package/examples/behaviours/outcome_ownership.yaml +0 -43
  111. package/examples/behaviours/polymathic_knowledge.yaml +0 -41
  112. package/examples/behaviours/precise_communication.yaml +0 -39
  113. package/examples/behaviours/relentless_curiosity.yaml +0 -37
  114. package/examples/behaviours/systems_thinking.yaml +0 -40
  115. package/examples/capabilities/_index.yaml +0 -8
  116. package/examples/capabilities/business.yaml +0 -189
  117. package/examples/capabilities/delivery.yaml +0 -303
  118. package/examples/capabilities/people.yaml +0 -68
  119. package/examples/capabilities/reliability.yaml +0 -412
  120. package/examples/capabilities/scale.yaml +0 -378
  121. package/examples/copilot-setup-steps.yaml +0 -25
  122. package/examples/devcontainer.yaml +0 -21
  123. package/examples/disciplines/_index.yaml +0 -6
  124. package/examples/disciplines/data_engineering.yaml +0 -78
  125. package/examples/disciplines/engineering_management.yaml +0 -63
  126. package/examples/disciplines/software_engineering.yaml +0 -78
  127. package/examples/drivers.yaml +0 -202
  128. package/examples/framework.yaml +0 -69
  129. package/examples/grades.yaml +0 -115
  130. package/examples/questions/behaviours/outcome_ownership.yaml +0 -51
  131. package/examples/questions/behaviours/polymathic_knowledge.yaml +0 -47
  132. package/examples/questions/behaviours/precise_communication.yaml +0 -54
  133. package/examples/questions/behaviours/relentless_curiosity.yaml +0 -50
  134. package/examples/questions/behaviours/systems_thinking.yaml +0 -52
  135. package/examples/questions/skills/architecture_design.yaml +0 -53
  136. package/examples/questions/skills/cloud_platforms.yaml +0 -47
  137. package/examples/questions/skills/code_quality.yaml +0 -48
  138. package/examples/questions/skills/data_modeling.yaml +0 -45
  139. package/examples/questions/skills/devops.yaml +0 -46
  140. package/examples/questions/skills/full_stack_development.yaml +0 -47
  141. package/examples/questions/skills/sre_practices.yaml +0 -43
  142. package/examples/questions/skills/stakeholder_management.yaml +0 -48
  143. package/examples/questions/skills/team_collaboration.yaml +0 -42
  144. package/examples/questions/skills/technical_writing.yaml +0 -42
  145. package/examples/self-assessments.yaml +0 -64
  146. package/examples/stages.yaml +0 -131
  147. package/examples/tracks/_index.yaml +0 -5
  148. package/examples/tracks/platform.yaml +0 -49
  149. package/examples/tracks/sre.yaml +0 -48
  150. package/examples/vscode-settings.yaml +0 -17
  151. /package/{app → src}/commands/index.js +0 -0
  152. /package/{app → src}/commands/init.js +0 -0
  153. /package/{app → src}/commands/interview.js +0 -0
  154. /package/{app → src}/commands/progress.js +0 -0
  155. /package/{app → src}/commands/questions.js +0 -0
  156. /package/{app → src}/commands/tool.js +0 -0
  157. /package/{app → src}/components/action-buttons.js +0 -0
  158. /package/{app → src}/components/behaviour-profile.js +0 -0
  159. /package/{app → src}/components/builder.js +0 -0
  160. /package/{app → src}/components/error-page.js +0 -0
  161. /package/{app → src}/components/grid.js +0 -0
  162. /package/{app → src}/components/list.js +0 -0
  163. /package/{app → src}/components/modifier-table.js +0 -0
  164. /package/{app → src}/components/nav.js +0 -0
  165. /package/{app → src}/components/progression-table.js +0 -0
  166. /package/{app → src}/components/radar-chart.js +0 -0
  167. /package/{app → src}/css/base.css +0 -0
  168. /package/{app → src}/css/bundles/handout.css +0 -0
  169. /package/{app → src}/css/bundles/slides.css +0 -0
  170. /package/{app → src}/css/components/buttons.css +0 -0
  171. /package/{app → src}/css/components/layout.css +0 -0
  172. /package/{app → src}/css/components/nav.css +0 -0
  173. /package/{app → src}/css/components/progress.css +0 -0
  174. /package/{app → src}/css/components/states.css +0 -0
  175. /package/{app → src}/css/components/tables.css +0 -0
  176. /package/{app → src}/css/components/utilities.css +0 -0
  177. /package/{app → src}/css/pages/assessment-results.css +0 -0
  178. /package/{app → src}/css/pages/interview-builder.css +0 -0
  179. /package/{app → src}/css/pages/job-builder.css +0 -0
  180. /package/{app → src}/css/pages/landing.css +0 -0
  181. /package/{app → src}/css/pages/lifecycle.css +0 -0
  182. /package/{app → src}/css/pages/progress-builder.css +0 -0
  183. /package/{app → src}/css/pages/self-assessment.css +0 -0
  184. /package/{app → src}/css/reset.css +0 -0
  185. /package/{app → src}/css/views/handout.css +0 -0
  186. /package/{app → src}/css/views/print.css +0 -0
  187. /package/{app → src}/css/views/slide-animations.css +0 -0
  188. /package/{app → src}/css/views/slide-base.css +0 -0
  189. /package/{app → src}/css/views/slide-sections.css +0 -0
  190. /package/{app → src}/css/views/slide-tables.css +0 -0
  191. /package/{app → src}/formatters/agent/skill.js +0 -0
  192. /package/{app → src}/formatters/behaviour/markdown.js +0 -0
  193. /package/{app → src}/formatters/behaviour/microdata.js +0 -0
  194. /package/{app → src}/formatters/behaviour/shared.js +0 -0
  195. /package/{app → src}/formatters/discipline/markdown.js +0 -0
  196. /package/{app → src}/formatters/discipline/microdata.js +0 -0
  197. /package/{app → src}/formatters/discipline/shared.js +0 -0
  198. /package/{app → src}/formatters/driver/microdata.js +0 -0
  199. /package/{app → src}/formatters/driver/shared.js +0 -0
  200. /package/{app → src}/formatters/grade/microdata.js +0 -0
  201. /package/{app → src}/formatters/grade/shared.js +0 -0
  202. /package/{app → src}/formatters/index.js +0 -0
  203. /package/{app → src}/formatters/microdata-shared.js +0 -0
  204. /package/{app → src}/formatters/progress/dom.js +0 -0
  205. /package/{app → src}/formatters/progress/markdown.js +0 -0
  206. /package/{app → src}/formatters/questions/json.js +0 -0
  207. /package/{app → src}/formatters/questions/markdown.js +0 -0
  208. /package/{app → src}/formatters/questions/shared.js +0 -0
  209. /package/{app → src}/formatters/questions/yaml.js +0 -0
  210. /package/{app → src}/formatters/shared.js +0 -0
  211. /package/{app → src}/formatters/skill/microdata.js +0 -0
  212. /package/{app → src}/formatters/stage/dom.js +0 -0
  213. /package/{app → src}/formatters/stage/index.js +0 -0
  214. /package/{app → src}/formatters/track/microdata.js +0 -0
  215. /package/{app → src}/lib/cli-output.js +0 -0
  216. /package/{app → src}/lib/error-boundary.js +0 -0
  217. /package/{app → src}/lib/errors.js +0 -0
  218. /package/{app → src}/lib/form-controls.js +0 -0
  219. /package/{app → src}/lib/markdown.js +0 -0
  220. /package/{app → src}/lib/radar.js +0 -0
  221. /package/{app → src}/lib/reactive.js +0 -0
  222. /package/{app → src}/lib/router-core.js +0 -0
  223. /package/{app → src}/lib/router-pages.js +0 -0
  224. /package/{app → src}/lib/router-slides.js +0 -0
  225. /package/{app → src}/lib/state.js +0 -0
  226. /package/{app → src}/lib/template-loader.js +0 -0
  227. /package/{app → src}/lib/utils.js +0 -0
  228. /package/{app → src}/lib/yaml-loader.js +0 -0
  229. /package/{app → src}/main.js +0 -0
  230. /package/{app → src}/pages/behaviour.js +0 -0
  231. /package/{app → src}/pages/discipline.js +0 -0
  232. /package/{app → src}/pages/driver.js +0 -0
  233. /package/{app → src}/pages/grade.js +0 -0
  234. /package/{app → src}/pages/interview-builder.js +0 -0
  235. /package/{app → src}/pages/progress-builder.js +0 -0
  236. /package/{app → src}/pages/progress.js +0 -0
  237. /package/{app → src}/pages/track.js +0 -0
  238. /package/{app → src}/slides/behaviour.js +0 -0
  239. /package/{app → src}/slides/discipline.js +0 -0
  240. /package/{app → src}/slides/driver.js +0 -0
  241. /package/{app → src}/slides/grade.js +0 -0
  242. /package/{app → src}/slides/interview.js +0 -0
  243. /package/{app → src}/slides/progress.js +0 -0
  244. /package/{app → src}/slides/track.js +0 -0
  245. /package/{app → src}/types.js +0 -0
@@ -1,888 +0,0 @@
1
- /**
2
- * Engineering Pathway Matching Functions
3
- *
4
- * This module provides pure functions for self-assessment validation,
5
- * job matching, and development path derivation.
6
- */
7
-
8
- import { getSkillLevelIndex, getBehaviourMaturityIndex } from "./levels.js";
9
-
10
- import {
11
- deriveJob,
12
- isValidJobCombination,
13
- isSeniorGrade,
14
- } from "./derivation.js";
15
-
16
- // ============================================================================
17
- // Match Tier Types and Constants
18
- // ============================================================================
19
-
20
- /**
21
- * Match tier identifiers
22
- * @readonly
23
- * @enum {number}
24
- */
25
- export const MatchTier = {
26
- STRONG: 1,
27
- GOOD: 2,
28
- STRETCH: 3,
29
- ASPIRATIONAL: 4,
30
- };
31
-
32
- /**
33
- * Match tier configuration with thresholds and display properties
34
- * @type {Object<number, {label: string, color: string, minScore: number, description: string}>}
35
- */
36
- export const MATCH_TIER_CONFIG = {
37
- [MatchTier.STRONG]: {
38
- label: "Strong Match",
39
- color: "green",
40
- minScore: 0.85,
41
- description: "Ready for this role now",
42
- },
43
- [MatchTier.GOOD]: {
44
- label: "Good Match",
45
- color: "blue",
46
- minScore: 0.7,
47
- description: "Ready within 6-12 months of focused growth",
48
- },
49
- [MatchTier.STRETCH]: {
50
- label: "Stretch Role",
51
- color: "amber",
52
- minScore: 0.55,
53
- description: "Ambitious but achievable with dedicated development",
54
- },
55
- [MatchTier.ASPIRATIONAL]: {
56
- label: "Aspirational",
57
- color: "gray",
58
- minScore: 0,
59
- description: "Long-term career goal requiring significant growth",
60
- },
61
- };
62
-
63
- /**
64
- * @typedef {Object} MatchTierInfo
65
- * @property {number} tier - The tier number (1-4)
66
- * @property {string} label - Human-readable tier label
67
- * @property {string} color - Color for UI display
68
- * @property {string} description - Description of what this tier means
69
- */
70
-
71
- /**
72
- * Classify a match score into a tier
73
- * @param {number} score - Match score from 0 to 1
74
- * @returns {MatchTierInfo} Tier classification
75
- */
76
- export function classifyMatchTier(score) {
77
- if (score >= MATCH_TIER_CONFIG[MatchTier.STRONG].minScore) {
78
- return { tier: MatchTier.STRONG, ...MATCH_TIER_CONFIG[MatchTier.STRONG] };
79
- }
80
- if (score >= MATCH_TIER_CONFIG[MatchTier.GOOD].minScore) {
81
- return { tier: MatchTier.GOOD, ...MATCH_TIER_CONFIG[MatchTier.GOOD] };
82
- }
83
- if (score >= MATCH_TIER_CONFIG[MatchTier.STRETCH].minScore) {
84
- return { tier: MatchTier.STRETCH, ...MATCH_TIER_CONFIG[MatchTier.STRETCH] };
85
- }
86
- return {
87
- tier: MatchTier.ASPIRATIONAL,
88
- ...MATCH_TIER_CONFIG[MatchTier.ASPIRATIONAL],
89
- };
90
- }
91
-
92
- // ============================================================================
93
- // Gap Scoring Constants
94
- // ============================================================================
95
-
96
- /**
97
- * Score values for different gap sizes
98
- * Uses a smooth decay that reflects real-world readiness
99
- * @type {Object<number, number>}
100
- */
101
- export const GAP_SCORES = {
102
- 0: 1.0, // Meets or exceeds
103
- 1: 0.7, // Minor development needed
104
- 2: 0.4, // Significant but achievable gap
105
- 3: 0.15, // Major development required
106
- 4: 0.05, // Aspirational only
107
- };
108
-
109
- /**
110
- * Calculate gap score with smooth decay
111
- * @param {number} gap - The gap size (negative = exceeds, positive = below)
112
- * @returns {number} Score from 0 to 1
113
- */
114
- export function calculateGapScore(gap) {
115
- if (gap <= 0) return GAP_SCORES[0]; // Meets or exceeds
116
- if (gap === 1) return GAP_SCORES[1];
117
- if (gap === 2) return GAP_SCORES[2];
118
- if (gap === 3) return GAP_SCORES[3];
119
- return GAP_SCORES[4]; // 4+ levels below
120
- }
121
-
122
- /**
123
- * Calculate skill match score using smooth decay scoring
124
- * @param {Object<string, string>} selfSkills - Self-assessed skill levels
125
- * @param {import('./levels.js').SkillMatrixEntry[]} jobSkills - Required job skill levels
126
- * @returns {{score: number, gaps: import('./levels.js').MatchGap[]}}
127
- */
128
- function calculateSkillScore(selfSkills, jobSkills) {
129
- if (jobSkills.length === 0) {
130
- return { score: 1, gaps: [] };
131
- }
132
-
133
- let totalScore = 0;
134
- const gaps = [];
135
-
136
- for (const jobSkill of jobSkills) {
137
- const selfLevel = selfSkills[jobSkill.skillId];
138
- const requiredIndex = getSkillLevelIndex(jobSkill.level);
139
-
140
- if (!selfLevel) {
141
- // No self-assessment for this skill - count as gap with max penalty
142
- const gap = requiredIndex + 1;
143
- totalScore += calculateGapScore(gap);
144
- gaps.push({
145
- id: jobSkill.skillId,
146
- name: jobSkill.skillName,
147
- type: "skill",
148
- current: "none",
149
- required: jobSkill.level,
150
- gap,
151
- });
152
- continue;
153
- }
154
-
155
- const selfIndex = getSkillLevelIndex(selfLevel);
156
- const difference = selfIndex - requiredIndex;
157
-
158
- if (difference >= 0) {
159
- // Meets or exceeds requirement
160
- totalScore += 1;
161
- } else {
162
- // Below requirement - use smooth decay scoring
163
- const gap = -difference;
164
- totalScore += calculateGapScore(gap);
165
- gaps.push({
166
- id: jobSkill.skillId,
167
- name: jobSkill.skillName,
168
- type: "skill",
169
- current: selfLevel,
170
- required: jobSkill.level,
171
- gap,
172
- });
173
- }
174
- }
175
-
176
- return {
177
- score: totalScore / jobSkills.length,
178
- gaps,
179
- };
180
- }
181
-
182
- /**
183
- * Calculate behaviour match score using smooth decay scoring
184
- * @param {Object<string, string>} selfBehaviours - Self-assessed behaviour maturities
185
- * @param {import('./levels.js').BehaviourProfileEntry[]} jobBehaviours - Required job behaviour maturities
186
- * @returns {{score: number, gaps: import('./levels.js').MatchGap[]}}
187
- */
188
- function calculateBehaviourScore(selfBehaviours, jobBehaviours) {
189
- if (jobBehaviours.length === 0) {
190
- return { score: 1, gaps: [] };
191
- }
192
-
193
- let totalScore = 0;
194
- const gaps = [];
195
-
196
- for (const jobBehaviour of jobBehaviours) {
197
- const selfMaturity = selfBehaviours[jobBehaviour.behaviourId];
198
- const requiredIndex = getBehaviourMaturityIndex(jobBehaviour.maturity);
199
-
200
- if (!selfMaturity) {
201
- // No self-assessment for this behaviour - count as gap with max penalty
202
- const gap = requiredIndex + 1;
203
- totalScore += calculateGapScore(gap);
204
- gaps.push({
205
- id: jobBehaviour.behaviourId,
206
- name: jobBehaviour.behaviourName,
207
- type: "behaviour",
208
- current: "none",
209
- required: jobBehaviour.maturity,
210
- gap,
211
- });
212
- continue;
213
- }
214
-
215
- const selfIndex = getBehaviourMaturityIndex(selfMaturity);
216
- const difference = selfIndex - requiredIndex;
217
-
218
- if (difference >= 0) {
219
- // Meets or exceeds requirement
220
- totalScore += 1;
221
- } else {
222
- // Below requirement - use smooth decay scoring
223
- const gap = -difference;
224
- totalScore += calculateGapScore(gap);
225
- gaps.push({
226
- id: jobBehaviour.behaviourId,
227
- name: jobBehaviour.behaviourName,
228
- type: "behaviour",
229
- current: selfMaturity,
230
- required: jobBehaviour.maturity,
231
- gap,
232
- });
233
- }
234
- }
235
-
236
- return {
237
- score: totalScore / jobBehaviours.length,
238
- gaps,
239
- };
240
- }
241
-
242
- /**
243
- * Calculate expectations match score for senior roles
244
- * @param {Object} selfExpectations - Self-assessed expectations
245
- * @param {import('./levels.js').GradeExpectations} jobExpectations - Required grade expectations
246
- * @returns {number} Score from 0 to 1
247
- */
248
- function calculateExpectationsScore(selfExpectations, jobExpectations) {
249
- if (!selfExpectations || !jobExpectations) {
250
- return 0;
251
- }
252
-
253
- // Simple text matching - in a real system this would be more sophisticated
254
- const fields = ["scope", "autonomy", "influence"];
255
- let matches = 0;
256
- let total = 0;
257
-
258
- for (const field of fields) {
259
- if (jobExpectations[field]) {
260
- total++;
261
- if (selfExpectations[field]) {
262
- // Basic matching - could be enhanced with semantic similarity
263
- matches++;
264
- }
265
- }
266
- }
267
-
268
- return total > 0 ? matches / total : 0;
269
- }
270
-
271
- /**
272
- * Calculate job match analysis between a self-assessment and a job
273
- * @param {import('./levels.js').SelfAssessment} selfAssessment - The self-assessment
274
- * @param {import('./levels.js').JobDefinition} job - The job definition
275
- * @returns {import('./levels.js').MatchAnalysis}
276
- */
277
- export function calculateJobMatch(selfAssessment, job) {
278
- // Get weights from track or use defaults (track may be null for trackless jobs)
279
- const skillWeight = job.track?.assessmentWeights?.skillWeight ?? 0.5;
280
- const behaviourWeight = job.track?.assessmentWeights?.behaviourWeight ?? 0.5;
281
-
282
- // Calculate skill score
283
- const skillResult = calculateSkillScore(
284
- selfAssessment.skillLevels || {},
285
- job.skillMatrix,
286
- );
287
-
288
- // Calculate behaviour score
289
- const behaviourResult = calculateBehaviourScore(
290
- selfAssessment.behaviourMaturities || {},
291
- job.behaviourProfile,
292
- );
293
-
294
- // Calculate weighted overall score
295
- let overallScore =
296
- skillResult.score * skillWeight + behaviourResult.score * behaviourWeight;
297
-
298
- // For senior roles, add expectations score as a bonus
299
- let expectationsScore = undefined;
300
- if (isSeniorGrade(job.grade)) {
301
- expectationsScore = calculateExpectationsScore(
302
- selfAssessment.expectations,
303
- job.expectations,
304
- );
305
- // Add up to 10% bonus for expectations match
306
- overallScore = overallScore * 0.9 + expectationsScore * 0.1;
307
- }
308
-
309
- // Combine all gaps
310
- const allGaps = [...skillResult.gaps, ...behaviourResult.gaps];
311
-
312
- // Sort gaps by gap size (largest first)
313
- allGaps.sort((a, b) => b.gap - a.gap);
314
-
315
- // Classify match into tier
316
- const tier = classifyMatchTier(overallScore);
317
-
318
- // Identify top priority gaps (top 3 by gap size)
319
- const priorityGaps = allGaps.slice(0, 3);
320
-
321
- const result = {
322
- overallScore,
323
- skillScore: skillResult.score,
324
- behaviourScore: behaviourResult.score,
325
- weightsUsed: { skillWeight, behaviourWeight },
326
- gaps: allGaps,
327
- tier,
328
- priorityGaps,
329
- };
330
-
331
- if (expectationsScore !== undefined) {
332
- result.expectationsScore = expectationsScore;
333
- }
334
-
335
- return result;
336
- }
337
-
338
- /**
339
- * Find matching jobs for a self-assessment
340
- * @param {Object} params
341
- * @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
342
- * @param {import('./levels.js').Discipline[]} params.disciplines - All disciplines
343
- * @param {import('./levels.js').Grade[]} params.grades - All grades
344
- * @param {import('./levels.js').Track[]} params.tracks - All tracks
345
- * @param {import('./levels.js').Skill[]} params.skills - All skills
346
- * @param {import('./levels.js').Behaviour[]} params.behaviours - All behaviours
347
- * @param {import('./levels.js').JobValidationRules} [params.validationRules] - Optional validation rules
348
- * @param {number} [params.topN=10] - Number of top matches to return
349
- * @returns {import('./levels.js').JobMatch[]} Ranked job matches
350
- */
351
- export function findMatchingJobs({
352
- selfAssessment,
353
- disciplines,
354
- grades,
355
- tracks,
356
- skills,
357
- behaviours,
358
- validationRules,
359
- topN = 10,
360
- }) {
361
- const matches = [];
362
-
363
- // Generate all valid job combinations
364
- for (const discipline of disciplines) {
365
- // First generate trackless jobs for each discipline × grade
366
- for (const grade of grades) {
367
- if (
368
- !isValidJobCombination({
369
- discipline,
370
- grade,
371
- track: null,
372
- validationRules,
373
- grades,
374
- })
375
- ) {
376
- continue;
377
- }
378
-
379
- const job = deriveJob({
380
- discipline,
381
- grade,
382
- track: null,
383
- skills,
384
- behaviours,
385
- validationRules,
386
- });
387
-
388
- if (job) {
389
- const analysis = calculateJobMatch(selfAssessment, job);
390
- matches.push({ job, analysis });
391
- }
392
- }
393
-
394
- // Then generate jobs with valid tracks
395
- for (const track of tracks) {
396
- for (const grade of grades) {
397
- // Skip invalid combinations
398
- if (
399
- !isValidJobCombination({
400
- discipline,
401
- grade,
402
- track,
403
- validationRules,
404
- grades,
405
- })
406
- ) {
407
- continue;
408
- }
409
-
410
- const job = deriveJob({
411
- discipline,
412
- grade,
413
- track,
414
- skills,
415
- behaviours,
416
- validationRules,
417
- });
418
-
419
- if (!job) {
420
- continue;
421
- }
422
-
423
- const analysis = calculateJobMatch(selfAssessment, job);
424
- matches.push({ job, analysis });
425
- }
426
- }
427
- }
428
-
429
- // Sort by overall score descending
430
- matches.sort((a, b) => b.analysis.overallScore - a.analysis.overallScore);
431
-
432
- // Return top N
433
- return matches.slice(0, topN);
434
- }
435
-
436
- /**
437
- * Estimate the best-fit grade level for a self-assessment
438
- * Maps the candidate's average skill level to the most appropriate grade
439
- * @param {Object} params
440
- * @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
441
- * @param {import('./levels.js').Grade[]} params.grades - All grades (sorted by level)
442
- * @param {import('./levels.js').Skill[]} params.skills - All skills
443
- * @returns {{grade: import('./levels.js').Grade, confidence: number, averageSkillIndex: number}}
444
- */
445
- export function estimateBestFitGrade({ selfAssessment, grades, _skills }) {
446
- const assessedSkills = Object.entries(selfAssessment.skillLevels || {});
447
-
448
- if (assessedSkills.length === 0) {
449
- // No skills assessed - return lowest grade
450
- const sortedGrades = [...grades].sort(
451
- (a, b) => a.ordinalRank - b.ordinalRank,
452
- );
453
- return {
454
- grade: sortedGrades[0],
455
- confidence: 0,
456
- averageSkillIndex: 0,
457
- };
458
- }
459
-
460
- // Calculate average skill level index
461
- let totalIndex = 0;
462
- for (const [, level] of assessedSkills) {
463
- totalIndex += getSkillLevelIndex(level);
464
- }
465
- const averageSkillIndex = totalIndex / assessedSkills.length;
466
-
467
- // Sort grades by ordinalRank
468
- const sortedGrades = [...grades].sort(
469
- (a, b) => a.ordinalRank - b.ordinalRank,
470
- );
471
-
472
- // Map skill index to grade
473
- // Skill levels: 0=awareness, 1=foundational, 2=working, 3=practitioner, 4=expert
474
- // We estimate based on what primary skill level the grade expects
475
- let bestGrade = sortedGrades[0];
476
- let minDistance = Infinity;
477
-
478
- for (const grade of sortedGrades) {
479
- const primaryLevelIndex = getSkillLevelIndex(
480
- grade.baseSkillLevels?.primary || "awareness",
481
- );
482
- const distance = Math.abs(averageSkillIndex - primaryLevelIndex);
483
- if (distance < minDistance) {
484
- minDistance = distance;
485
- bestGrade = grade;
486
- }
487
- }
488
-
489
- // Confidence is higher when the average skill level closely matches a grade
490
- // Max confidence when exactly matching, lower when between grades
491
- const confidence = Math.max(0, 1 - minDistance / 2);
492
-
493
- return {
494
- grade: bestGrade,
495
- confidence,
496
- averageSkillIndex,
497
- };
498
- }
499
-
500
- /**
501
- * Find realistic job matches with tier filtering
502
- * Returns matches grouped by tier, filtered to a realistic range (±1 grade from best fit)
503
- * @param {Object} params
504
- * @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
505
- * @param {import('./levels.js').Discipline[]} params.disciplines - All disciplines
506
- * @param {import('./levels.js').Grade[]} params.grades - All grades
507
- * @param {import('./levels.js').Track[]} params.tracks - All tracks
508
- * @param {import('./levels.js').Skill[]} params.skills - All skills
509
- * @param {import('./levels.js').Behaviour[]} params.behaviours - All behaviours
510
- * @param {import('./levels.js').JobValidationRules} [params.validationRules] - Optional validation rules
511
- * @param {boolean} [params.filterByGrade=true] - Whether to filter to ±1 grade from best fit
512
- * @param {number} [params.topN=20] - Maximum matches to return
513
- * @returns {{
514
- * matches: import('./levels.js').JobMatch[],
515
- * matchesByTier: Object<number, import('./levels.js').JobMatch[]>,
516
- * estimatedGrade: {grade: import('./levels.js').Grade, confidence: number},
517
- * gradeRange: {min: number, max: number}
518
- * }}
519
- */
520
- export function findRealisticMatches({
521
- selfAssessment,
522
- disciplines,
523
- grades,
524
- tracks,
525
- skills,
526
- behaviours,
527
- validationRules,
528
- filterByGrade = true,
529
- topN = 20,
530
- }) {
531
- // Estimate best-fit grade
532
- const estimatedGrade = estimateBestFitGrade({
533
- selfAssessment,
534
- grades,
535
- skills,
536
- });
537
-
538
- // Determine grade range (±1 level)
539
- const bestFitLevel = estimatedGrade.grade.ordinalRank;
540
- const gradeRange = {
541
- min: bestFitLevel - 1,
542
- max: bestFitLevel + 1,
543
- };
544
-
545
- // Find all matches
546
- const allMatches = findMatchingJobs({
547
- selfAssessment,
548
- disciplines,
549
- grades,
550
- tracks,
551
- skills,
552
- behaviours,
553
- validationRules,
554
- topN: 100, // Get more than needed for filtering
555
- });
556
-
557
- // Filter by grade range if enabled
558
- let filteredMatches = allMatches;
559
- if (filterByGrade) {
560
- filteredMatches = allMatches.filter(
561
- (m) =>
562
- m.job.grade.ordinalRank >= gradeRange.min &&
563
- m.job.grade.ordinalRank <= gradeRange.max,
564
- );
565
- }
566
-
567
- // Group by tier
568
- const matchesByTier = {
569
- 1: [],
570
- 2: [],
571
- 3: [],
572
- 4: [],
573
- };
574
-
575
- for (const match of filteredMatches) {
576
- const tierNum = match.analysis.tier.tier;
577
- matchesByTier[tierNum].push(match);
578
- }
579
-
580
- // Sort each tier by grade ordinalRank (descending - more senior first), then by score
581
- for (const tierNum of Object.keys(matchesByTier)) {
582
- matchesByTier[tierNum].sort((a, b) => {
583
- // First sort by grade ordinalRank descending (more senior first)
584
- const gradeDiff = b.job.grade.ordinalRank - a.job.grade.ordinalRank;
585
- if (gradeDiff !== 0) return gradeDiff;
586
- // Then by score descending
587
- return b.analysis.overallScore - a.analysis.overallScore;
588
- });
589
- }
590
-
591
- // Intelligent filtering: limit lower-level matches when strong matches exist
592
- // Find the highest grade ordinalRank with a Strong or Good match
593
- const strongAndGoodMatches = [...matchesByTier[1], ...matchesByTier[2]];
594
- let highestMatchedLevel = 0;
595
- for (const match of strongAndGoodMatches) {
596
- if (match.job.grade.ordinalRank > highestMatchedLevel) {
597
- highestMatchedLevel = match.job.grade.ordinalRank;
598
- }
599
- }
600
-
601
- // Filter each tier to only show grades within reasonable range of highest match
602
- // For Strong/Good matches: show up to 2 levels below highest match
603
- // For Stretch/Aspirational: show only at or above highest match (growth opportunities)
604
- if (highestMatchedLevel > 0) {
605
- const minLevelForReady = highestMatchedLevel - 2; // Show some consolidation options
606
- const minLevelForStretch = highestMatchedLevel; // Stretch roles should be at or above current
607
-
608
- matchesByTier[1] = matchesByTier[1].filter(
609
- (m) => m.job.grade.ordinalRank >= minLevelForReady,
610
- );
611
- matchesByTier[2] = matchesByTier[2].filter(
612
- (m) => m.job.grade.ordinalRank >= minLevelForReady,
613
- );
614
- matchesByTier[3] = matchesByTier[3].filter(
615
- (m) => m.job.grade.ordinalRank >= minLevelForStretch,
616
- );
617
- matchesByTier[4] = matchesByTier[4].filter(
618
- (m) => m.job.grade.ordinalRank >= minLevelForStretch,
619
- );
620
- }
621
-
622
- // Combine all filtered matches, sorted by grade (descending) then score
623
- const allFilteredMatches = [
624
- ...matchesByTier[1],
625
- ...matchesByTier[2],
626
- ...matchesByTier[3],
627
- ...matchesByTier[4],
628
- ];
629
-
630
- // Return top N overall
631
- const matches = allFilteredMatches.slice(0, topN);
632
-
633
- return {
634
- matches,
635
- matchesByTier,
636
- estimatedGrade: {
637
- grade: estimatedGrade.grade,
638
- confidence: estimatedGrade.confidence,
639
- },
640
- gradeRange,
641
- };
642
- }
643
-
644
- /**
645
- * Derive a development path from current self-assessment to a target job
646
- * @param {Object} params
647
- * @param {import('./levels.js').SelfAssessment} params.selfAssessment - Current self-assessment
648
- * @param {import('./levels.js').JobDefinition} params.targetJob - Target job
649
- * @returns {import('./levels.js').DevelopmentPath}
650
- */
651
- export function deriveDevelopmentPath({ selfAssessment, targetJob }) {
652
- const items = [];
653
-
654
- // Analyze skill gaps
655
- for (const jobSkill of targetJob.skillMatrix) {
656
- const selfLevel = selfAssessment.skillLevels?.[jobSkill.skillId];
657
- const selfIndex = selfLevel ? getSkillLevelIndex(selfLevel) : -1;
658
- const targetIndex = getSkillLevelIndex(jobSkill.level);
659
-
660
- if (selfIndex < targetIndex) {
661
- // Calculate priority based on:
662
- // - Gap size (larger gaps = higher priority)
663
- // - Skill type (primary > secondary > broad)
664
- // - AI skills get a boost for "AI-era focus"
665
- const gapSize = targetIndex - selfIndex;
666
- const typeMultiplier =
667
- jobSkill.type === "primary" ? 3 : jobSkill.type === "secondary" ? 2 : 1;
668
- const aiBoost = jobSkill.capability === "ai" ? 1.5 : 1;
669
- const priority = gapSize * typeMultiplier * aiBoost;
670
-
671
- items.push({
672
- id: jobSkill.skillId,
673
- name: jobSkill.skillName,
674
- type: "skill",
675
- currentLevel: selfLevel || "none",
676
- targetLevel: jobSkill.level,
677
- priority,
678
- rationale:
679
- jobSkill.type === "primary"
680
- ? "Primary skill for this discipline - essential for the role"
681
- : jobSkill.type === "secondary"
682
- ? "Secondary skill - important for full effectiveness"
683
- : "Broad skill - needed for collaboration and context",
684
- });
685
- }
686
- }
687
-
688
- // Analyze behaviour gaps
689
- for (const jobBehaviour of targetJob.behaviourProfile) {
690
- const selfMaturity =
691
- selfAssessment.behaviourMaturities?.[jobBehaviour.behaviourId];
692
- const selfIndex = selfMaturity
693
- ? getBehaviourMaturityIndex(selfMaturity)
694
- : -1;
695
- const targetIndex = getBehaviourMaturityIndex(jobBehaviour.maturity);
696
-
697
- if (selfIndex < targetIndex) {
698
- // Priority for behaviours considers gap size
699
- const gapSize = targetIndex - selfIndex;
700
- const priority = gapSize;
701
-
702
- items.push({
703
- id: jobBehaviour.behaviourId,
704
- name: jobBehaviour.behaviourName,
705
- type: "behaviour",
706
- currentLevel: selfMaturity || "none",
707
- targetLevel: jobBehaviour.maturity,
708
- priority,
709
- rationale:
710
- "Required behaviour - important for professional effectiveness",
711
- });
712
- }
713
- }
714
-
715
- // Sort by priority (highest first)
716
- items.sort((a, b) => b.priority - a.priority);
717
-
718
- // Calculate readiness score
719
- const matchAnalysis = calculateJobMatch(selfAssessment, targetJob);
720
- const estimatedReadiness = matchAnalysis.overallScore;
721
-
722
- return {
723
- targetJob,
724
- items,
725
- estimatedReadiness,
726
- };
727
- }
728
-
729
- /**
730
- * Find the best next step job (one grade level up) based on current assessment
731
- * @param {Object} params
732
- * @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
733
- * @param {import('./levels.js').JobDefinition} params.currentJob - Current job (or best match)
734
- * @param {import('./levels.js').Discipline[]} params.disciplines - All disciplines
735
- * @param {import('./levels.js').Grade[]} params.grades - All grades (sorted by level)
736
- * @param {import('./levels.js').Track[]} params.tracks - All tracks
737
- * @param {import('./levels.js').Skill[]} params.skills - All skills
738
- * @param {import('./levels.js').Behaviour[]} params.behaviours - All behaviours
739
- * @param {import('./levels.js').JobValidationRules} [params.validationRules] - Optional validation rules
740
- * @returns {import('./levels.js').JobMatch|null} Best next-step job or null if at top
741
- */
742
- export function findNextStepJob({
743
- selfAssessment,
744
- currentJob,
745
- _disciplines,
746
- grades,
747
- tracks,
748
- skills,
749
- behaviours,
750
- validationRules,
751
- }) {
752
- const currentGradeLevel = currentJob.grade.ordinalRank;
753
-
754
- // Find next grade level
755
- const sortedGrades = [...grades].sort(
756
- (a, b) => a.ordinalRank - b.ordinalRank,
757
- );
758
- const nextGrade = sortedGrades.find((g) => g.ordinalRank > currentGradeLevel);
759
-
760
- if (!nextGrade) {
761
- return null; // Already at top grade
762
- }
763
-
764
- // Find best match at the next grade level, same discipline preferred
765
- const candidates = [];
766
-
767
- for (const track of tracks) {
768
- // Check same discipline first
769
- if (
770
- isValidJobCombination({
771
- discipline: currentJob.discipline,
772
- grade: nextGrade,
773
- track,
774
- validationRules,
775
- grades,
776
- })
777
- ) {
778
- const job = deriveJob({
779
- discipline: currentJob.discipline,
780
- grade: nextGrade,
781
- track,
782
- skills,
783
- behaviours,
784
- validationRules,
785
- });
786
-
787
- if (job) {
788
- const analysis = calculateJobMatch(selfAssessment, job);
789
- // Boost score for same track
790
- const trackBonus = track.id === currentJob.track.id ? 0.1 : 0;
791
- candidates.push({
792
- job,
793
- analysis,
794
- adjustedScore: analysis.overallScore + trackBonus,
795
- });
796
- }
797
- }
798
- }
799
-
800
- if (candidates.length === 0) {
801
- return null;
802
- }
803
-
804
- // Sort by adjusted score
805
- candidates.sort((a, b) => b.adjustedScore - a.adjustedScore);
806
-
807
- return { job: candidates[0].job, analysis: candidates[0].analysis };
808
- }
809
-
810
- /**
811
- * Comprehensive analysis of a candidate's self-assessment
812
- * @param {Object} params
813
- * @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
814
- * @param {import('./levels.js').Discipline[]} params.disciplines - All disciplines
815
- * @param {import('./levels.js').Grade[]} params.grades - All grades
816
- * @param {import('./levels.js').Track[]} params.tracks - All tracks
817
- * @param {import('./levels.js').Skill[]} params.skills - All skills
818
- * @param {import('./levels.js').Behaviour[]} params.behaviours - All behaviours
819
- * @param {import('./levels.js').JobValidationRules} [params.validationRules] - Optional validation rules
820
- * @param {number} [params.topN=5] - Number of top job matches to return
821
- * @returns {Object} Comprehensive analysis
822
- */
823
- export function analyzeCandidate({
824
- selfAssessment,
825
- disciplines,
826
- grades,
827
- tracks,
828
- skills,
829
- behaviours,
830
- validationRules,
831
- topN = 5,
832
- }) {
833
- // Find best matching jobs
834
- const matches = findMatchingJobs({
835
- selfAssessment,
836
- disciplines,
837
- grades,
838
- tracks,
839
- skills,
840
- behaviours,
841
- validationRules,
842
- topN,
843
- });
844
-
845
- // Generate development path for the best match
846
- const bestMatch = matches[0];
847
- const developmentPath = bestMatch
848
- ? deriveDevelopmentPath({ selfAssessment, targetJob: bestMatch.job })
849
- : null;
850
-
851
- // Calculate overall skill profile
852
- const skillProfile = {};
853
- for (const [skillId, level] of Object.entries(
854
- selfAssessment.skillLevels || {},
855
- )) {
856
- const skill = skills.find((s) => s.id === skillId);
857
- if (skill) {
858
- skillProfile[skillId] = {
859
- name: skill.name,
860
- capability: skill.capability,
861
- level,
862
- };
863
- }
864
- }
865
-
866
- // Calculate overall behaviour profile
867
- const behaviourProfile = {};
868
- for (const [behaviourId, maturity] of Object.entries(
869
- selfAssessment.behaviourMaturities || {},
870
- )) {
871
- const behaviour = behaviours.find((b) => b.id === behaviourId);
872
- if (behaviour) {
873
- behaviourProfile[behaviourId] = {
874
- name: behaviour.name,
875
- maturity,
876
- };
877
- }
878
- }
879
-
880
- return {
881
- selfAssessment,
882
- topMatches: matches,
883
- bestMatch,
884
- developmentPath,
885
- skillProfile,
886
- behaviourProfile,
887
- };
888
- }