@forwardimpact/pathway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/app/commands/agent.js +430 -0
- package/app/commands/behaviour.js +61 -0
- package/app/commands/command-factory.js +211 -0
- package/app/commands/discipline.js +58 -0
- package/app/commands/driver.js +94 -0
- package/app/commands/grade.js +60 -0
- package/app/commands/index.js +20 -0
- package/app/commands/init.js +67 -0
- package/app/commands/interview.js +68 -0
- package/app/commands/job.js +157 -0
- package/app/commands/progress.js +77 -0
- package/app/commands/questions.js +179 -0
- package/app/commands/serve.js +143 -0
- package/app/commands/site.js +121 -0
- package/app/commands/skill.js +76 -0
- package/app/commands/stage.js +129 -0
- package/app/commands/track.js +70 -0
- package/app/components/action-buttons.js +66 -0
- package/app/components/behaviour-profile.js +53 -0
- package/app/components/builder.js +341 -0
- package/app/components/card.js +98 -0
- package/app/components/checklist.js +145 -0
- package/app/components/comparison-radar.js +237 -0
- package/app/components/detail.js +230 -0
- package/app/components/error-page.js +72 -0
- package/app/components/grid.js +109 -0
- package/app/components/list.js +120 -0
- package/app/components/modifier-table.js +142 -0
- package/app/components/nav.js +64 -0
- package/app/components/progression-table.js +320 -0
- package/app/components/radar-chart.js +102 -0
- package/app/components/skill-matrix.js +97 -0
- package/app/css/base.css +56 -0
- package/app/css/bundles/app.css +40 -0
- package/app/css/bundles/handout.css +43 -0
- package/app/css/bundles/slides.css +40 -0
- package/app/css/components/badges.css +215 -0
- package/app/css/components/buttons.css +101 -0
- package/app/css/components/forms.css +105 -0
- package/app/css/components/layout.css +209 -0
- package/app/css/components/nav.css +166 -0
- package/app/css/components/progress.css +166 -0
- package/app/css/components/states.css +82 -0
- package/app/css/components/surfaces.css +243 -0
- package/app/css/components/tables.css +362 -0
- package/app/css/components/typography.css +122 -0
- package/app/css/components/utilities.css +41 -0
- package/app/css/pages/agent-builder.css +391 -0
- package/app/css/pages/assessment-results.css +453 -0
- package/app/css/pages/detail.css +59 -0
- package/app/css/pages/interview-builder.css +148 -0
- package/app/css/pages/job-builder.css +134 -0
- package/app/css/pages/landing.css +92 -0
- package/app/css/pages/lifecycle.css +118 -0
- package/app/css/pages/progress-builder.css +274 -0
- package/app/css/pages/self-assessment.css +502 -0
- package/app/css/reset.css +50 -0
- package/app/css/tokens.css +153 -0
- package/app/css/views/handout.css +30 -0
- package/app/css/views/print.css +608 -0
- package/app/css/views/slide-animations.css +113 -0
- package/app/css/views/slide-base.css +330 -0
- package/app/css/views/slide-sections.css +597 -0
- package/app/css/views/slide-tables.css +275 -0
- package/app/formatters/agent/dom.js +540 -0
- package/app/formatters/agent/profile.js +133 -0
- package/app/formatters/agent/skill.js +58 -0
- package/app/formatters/behaviour/dom.js +91 -0
- package/app/formatters/behaviour/markdown.js +54 -0
- package/app/formatters/behaviour/shared.js +64 -0
- package/app/formatters/discipline/dom.js +187 -0
- package/app/formatters/discipline/markdown.js +87 -0
- package/app/formatters/discipline/shared.js +131 -0
- package/app/formatters/driver/dom.js +103 -0
- package/app/formatters/driver/shared.js +92 -0
- package/app/formatters/grade/dom.js +208 -0
- package/app/formatters/grade/markdown.js +94 -0
- package/app/formatters/grade/shared.js +86 -0
- package/app/formatters/index.js +50 -0
- package/app/formatters/interview/dom.js +97 -0
- package/app/formatters/interview/markdown.js +66 -0
- package/app/formatters/interview/shared.js +332 -0
- package/app/formatters/job/description.js +176 -0
- package/app/formatters/job/dom.js +411 -0
- package/app/formatters/job/markdown.js +102 -0
- package/app/formatters/progress/dom.js +135 -0
- package/app/formatters/progress/markdown.js +86 -0
- package/app/formatters/progress/shared.js +339 -0
- package/app/formatters/questions/json.js +43 -0
- package/app/formatters/questions/markdown.js +303 -0
- package/app/formatters/questions/shared.js +274 -0
- package/app/formatters/questions/yaml.js +76 -0
- package/app/formatters/shared.js +71 -0
- package/app/formatters/skill/dom.js +168 -0
- package/app/formatters/skill/markdown.js +109 -0
- package/app/formatters/skill/shared.js +125 -0
- package/app/formatters/stage/dom.js +135 -0
- package/app/formatters/stage/index.js +12 -0
- package/app/formatters/stage/shared.js +111 -0
- package/app/formatters/track/dom.js +128 -0
- package/app/formatters/track/markdown.js +105 -0
- package/app/formatters/track/shared.js +181 -0
- package/app/handout-main.js +421 -0
- package/app/handout.html +21 -0
- package/app/index.html +59 -0
- package/app/lib/card-mappers.js +173 -0
- package/app/lib/cli-output.js +270 -0
- package/app/lib/error-boundary.js +70 -0
- package/app/lib/errors.js +49 -0
- package/app/lib/form-controls.js +47 -0
- package/app/lib/job-cache.js +86 -0
- package/app/lib/markdown.js +114 -0
- package/app/lib/radar.js +866 -0
- package/app/lib/reactive.js +77 -0
- package/app/lib/render.js +212 -0
- package/app/lib/router-core.js +160 -0
- package/app/lib/router-pages.js +16 -0
- package/app/lib/router-slides.js +202 -0
- package/app/lib/state.js +148 -0
- package/app/lib/utils.js +14 -0
- package/app/lib/yaml-loader.js +327 -0
- package/app/main.js +213 -0
- package/app/model/agent.js +702 -0
- package/app/model/checklist.js +137 -0
- package/app/model/derivation.js +699 -0
- package/app/model/index-generator.js +71 -0
- package/app/model/interview.js +539 -0
- package/app/model/job.js +222 -0
- package/app/model/levels.js +591 -0
- package/app/model/loader.js +564 -0
- package/app/model/matching.js +858 -0
- package/app/model/modifiers.js +158 -0
- package/app/model/profile.js +266 -0
- package/app/model/progression.js +507 -0
- package/app/model/validation.js +1385 -0
- package/app/pages/agent-builder.js +823 -0
- package/app/pages/assessment-results.js +507 -0
- package/app/pages/behaviour.js +70 -0
- package/app/pages/discipline.js +71 -0
- package/app/pages/driver.js +106 -0
- package/app/pages/grade.js +117 -0
- package/app/pages/interview-builder.js +50 -0
- package/app/pages/interview.js +304 -0
- package/app/pages/job-builder.js +50 -0
- package/app/pages/job.js +58 -0
- package/app/pages/landing.js +305 -0
- package/app/pages/progress-builder.js +58 -0
- package/app/pages/progress.js +495 -0
- package/app/pages/self-assessment.js +729 -0
- package/app/pages/skill.js +113 -0
- package/app/pages/stage.js +231 -0
- package/app/pages/track.js +69 -0
- package/app/slide-main.js +360 -0
- package/app/slides/behaviour.js +38 -0
- package/app/slides/chapter.js +82 -0
- package/app/slides/discipline.js +40 -0
- package/app/slides/driver.js +39 -0
- package/app/slides/grade.js +32 -0
- package/app/slides/index.js +198 -0
- package/app/slides/interview.js +58 -0
- package/app/slides/job.js +55 -0
- package/app/slides/overview.js +126 -0
- package/app/slides/progress.js +83 -0
- package/app/slides/skill.js +40 -0
- package/app/slides/track.js +39 -0
- package/app/slides.html +56 -0
- package/app/types.js +147 -0
- package/bin/pathway.js +489 -0
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
- package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
- package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
- package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
- package/examples/agents/.vscode/settings.json +8 -0
- package/examples/behaviours/_index.yaml +8 -0
- package/examples/behaviours/outcome_ownership.yaml +44 -0
- package/examples/behaviours/polymathic_knowledge.yaml +42 -0
- package/examples/behaviours/precise_communication.yaml +40 -0
- package/examples/behaviours/relentless_curiosity.yaml +38 -0
- package/examples/behaviours/systems_thinking.yaml +41 -0
- package/examples/capabilities/_index.yaml +8 -0
- package/examples/capabilities/business.yaml +251 -0
- package/examples/capabilities/delivery.yaml +352 -0
- package/examples/capabilities/people.yaml +100 -0
- package/examples/capabilities/reliability.yaml +318 -0
- package/examples/capabilities/scale.yaml +394 -0
- package/examples/disciplines/_index.yaml +5 -0
- package/examples/disciplines/data_engineering.yaml +76 -0
- package/examples/disciplines/software_engineering.yaml +76 -0
- package/examples/drivers.yaml +205 -0
- package/examples/framework.yaml +58 -0
- package/examples/grades.yaml +118 -0
- package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
- package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
- package/examples/questions/behaviours/precise_communication.yaml +55 -0
- package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
- package/examples/questions/behaviours/systems_thinking.yaml +53 -0
- package/examples/questions/skills/architecture_design.yaml +54 -0
- package/examples/questions/skills/cloud_platforms.yaml +48 -0
- package/examples/questions/skills/code_quality.yaml +49 -0
- package/examples/questions/skills/data_modeling.yaml +46 -0
- package/examples/questions/skills/devops.yaml +47 -0
- package/examples/questions/skills/full_stack_development.yaml +48 -0
- package/examples/questions/skills/sre_practices.yaml +44 -0
- package/examples/questions/skills/stakeholder_management.yaml +49 -0
- package/examples/questions/skills/team_collaboration.yaml +43 -0
- package/examples/questions/skills/technical_writing.yaml +43 -0
- package/examples/self-assessments.yaml +66 -0
- package/examples/stages.yaml +76 -0
- package/examples/tracks/_index.yaml +6 -0
- package/examples/tracks/manager.yaml +53 -0
- package/examples/tracks/platform.yaml +54 -0
- package/examples/tracks/sre.yaml +58 -0
- package/examples/vscode-settings.yaml +22 -0
- package/package.json +68 -0
|
@@ -0,0 +1,858 @@
|
|
|
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
|
|
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
|
+
for (const track of tracks) {
|
|
366
|
+
for (const grade of grades) {
|
|
367
|
+
// Skip invalid combinations
|
|
368
|
+
if (
|
|
369
|
+
!isValidJobCombination({
|
|
370
|
+
discipline,
|
|
371
|
+
grade,
|
|
372
|
+
track,
|
|
373
|
+
validationRules,
|
|
374
|
+
grades,
|
|
375
|
+
})
|
|
376
|
+
) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const job = deriveJob({
|
|
381
|
+
discipline,
|
|
382
|
+
grade,
|
|
383
|
+
track,
|
|
384
|
+
skills,
|
|
385
|
+
behaviours,
|
|
386
|
+
validationRules,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if (!job) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const analysis = calculateJobMatch(selfAssessment, job);
|
|
394
|
+
matches.push({ job, analysis });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Sort by overall score descending
|
|
400
|
+
matches.sort((a, b) => b.analysis.overallScore - a.analysis.overallScore);
|
|
401
|
+
|
|
402
|
+
// Return top N
|
|
403
|
+
return matches.slice(0, topN);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Estimate the best-fit grade level for a self-assessment
|
|
408
|
+
* Maps the candidate's average skill level to the most appropriate grade
|
|
409
|
+
* @param {Object} params
|
|
410
|
+
* @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
|
|
411
|
+
* @param {import('./levels.js').Grade[]} params.grades - All grades (sorted by level)
|
|
412
|
+
* @param {import('./levels.js').Skill[]} params.skills - All skills
|
|
413
|
+
* @returns {{grade: import('./levels.js').Grade, confidence: number, averageSkillIndex: number}}
|
|
414
|
+
*/
|
|
415
|
+
export function estimateBestFitGrade({ selfAssessment, grades, _skills }) {
|
|
416
|
+
const assessedSkills = Object.entries(selfAssessment.skillLevels || {});
|
|
417
|
+
|
|
418
|
+
if (assessedSkills.length === 0) {
|
|
419
|
+
// No skills assessed - return lowest grade
|
|
420
|
+
const sortedGrades = [...grades].sort(
|
|
421
|
+
(a, b) => a.ordinalRank - b.ordinalRank,
|
|
422
|
+
);
|
|
423
|
+
return {
|
|
424
|
+
grade: sortedGrades[0],
|
|
425
|
+
confidence: 0,
|
|
426
|
+
averageSkillIndex: 0,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Calculate average skill level index
|
|
431
|
+
let totalIndex = 0;
|
|
432
|
+
for (const [, level] of assessedSkills) {
|
|
433
|
+
totalIndex += getSkillLevelIndex(level);
|
|
434
|
+
}
|
|
435
|
+
const averageSkillIndex = totalIndex / assessedSkills.length;
|
|
436
|
+
|
|
437
|
+
// Sort grades by ordinalRank
|
|
438
|
+
const sortedGrades = [...grades].sort(
|
|
439
|
+
(a, b) => a.ordinalRank - b.ordinalRank,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
// Map skill index to grade
|
|
443
|
+
// Skill levels: 0=awareness, 1=foundational, 2=working, 3=practitioner, 4=expert
|
|
444
|
+
// We estimate based on what primary skill level the grade expects
|
|
445
|
+
let bestGrade = sortedGrades[0];
|
|
446
|
+
let minDistance = Infinity;
|
|
447
|
+
|
|
448
|
+
for (const grade of sortedGrades) {
|
|
449
|
+
const primaryLevelIndex = getSkillLevelIndex(
|
|
450
|
+
grade.baseSkillLevels?.primary || "awareness",
|
|
451
|
+
);
|
|
452
|
+
const distance = Math.abs(averageSkillIndex - primaryLevelIndex);
|
|
453
|
+
if (distance < minDistance) {
|
|
454
|
+
minDistance = distance;
|
|
455
|
+
bestGrade = grade;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Confidence is higher when the average skill level closely matches a grade
|
|
460
|
+
// Max confidence when exactly matching, lower when between grades
|
|
461
|
+
const confidence = Math.max(0, 1 - minDistance / 2);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
grade: bestGrade,
|
|
465
|
+
confidence,
|
|
466
|
+
averageSkillIndex,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Find realistic job matches with tier filtering
|
|
472
|
+
* Returns matches grouped by tier, filtered to a realistic range (±1 grade from best fit)
|
|
473
|
+
* @param {Object} params
|
|
474
|
+
* @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
|
|
475
|
+
* @param {import('./levels.js').Discipline[]} params.disciplines - All disciplines
|
|
476
|
+
* @param {import('./levels.js').Grade[]} params.grades - All grades
|
|
477
|
+
* @param {import('./levels.js').Track[]} params.tracks - All tracks
|
|
478
|
+
* @param {import('./levels.js').Skill[]} params.skills - All skills
|
|
479
|
+
* @param {import('./levels.js').Behaviour[]} params.behaviours - All behaviours
|
|
480
|
+
* @param {import('./levels.js').JobValidationRules} [params.validationRules] - Optional validation rules
|
|
481
|
+
* @param {boolean} [params.filterByGrade=true] - Whether to filter to ±1 grade from best fit
|
|
482
|
+
* @param {number} [params.topN=20] - Maximum matches to return
|
|
483
|
+
* @returns {{
|
|
484
|
+
* matches: import('./levels.js').JobMatch[],
|
|
485
|
+
* matchesByTier: Object<number, import('./levels.js').JobMatch[]>,
|
|
486
|
+
* estimatedGrade: {grade: import('./levels.js').Grade, confidence: number},
|
|
487
|
+
* gradeRange: {min: number, max: number}
|
|
488
|
+
* }}
|
|
489
|
+
*/
|
|
490
|
+
export function findRealisticMatches({
|
|
491
|
+
selfAssessment,
|
|
492
|
+
disciplines,
|
|
493
|
+
grades,
|
|
494
|
+
tracks,
|
|
495
|
+
skills,
|
|
496
|
+
behaviours,
|
|
497
|
+
validationRules,
|
|
498
|
+
filterByGrade = true,
|
|
499
|
+
topN = 20,
|
|
500
|
+
}) {
|
|
501
|
+
// Estimate best-fit grade
|
|
502
|
+
const estimatedGrade = estimateBestFitGrade({
|
|
503
|
+
selfAssessment,
|
|
504
|
+
grades,
|
|
505
|
+
skills,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Determine grade range (±1 level)
|
|
509
|
+
const bestFitLevel = estimatedGrade.grade.ordinalRank;
|
|
510
|
+
const gradeRange = {
|
|
511
|
+
min: bestFitLevel - 1,
|
|
512
|
+
max: bestFitLevel + 1,
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// Find all matches
|
|
516
|
+
const allMatches = findMatchingJobs({
|
|
517
|
+
selfAssessment,
|
|
518
|
+
disciplines,
|
|
519
|
+
grades,
|
|
520
|
+
tracks,
|
|
521
|
+
skills,
|
|
522
|
+
behaviours,
|
|
523
|
+
validationRules,
|
|
524
|
+
topN: 100, // Get more than needed for filtering
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Filter by grade range if enabled
|
|
528
|
+
let filteredMatches = allMatches;
|
|
529
|
+
if (filterByGrade) {
|
|
530
|
+
filteredMatches = allMatches.filter(
|
|
531
|
+
(m) =>
|
|
532
|
+
m.job.grade.ordinalRank >= gradeRange.min &&
|
|
533
|
+
m.job.grade.ordinalRank <= gradeRange.max,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Group by tier
|
|
538
|
+
const matchesByTier = {
|
|
539
|
+
1: [],
|
|
540
|
+
2: [],
|
|
541
|
+
3: [],
|
|
542
|
+
4: [],
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
for (const match of filteredMatches) {
|
|
546
|
+
const tierNum = match.analysis.tier.tier;
|
|
547
|
+
matchesByTier[tierNum].push(match);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Sort each tier by grade ordinalRank (descending - more senior first), then by score
|
|
551
|
+
for (const tierNum of Object.keys(matchesByTier)) {
|
|
552
|
+
matchesByTier[tierNum].sort((a, b) => {
|
|
553
|
+
// First sort by grade ordinalRank descending (more senior first)
|
|
554
|
+
const gradeDiff = b.job.grade.ordinalRank - a.job.grade.ordinalRank;
|
|
555
|
+
if (gradeDiff !== 0) return gradeDiff;
|
|
556
|
+
// Then by score descending
|
|
557
|
+
return b.analysis.overallScore - a.analysis.overallScore;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Intelligent filtering: limit lower-level matches when strong matches exist
|
|
562
|
+
// Find the highest grade ordinalRank with a Strong or Good match
|
|
563
|
+
const strongAndGoodMatches = [...matchesByTier[1], ...matchesByTier[2]];
|
|
564
|
+
let highestMatchedLevel = 0;
|
|
565
|
+
for (const match of strongAndGoodMatches) {
|
|
566
|
+
if (match.job.grade.ordinalRank > highestMatchedLevel) {
|
|
567
|
+
highestMatchedLevel = match.job.grade.ordinalRank;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Filter each tier to only show grades within reasonable range of highest match
|
|
572
|
+
// For Strong/Good matches: show up to 2 levels below highest match
|
|
573
|
+
// For Stretch/Aspirational: show only at or above highest match (growth opportunities)
|
|
574
|
+
if (highestMatchedLevel > 0) {
|
|
575
|
+
const minLevelForReady = highestMatchedLevel - 2; // Show some consolidation options
|
|
576
|
+
const minLevelForStretch = highestMatchedLevel; // Stretch roles should be at or above current
|
|
577
|
+
|
|
578
|
+
matchesByTier[1] = matchesByTier[1].filter(
|
|
579
|
+
(m) => m.job.grade.ordinalRank >= minLevelForReady,
|
|
580
|
+
);
|
|
581
|
+
matchesByTier[2] = matchesByTier[2].filter(
|
|
582
|
+
(m) => m.job.grade.ordinalRank >= minLevelForReady,
|
|
583
|
+
);
|
|
584
|
+
matchesByTier[3] = matchesByTier[3].filter(
|
|
585
|
+
(m) => m.job.grade.ordinalRank >= minLevelForStretch,
|
|
586
|
+
);
|
|
587
|
+
matchesByTier[4] = matchesByTier[4].filter(
|
|
588
|
+
(m) => m.job.grade.ordinalRank >= minLevelForStretch,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Combine all filtered matches, sorted by grade (descending) then score
|
|
593
|
+
const allFilteredMatches = [
|
|
594
|
+
...matchesByTier[1],
|
|
595
|
+
...matchesByTier[2],
|
|
596
|
+
...matchesByTier[3],
|
|
597
|
+
...matchesByTier[4],
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
// Return top N overall
|
|
601
|
+
const matches = allFilteredMatches.slice(0, topN);
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
matches,
|
|
605
|
+
matchesByTier,
|
|
606
|
+
estimatedGrade: {
|
|
607
|
+
grade: estimatedGrade.grade,
|
|
608
|
+
confidence: estimatedGrade.confidence,
|
|
609
|
+
},
|
|
610
|
+
gradeRange,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Derive a development path from current self-assessment to a target job
|
|
616
|
+
* @param {Object} params
|
|
617
|
+
* @param {import('./levels.js').SelfAssessment} params.selfAssessment - Current self-assessment
|
|
618
|
+
* @param {import('./levels.js').JobDefinition} params.targetJob - Target job
|
|
619
|
+
* @returns {import('./levels.js').DevelopmentPath}
|
|
620
|
+
*/
|
|
621
|
+
export function deriveDevelopmentPath({ selfAssessment, targetJob }) {
|
|
622
|
+
const items = [];
|
|
623
|
+
|
|
624
|
+
// Analyze skill gaps
|
|
625
|
+
for (const jobSkill of targetJob.skillMatrix) {
|
|
626
|
+
const selfLevel = selfAssessment.skillLevels?.[jobSkill.skillId];
|
|
627
|
+
const selfIndex = selfLevel ? getSkillLevelIndex(selfLevel) : -1;
|
|
628
|
+
const targetIndex = getSkillLevelIndex(jobSkill.level);
|
|
629
|
+
|
|
630
|
+
if (selfIndex < targetIndex) {
|
|
631
|
+
// Calculate priority based on:
|
|
632
|
+
// - Gap size (larger gaps = higher priority)
|
|
633
|
+
// - Skill type (primary > secondary > broad)
|
|
634
|
+
// - AI skills get a boost for "AI-era focus"
|
|
635
|
+
const gapSize = targetIndex - selfIndex;
|
|
636
|
+
const typeMultiplier =
|
|
637
|
+
jobSkill.type === "primary" ? 3 : jobSkill.type === "secondary" ? 2 : 1;
|
|
638
|
+
const aiBoost = jobSkill.capability === "ai" ? 1.5 : 1;
|
|
639
|
+
const priority = gapSize * typeMultiplier * aiBoost;
|
|
640
|
+
|
|
641
|
+
items.push({
|
|
642
|
+
id: jobSkill.skillId,
|
|
643
|
+
name: jobSkill.skillName,
|
|
644
|
+
type: "skill",
|
|
645
|
+
currentLevel: selfLevel || "none",
|
|
646
|
+
targetLevel: jobSkill.level,
|
|
647
|
+
priority,
|
|
648
|
+
rationale:
|
|
649
|
+
jobSkill.type === "primary"
|
|
650
|
+
? "Primary skill for this discipline - essential for the role"
|
|
651
|
+
: jobSkill.type === "secondary"
|
|
652
|
+
? "Secondary skill - important for full effectiveness"
|
|
653
|
+
: "Broad skill - needed for collaboration and context",
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Analyze behaviour gaps
|
|
659
|
+
for (const jobBehaviour of targetJob.behaviourProfile) {
|
|
660
|
+
const selfMaturity =
|
|
661
|
+
selfAssessment.behaviourMaturities?.[jobBehaviour.behaviourId];
|
|
662
|
+
const selfIndex = selfMaturity
|
|
663
|
+
? getBehaviourMaturityIndex(selfMaturity)
|
|
664
|
+
: -1;
|
|
665
|
+
const targetIndex = getBehaviourMaturityIndex(jobBehaviour.maturity);
|
|
666
|
+
|
|
667
|
+
if (selfIndex < targetIndex) {
|
|
668
|
+
// Priority for behaviours considers gap size
|
|
669
|
+
const gapSize = targetIndex - selfIndex;
|
|
670
|
+
const priority = gapSize;
|
|
671
|
+
|
|
672
|
+
items.push({
|
|
673
|
+
id: jobBehaviour.behaviourId,
|
|
674
|
+
name: jobBehaviour.behaviourName,
|
|
675
|
+
type: "behaviour",
|
|
676
|
+
currentLevel: selfMaturity || "none",
|
|
677
|
+
targetLevel: jobBehaviour.maturity,
|
|
678
|
+
priority,
|
|
679
|
+
rationale:
|
|
680
|
+
"Required behaviour - important for professional effectiveness",
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Sort by priority (highest first)
|
|
686
|
+
items.sort((a, b) => b.priority - a.priority);
|
|
687
|
+
|
|
688
|
+
// Calculate readiness score
|
|
689
|
+
const matchAnalysis = calculateJobMatch(selfAssessment, targetJob);
|
|
690
|
+
const estimatedReadiness = matchAnalysis.overallScore;
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
targetJob,
|
|
694
|
+
items,
|
|
695
|
+
estimatedReadiness,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Find the best next step job (one grade level up) based on current assessment
|
|
701
|
+
* @param {Object} params
|
|
702
|
+
* @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
|
|
703
|
+
* @param {import('./levels.js').JobDefinition} params.currentJob - Current job (or best match)
|
|
704
|
+
* @param {import('./levels.js').Discipline[]} params.disciplines - All disciplines
|
|
705
|
+
* @param {import('./levels.js').Grade[]} params.grades - All grades (sorted by level)
|
|
706
|
+
* @param {import('./levels.js').Track[]} params.tracks - All tracks
|
|
707
|
+
* @param {import('./levels.js').Skill[]} params.skills - All skills
|
|
708
|
+
* @param {import('./levels.js').Behaviour[]} params.behaviours - All behaviours
|
|
709
|
+
* @param {import('./levels.js').JobValidationRules} [params.validationRules] - Optional validation rules
|
|
710
|
+
* @returns {import('./levels.js').JobMatch|null} Best next-step job or null if at top
|
|
711
|
+
*/
|
|
712
|
+
export function findNextStepJob({
|
|
713
|
+
selfAssessment,
|
|
714
|
+
currentJob,
|
|
715
|
+
_disciplines,
|
|
716
|
+
grades,
|
|
717
|
+
tracks,
|
|
718
|
+
skills,
|
|
719
|
+
behaviours,
|
|
720
|
+
validationRules,
|
|
721
|
+
}) {
|
|
722
|
+
const currentGradeLevel = currentJob.grade.ordinalRank;
|
|
723
|
+
|
|
724
|
+
// Find next grade level
|
|
725
|
+
const sortedGrades = [...grades].sort(
|
|
726
|
+
(a, b) => a.ordinalRank - b.ordinalRank,
|
|
727
|
+
);
|
|
728
|
+
const nextGrade = sortedGrades.find((g) => g.ordinalRank > currentGradeLevel);
|
|
729
|
+
|
|
730
|
+
if (!nextGrade) {
|
|
731
|
+
return null; // Already at top grade
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Find best match at the next grade level, same discipline preferred
|
|
735
|
+
const candidates = [];
|
|
736
|
+
|
|
737
|
+
for (const track of tracks) {
|
|
738
|
+
// Check same discipline first
|
|
739
|
+
if (
|
|
740
|
+
isValidJobCombination({
|
|
741
|
+
discipline: currentJob.discipline,
|
|
742
|
+
grade: nextGrade,
|
|
743
|
+
track,
|
|
744
|
+
validationRules,
|
|
745
|
+
grades,
|
|
746
|
+
})
|
|
747
|
+
) {
|
|
748
|
+
const job = deriveJob({
|
|
749
|
+
discipline: currentJob.discipline,
|
|
750
|
+
grade: nextGrade,
|
|
751
|
+
track,
|
|
752
|
+
skills,
|
|
753
|
+
behaviours,
|
|
754
|
+
validationRules,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
if (job) {
|
|
758
|
+
const analysis = calculateJobMatch(selfAssessment, job);
|
|
759
|
+
// Boost score for same track
|
|
760
|
+
const trackBonus = track.id === currentJob.track.id ? 0.1 : 0;
|
|
761
|
+
candidates.push({
|
|
762
|
+
job,
|
|
763
|
+
analysis,
|
|
764
|
+
adjustedScore: analysis.overallScore + trackBonus,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (candidates.length === 0) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Sort by adjusted score
|
|
775
|
+
candidates.sort((a, b) => b.adjustedScore - a.adjustedScore);
|
|
776
|
+
|
|
777
|
+
return { job: candidates[0].job, analysis: candidates[0].analysis };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Comprehensive analysis of a candidate's self-assessment
|
|
782
|
+
* @param {Object} params
|
|
783
|
+
* @param {import('./levels.js').SelfAssessment} params.selfAssessment - The self-assessment
|
|
784
|
+
* @param {import('./levels.js').Discipline[]} params.disciplines - All disciplines
|
|
785
|
+
* @param {import('./levels.js').Grade[]} params.grades - All grades
|
|
786
|
+
* @param {import('./levels.js').Track[]} params.tracks - All tracks
|
|
787
|
+
* @param {import('./levels.js').Skill[]} params.skills - All skills
|
|
788
|
+
* @param {import('./levels.js').Behaviour[]} params.behaviours - All behaviours
|
|
789
|
+
* @param {import('./levels.js').JobValidationRules} [params.validationRules] - Optional validation rules
|
|
790
|
+
* @param {number} [params.topN=5] - Number of top job matches to return
|
|
791
|
+
* @returns {Object} Comprehensive analysis
|
|
792
|
+
*/
|
|
793
|
+
export function analyzeCandidate({
|
|
794
|
+
selfAssessment,
|
|
795
|
+
disciplines,
|
|
796
|
+
grades,
|
|
797
|
+
tracks,
|
|
798
|
+
skills,
|
|
799
|
+
behaviours,
|
|
800
|
+
validationRules,
|
|
801
|
+
topN = 5,
|
|
802
|
+
}) {
|
|
803
|
+
// Find best matching jobs
|
|
804
|
+
const matches = findMatchingJobs({
|
|
805
|
+
selfAssessment,
|
|
806
|
+
disciplines,
|
|
807
|
+
grades,
|
|
808
|
+
tracks,
|
|
809
|
+
skills,
|
|
810
|
+
behaviours,
|
|
811
|
+
validationRules,
|
|
812
|
+
topN,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Generate development path for the best match
|
|
816
|
+
const bestMatch = matches[0];
|
|
817
|
+
const developmentPath = bestMatch
|
|
818
|
+
? deriveDevelopmentPath({ selfAssessment, targetJob: bestMatch.job })
|
|
819
|
+
: null;
|
|
820
|
+
|
|
821
|
+
// Calculate overall skill profile
|
|
822
|
+
const skillProfile = {};
|
|
823
|
+
for (const [skillId, level] of Object.entries(
|
|
824
|
+
selfAssessment.skillLevels || {},
|
|
825
|
+
)) {
|
|
826
|
+
const skill = skills.find((s) => s.id === skillId);
|
|
827
|
+
if (skill) {
|
|
828
|
+
skillProfile[skillId] = {
|
|
829
|
+
name: skill.name,
|
|
830
|
+
capability: skill.capability,
|
|
831
|
+
level,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Calculate overall behaviour profile
|
|
837
|
+
const behaviourProfile = {};
|
|
838
|
+
for (const [behaviourId, maturity] of Object.entries(
|
|
839
|
+
selfAssessment.behaviourMaturities || {},
|
|
840
|
+
)) {
|
|
841
|
+
const behaviour = behaviours.find((b) => b.id === behaviourId);
|
|
842
|
+
if (behaviour) {
|
|
843
|
+
behaviourProfile[behaviourId] = {
|
|
844
|
+
name: behaviour.name,
|
|
845
|
+
maturity,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
selfAssessment,
|
|
852
|
+
topMatches: matches,
|
|
853
|
+
bestMatch,
|
|
854
|
+
developmentPath,
|
|
855
|
+
skillProfile,
|
|
856
|
+
behaviourProfile,
|
|
857
|
+
};
|
|
858
|
+
}
|