@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,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Career Progression Functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides pure functions for calculating skill and behaviour
|
|
5
|
+
* changes between job definitions, supporting both grade progression and
|
|
6
|
+
* track comparison scenarios.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getSkillLevelIndex, getBehaviourMaturityIndex } from "./levels.js";
|
|
10
|
+
import { deriveJob, isValidJobCombination } from "./derivation.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} SkillChange
|
|
14
|
+
* @property {string} id - Skill ID
|
|
15
|
+
* @property {string} name - Skill name
|
|
16
|
+
* @property {string} capability - Skill capability
|
|
17
|
+
* @property {string} type - Skill type (primary/secondary/broad)
|
|
18
|
+
* @property {string|null} currentLevel - Current skill level (null if skill is gained)
|
|
19
|
+
* @property {string|null} targetLevel - Target skill level (null if skill is lost)
|
|
20
|
+
* @property {number} currentIndex - Current level index (0-4, or -1 if not present)
|
|
21
|
+
* @property {number} targetIndex - Target level index (0-4, or -1 if not present)
|
|
22
|
+
* @property {number} change - Difference between target and current index
|
|
23
|
+
* @property {string|null} currentDescription - Description at current level
|
|
24
|
+
* @property {string|null} targetDescription - Description at target level
|
|
25
|
+
* @property {boolean} [isGained] - True if skill is new in target (not in current)
|
|
26
|
+
* @property {boolean} [isLost] - True if skill is removed in target (not in target)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} BehaviourChange
|
|
31
|
+
* @property {string} id - Behaviour ID
|
|
32
|
+
* @property {string} name - Behaviour name
|
|
33
|
+
* @property {string} currentLevel - Current maturity level
|
|
34
|
+
* @property {string} targetLevel - Target maturity level
|
|
35
|
+
* @property {number} currentIndex - Current level index (0-4)
|
|
36
|
+
* @property {number} targetIndex - Target level index (0-4)
|
|
37
|
+
* @property {number} change - Difference between target and current index
|
|
38
|
+
|
|
39
|
+
* @property {string} currentDescription - Description at current level
|
|
40
|
+
* @property {string} targetDescription - Description at target level
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} ProgressionAnalysis
|
|
45
|
+
* @property {Object} current - Current job definition
|
|
46
|
+
* @property {Object} target - Target job definition
|
|
47
|
+
* @property {SkillChange[]} skillChanges - All skill changes
|
|
48
|
+
* @property {BehaviourChange[]} behaviourChanges - All behaviour changes
|
|
49
|
+
* @property {Object} summary - Summary statistics
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate skill level changes between two skill matrices
|
|
54
|
+
* Handles cross-discipline comparisons by including gained and lost skills
|
|
55
|
+
* @param {Array} currentMatrix - Current skill matrix entries
|
|
56
|
+
* @param {Array} targetMatrix - Target skill matrix entries
|
|
57
|
+
* @returns {SkillChange[]} Array of skill changes, sorted by change magnitude
|
|
58
|
+
*/
|
|
59
|
+
export function calculateSkillChanges(currentMatrix, targetMatrix) {
|
|
60
|
+
const changes = [];
|
|
61
|
+
const processedSkillIds = new Set();
|
|
62
|
+
|
|
63
|
+
// Process skills in current matrix
|
|
64
|
+
for (const current of currentMatrix) {
|
|
65
|
+
processedSkillIds.add(current.skillId);
|
|
66
|
+
const target = targetMatrix.find((t) => t.skillId === current.skillId);
|
|
67
|
+
|
|
68
|
+
if (target) {
|
|
69
|
+
// Skill exists in both - calculate level change
|
|
70
|
+
const currentIndex = getSkillLevelIndex(current.level);
|
|
71
|
+
const targetIndex = getSkillLevelIndex(target.level);
|
|
72
|
+
const change = targetIndex - currentIndex;
|
|
73
|
+
|
|
74
|
+
changes.push({
|
|
75
|
+
id: current.skillId,
|
|
76
|
+
name: current.skillName,
|
|
77
|
+
capability: current.capability,
|
|
78
|
+
type: current.type,
|
|
79
|
+
currentLevel: current.level,
|
|
80
|
+
targetLevel: target.level,
|
|
81
|
+
currentIndex,
|
|
82
|
+
targetIndex,
|
|
83
|
+
change,
|
|
84
|
+
currentDescription: current.levelDescription,
|
|
85
|
+
targetDescription: target.levelDescription,
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
// Skill is lost (in current but not in target)
|
|
89
|
+
const currentIndex = getSkillLevelIndex(current.level);
|
|
90
|
+
changes.push({
|
|
91
|
+
id: current.skillId,
|
|
92
|
+
name: current.skillName,
|
|
93
|
+
capability: current.capability,
|
|
94
|
+
type: current.type,
|
|
95
|
+
currentLevel: current.level,
|
|
96
|
+
targetLevel: null,
|
|
97
|
+
currentIndex,
|
|
98
|
+
targetIndex: -1,
|
|
99
|
+
change: -(currentIndex + 1), // Negative change representing loss
|
|
100
|
+
currentDescription: current.levelDescription,
|
|
101
|
+
targetDescription: null,
|
|
102
|
+
isLost: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Process skills only in target matrix (gained skills)
|
|
108
|
+
for (const target of targetMatrix) {
|
|
109
|
+
if (!processedSkillIds.has(target.skillId)) {
|
|
110
|
+
const targetIndex = getSkillLevelIndex(target.level);
|
|
111
|
+
changes.push({
|
|
112
|
+
id: target.skillId,
|
|
113
|
+
name: target.skillName,
|
|
114
|
+
capability: target.capability,
|
|
115
|
+
type: target.type,
|
|
116
|
+
currentLevel: null,
|
|
117
|
+
targetLevel: target.level,
|
|
118
|
+
currentIndex: -1,
|
|
119
|
+
targetIndex,
|
|
120
|
+
change: targetIndex + 1, // Positive change representing gain
|
|
121
|
+
currentDescription: null,
|
|
122
|
+
targetDescription: target.levelDescription,
|
|
123
|
+
isGained: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Sort by change (largest first), then by type, then by name
|
|
129
|
+
const typeOrder = { primary: 0, secondary: 1, broad: 2 };
|
|
130
|
+
changes.sort((a, b) => {
|
|
131
|
+
if (b.change !== a.change) return b.change - a.change;
|
|
132
|
+
if (typeOrder[a.type] !== typeOrder[b.type])
|
|
133
|
+
return typeOrder[a.type] - typeOrder[b.type];
|
|
134
|
+
return a.name.localeCompare(b.name);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return changes;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Calculate behaviour maturity changes between two profiles
|
|
142
|
+
* @param {Array} currentProfile - Current behaviour profile entries
|
|
143
|
+
* @param {Array} targetProfile - Target behaviour profile entries
|
|
144
|
+
* @returns {BehaviourChange[]} Array of behaviour changes, sorted by change magnitude
|
|
145
|
+
*/
|
|
146
|
+
export function calculateBehaviourChanges(currentProfile, targetProfile) {
|
|
147
|
+
const changes = [];
|
|
148
|
+
|
|
149
|
+
for (const current of currentProfile) {
|
|
150
|
+
const target = targetProfile.find(
|
|
151
|
+
(t) => t.behaviourId === current.behaviourId,
|
|
152
|
+
);
|
|
153
|
+
if (target) {
|
|
154
|
+
const currentIndex = getBehaviourMaturityIndex(current.maturity);
|
|
155
|
+
const targetIndex = getBehaviourMaturityIndex(target.maturity);
|
|
156
|
+
const change = targetIndex - currentIndex;
|
|
157
|
+
|
|
158
|
+
changes.push({
|
|
159
|
+
id: current.behaviourId,
|
|
160
|
+
name: current.behaviourName,
|
|
161
|
+
currentLevel: current.maturity,
|
|
162
|
+
targetLevel: target.maturity,
|
|
163
|
+
currentIndex,
|
|
164
|
+
targetIndex,
|
|
165
|
+
change,
|
|
166
|
+
currentDescription: current.maturityDescription,
|
|
167
|
+
targetDescription: target.maturityDescription,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Sort by change (largest first), then by name
|
|
173
|
+
changes.sort((a, b) => {
|
|
174
|
+
if (b.change !== a.change) return b.change - a.change;
|
|
175
|
+
return a.name.localeCompare(b.name);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return changes;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Analyze progression between two job definitions
|
|
183
|
+
* @param {Object} currentJob - Current job definition
|
|
184
|
+
* @param {Object} targetJob - Target job definition
|
|
185
|
+
* @returns {ProgressionAnalysis} Complete progression analysis
|
|
186
|
+
*/
|
|
187
|
+
export function analyzeProgression(currentJob, targetJob) {
|
|
188
|
+
const skillChanges = calculateSkillChanges(
|
|
189
|
+
currentJob.skillMatrix,
|
|
190
|
+
targetJob.skillMatrix,
|
|
191
|
+
);
|
|
192
|
+
const behaviourChanges = calculateBehaviourChanges(
|
|
193
|
+
currentJob.behaviourProfile,
|
|
194
|
+
targetJob.behaviourProfile,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const skillsUp = skillChanges.filter(
|
|
198
|
+
(s) => s.change > 0 && !s.isGained,
|
|
199
|
+
).length;
|
|
200
|
+
const skillsDown = skillChanges.filter(
|
|
201
|
+
(s) => s.change < 0 && !s.isLost,
|
|
202
|
+
).length;
|
|
203
|
+
const skillsSame = skillChanges.filter((s) => s.change === 0).length;
|
|
204
|
+
const skillsGained = skillChanges.filter((s) => s.isGained).length;
|
|
205
|
+
const skillsLost = skillChanges.filter((s) => s.isLost).length;
|
|
206
|
+
|
|
207
|
+
const behavioursUp = behaviourChanges.filter((b) => b.change > 0).length;
|
|
208
|
+
const behavioursDown = behaviourChanges.filter((b) => b.change < 0).length;
|
|
209
|
+
const behavioursSame = behaviourChanges.filter((b) => b.change === 0).length;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
current: currentJob,
|
|
213
|
+
target: targetJob,
|
|
214
|
+
skillChanges,
|
|
215
|
+
behaviourChanges,
|
|
216
|
+
summary: {
|
|
217
|
+
skillsUp,
|
|
218
|
+
skillsDown,
|
|
219
|
+
skillsSame,
|
|
220
|
+
skillsGained,
|
|
221
|
+
skillsLost,
|
|
222
|
+
totalSkillChange: skillChanges.reduce((sum, s) => sum + s.change, 0),
|
|
223
|
+
behavioursUp,
|
|
224
|
+
behavioursDown,
|
|
225
|
+
behavioursSame,
|
|
226
|
+
totalBehaviourChange: behaviourChanges.reduce(
|
|
227
|
+
(sum, b) => sum + b.change,
|
|
228
|
+
0,
|
|
229
|
+
),
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Analyze grade progression for a role
|
|
236
|
+
* @param {Object} params
|
|
237
|
+
* @param {Object} params.discipline - The discipline
|
|
238
|
+
* @param {Object} params.grade - Current grade
|
|
239
|
+
* @param {Object} params.track - The track
|
|
240
|
+
* @param {Object} params.nextGrade - Target grade (optional, will find next if not provided)
|
|
241
|
+
* @param {Array} params.grades - All grades (needed if nextGrade not provided)
|
|
242
|
+
* @param {Array} params.skills - All skills
|
|
243
|
+
* @param {Array} params.behaviours - All behaviours
|
|
244
|
+
* @returns {ProgressionAnalysis|null} Progression analysis or null if no next grade
|
|
245
|
+
*/
|
|
246
|
+
export function analyzeGradeProgression({
|
|
247
|
+
discipline,
|
|
248
|
+
grade,
|
|
249
|
+
track,
|
|
250
|
+
nextGrade,
|
|
251
|
+
grades,
|
|
252
|
+
skills,
|
|
253
|
+
behaviours,
|
|
254
|
+
}) {
|
|
255
|
+
// Find next grade if not provided
|
|
256
|
+
let targetGrade = nextGrade;
|
|
257
|
+
if (!targetGrade && grades) {
|
|
258
|
+
const sortedGrades = [...grades].sort(
|
|
259
|
+
(a, b) => a.ordinalRank - b.ordinalRank,
|
|
260
|
+
);
|
|
261
|
+
const currentIndex = sortedGrades.findIndex((g) => g.id === grade.id);
|
|
262
|
+
targetGrade = sortedGrades[currentIndex + 1];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!targetGrade) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Create job definitions
|
|
270
|
+
const currentJob = deriveJob({
|
|
271
|
+
discipline,
|
|
272
|
+
grade,
|
|
273
|
+
track,
|
|
274
|
+
skills,
|
|
275
|
+
behaviours,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const targetJob = deriveJob({
|
|
279
|
+
discipline,
|
|
280
|
+
grade: targetGrade,
|
|
281
|
+
track,
|
|
282
|
+
skills,
|
|
283
|
+
behaviours,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (!currentJob || !targetJob) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return analyzeProgression(currentJob, targetJob);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Analyze track comparison at the same grade
|
|
295
|
+
* @param {Object} params
|
|
296
|
+
* @param {Object} params.discipline - The discipline
|
|
297
|
+
* @param {Object} params.grade - The grade
|
|
298
|
+
* @param {Object} params.currentTrack - Current track
|
|
299
|
+
* @param {Object} params.targetTrack - Target track to compare
|
|
300
|
+
* @param {Array} params.skills - All skills
|
|
301
|
+
* @param {Array} params.behaviours - All behaviours
|
|
302
|
+
* @param {Array} params.grades - All grades (for validation)
|
|
303
|
+
* @returns {ProgressionAnalysis|null} Progression analysis or null if invalid combination
|
|
304
|
+
*/
|
|
305
|
+
export function analyzeTrackComparison({
|
|
306
|
+
discipline,
|
|
307
|
+
grade,
|
|
308
|
+
currentTrack,
|
|
309
|
+
targetTrack,
|
|
310
|
+
skills,
|
|
311
|
+
behaviours,
|
|
312
|
+
grades,
|
|
313
|
+
}) {
|
|
314
|
+
// Check if target track is valid for this discipline
|
|
315
|
+
if (
|
|
316
|
+
!isValidJobCombination({ discipline, grade, track: targetTrack, grades })
|
|
317
|
+
) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Create job definitions
|
|
322
|
+
const currentJob = deriveJob({
|
|
323
|
+
discipline,
|
|
324
|
+
grade,
|
|
325
|
+
track: currentTrack,
|
|
326
|
+
skills,
|
|
327
|
+
behaviours,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const targetJob = deriveJob({
|
|
331
|
+
discipline,
|
|
332
|
+
grade,
|
|
333
|
+
track: targetTrack,
|
|
334
|
+
skills,
|
|
335
|
+
behaviours,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (!currentJob || !targetJob) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return analyzeProgression(currentJob, targetJob);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get all valid tracks for comparison given a discipline and grade
|
|
347
|
+
* @param {Object} params
|
|
348
|
+
* @param {Object} params.discipline - The discipline
|
|
349
|
+
* @param {Object} params.grade - The grade
|
|
350
|
+
* @param {Object} params.currentTrack - Current track (will be excluded from results)
|
|
351
|
+
* @param {Array} params.tracks - All available tracks
|
|
352
|
+
* @param {Array} params.grades - All grades (for validation)
|
|
353
|
+
* @returns {Array} Valid tracks for comparison
|
|
354
|
+
*/
|
|
355
|
+
export function getValidTracksForComparison({
|
|
356
|
+
discipline,
|
|
357
|
+
grade,
|
|
358
|
+
currentTrack,
|
|
359
|
+
tracks,
|
|
360
|
+
grades,
|
|
361
|
+
}) {
|
|
362
|
+
return tracks.filter(
|
|
363
|
+
(t) =>
|
|
364
|
+
t.id !== currentTrack.id &&
|
|
365
|
+
isValidJobCombination({ discipline, grade, track: t, grades }),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get the next grade in the progression
|
|
371
|
+
* @param {Object} grade - Current grade
|
|
372
|
+
* @param {Array} grades - All grades
|
|
373
|
+
* @returns {Object|null} Next grade or null if at highest
|
|
374
|
+
*/
|
|
375
|
+
export function getNextGrade(grade, grades) {
|
|
376
|
+
const sortedGrades = [...grades].sort(
|
|
377
|
+
(a, b) => a.ordinalRank - b.ordinalRank,
|
|
378
|
+
);
|
|
379
|
+
const currentIndex = sortedGrades.findIndex((g) => g.id === grade.id);
|
|
380
|
+
return sortedGrades[currentIndex + 1] || null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get the previous grade in the progression
|
|
385
|
+
* @param {Object} grade - Current grade
|
|
386
|
+
* @param {Array} grades - All grades
|
|
387
|
+
* @returns {Object|null} Previous grade or null if at lowest
|
|
388
|
+
*/
|
|
389
|
+
export function getPreviousGrade(grade, grades) {
|
|
390
|
+
const sortedGrades = [...grades].sort(
|
|
391
|
+
(a, b) => a.ordinalRank - b.ordinalRank,
|
|
392
|
+
);
|
|
393
|
+
const currentIndex = sortedGrades.findIndex((g) => g.id === grade.id);
|
|
394
|
+
return currentIndex > 0 ? sortedGrades[currentIndex - 1] : null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Analyze custom progression from current role to any target discipline × grade × track combination
|
|
399
|
+
* This is the main abstraction for comparing arbitrary role combinations.
|
|
400
|
+
*
|
|
401
|
+
* @param {Object} params
|
|
402
|
+
* @param {Object} params.discipline - Current discipline
|
|
403
|
+
* @param {Object} params.currentGrade - Current grade
|
|
404
|
+
* @param {Object} params.currentTrack - Current track
|
|
405
|
+
* @param {Object} [params.targetDiscipline] - Target discipline (defaults to current discipline)
|
|
406
|
+
* @param {Object} params.targetGrade - Target grade for comparison
|
|
407
|
+
* @param {Object} params.targetTrack - Target track for comparison
|
|
408
|
+
* @param {Array} params.skills - All skills
|
|
409
|
+
* @param {Array} params.behaviours - All behaviours
|
|
410
|
+
* @param {Array} params.grades - All grades (for validation)
|
|
411
|
+
* @returns {ProgressionAnalysis|null} Progression analysis or null if invalid combination
|
|
412
|
+
*/
|
|
413
|
+
export function analyzeCustomProgression({
|
|
414
|
+
discipline,
|
|
415
|
+
currentGrade,
|
|
416
|
+
currentTrack,
|
|
417
|
+
targetDiscipline,
|
|
418
|
+
targetGrade,
|
|
419
|
+
targetTrack,
|
|
420
|
+
skills,
|
|
421
|
+
behaviours,
|
|
422
|
+
grades,
|
|
423
|
+
}) {
|
|
424
|
+
// Use current discipline if target not specified
|
|
425
|
+
const targetDisc = targetDiscipline || discipline;
|
|
426
|
+
|
|
427
|
+
// Validate target combination is valid
|
|
428
|
+
if (
|
|
429
|
+
!isValidJobCombination({
|
|
430
|
+
discipline: targetDisc,
|
|
431
|
+
grade: targetGrade,
|
|
432
|
+
track: targetTrack,
|
|
433
|
+
grades,
|
|
434
|
+
})
|
|
435
|
+
) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Create current job definition
|
|
440
|
+
const currentJob = deriveJob({
|
|
441
|
+
discipline,
|
|
442
|
+
grade: currentGrade,
|
|
443
|
+
track: currentTrack,
|
|
444
|
+
skills,
|
|
445
|
+
behaviours,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Create target job definition
|
|
449
|
+
const targetJob = deriveJob({
|
|
450
|
+
discipline: targetDisc,
|
|
451
|
+
grade: targetGrade,
|
|
452
|
+
track: targetTrack,
|
|
453
|
+
skills,
|
|
454
|
+
behaviours,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (!currentJob || !targetJob) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return analyzeProgression(currentJob, targetJob);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get all valid grade × track combinations for a discipline
|
|
466
|
+
* Useful for populating dropdowns in the UI
|
|
467
|
+
*
|
|
468
|
+
* @param {Object} params
|
|
469
|
+
* @param {Object} params.discipline - The discipline
|
|
470
|
+
* @param {Array} params.grades - All grades
|
|
471
|
+
* @param {Array} params.tracks - All tracks
|
|
472
|
+
* @param {Object} [params.excludeGrade] - Optional grade to exclude
|
|
473
|
+
* @param {Object} [params.excludeTrack] - Optional track to exclude
|
|
474
|
+
* @returns {Array<{grade: Object, track: Object}>} Valid combinations
|
|
475
|
+
*/
|
|
476
|
+
export function getValidGradeTrackCombinations({
|
|
477
|
+
discipline,
|
|
478
|
+
grades,
|
|
479
|
+
tracks,
|
|
480
|
+
excludeGrade,
|
|
481
|
+
excludeTrack,
|
|
482
|
+
}) {
|
|
483
|
+
const combinations = [];
|
|
484
|
+
|
|
485
|
+
for (const grade of grades) {
|
|
486
|
+
for (const track of tracks) {
|
|
487
|
+
// Skip if this is the excluded combination
|
|
488
|
+
if (excludeGrade?.id === grade.id && excludeTrack?.id === track.id) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (isValidJobCombination({ discipline, grade, track, grades })) {
|
|
493
|
+
combinations.push({ grade, track });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Sort by grade level, then by track name
|
|
499
|
+
combinations.sort((a, b) => {
|
|
500
|
+
if (a.grade.ordinalRank !== b.grade.ordinalRank) {
|
|
501
|
+
return a.grade.ordinalRank - b.grade.ordinalRank;
|
|
502
|
+
}
|
|
503
|
+
return a.track.name.localeCompare(b.track.name);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
return combinations;
|
|
507
|
+
}
|