@forwardimpact/pathway 0.1.0 → 0.2.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/app/commands/agent.js +109 -21
- package/app/commands/command-factory.js +3 -3
- package/app/commands/interview.js +14 -7
- package/app/commands/job.js +43 -29
- package/app/commands/progress.js +14 -7
- package/app/commands/serve.js +5 -0
- package/app/commands/stage.js +0 -10
- package/app/commands/track.js +5 -8
- package/app/components/builder.js +111 -27
- package/app/css/components/surfaces.css +16 -0
- package/app/formatters/agent/profile.js +113 -87
- package/app/formatters/agent/skill.js +64 -31
- package/app/formatters/behaviour/dom.js +3 -0
- package/app/formatters/behaviour/microdata.js +106 -0
- package/app/formatters/discipline/dom.js +28 -1
- package/app/formatters/discipline/microdata.js +117 -0
- package/app/formatters/discipline/shared.js +49 -8
- package/app/formatters/driver/dom.js +3 -0
- package/app/formatters/driver/microdata.js +91 -0
- package/app/formatters/grade/dom.js +3 -0
- package/app/formatters/grade/microdata.js +151 -0
- package/app/formatters/index.js +32 -1
- package/app/formatters/interview/shared.js +13 -8
- package/app/formatters/job/description.js +5 -3
- package/app/formatters/json-ld.js +242 -0
- package/app/formatters/microdata-shared.js +184 -0
- package/app/formatters/progress/shared.js +14 -11
- package/app/formatters/skill/dom.js +3 -0
- package/app/formatters/skill/microdata.js +151 -0
- package/app/formatters/stage/dom.js +3 -18
- package/app/formatters/stage/microdata.js +110 -0
- package/app/formatters/stage/shared.js +0 -27
- package/app/formatters/track/dom.js +5 -30
- package/app/formatters/track/markdown.js +2 -25
- package/app/formatters/track/microdata.js +111 -0
- package/app/formatters/track/shared.js +6 -58
- package/app/handout-main.js +26 -12
- package/app/index.html +11 -0
- package/app/lib/card-mappers.js +17 -12
- package/app/lib/job-cache.js +12 -9
- package/app/lib/template-loader.js +66 -0
- package/app/lib/yaml-loader.js +25 -8
- package/app/main.js +8 -4
- package/app/model/agent.js +158 -130
- package/app/model/checklist.js +57 -91
- package/app/model/derivation.js +135 -68
- package/app/model/index-generator.js +1 -7
- package/app/model/job.js +19 -13
- package/app/model/levels.js +20 -12
- package/app/model/loader.js +41 -17
- package/app/model/matching.js +33 -3
- package/app/model/profile.js +38 -45
- package/app/model/schema-validation.js +438 -0
- package/app/model/validation.js +747 -68
- package/app/pages/agent-builder.js +119 -25
- package/app/pages/assessment-results.js +10 -4
- package/app/pages/discipline.js +36 -6
- package/app/pages/driver.js +9 -47
- package/app/pages/interview-builder.js +3 -1
- package/app/pages/interview.js +15 -4
- package/app/pages/job-builder.js +4 -1
- package/app/pages/job.js +15 -4
- package/app/pages/landing.js +10 -10
- package/app/pages/progress-builder.js +3 -1
- package/app/pages/progress.js +72 -21
- package/app/pages/stage.js +3 -126
- package/app/slide-main.js +45 -17
- package/app/slides/index.js +3 -1
- package/app/slides/overview.js +40 -4
- package/app/slides/progress.js +4 -2
- package/bin/pathway.js +18 -64
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
- package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
- package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
- package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
- package/examples/agents/.vscode/settings.json +1 -1
- package/examples/behaviours/outcome_ownership.yaml +1 -2
- package/examples/behaviours/polymathic_knowledge.yaml +1 -2
- package/examples/behaviours/precise_communication.yaml +1 -2
- package/examples/behaviours/relentless_curiosity.yaml +1 -2
- package/examples/behaviours/systems_thinking.yaml +1 -2
- package/examples/capabilities/business.yaml +80 -142
- package/examples/capabilities/delivery.yaml +155 -219
- package/examples/capabilities/people.yaml +2 -34
- package/examples/capabilities/reliability.yaml +161 -80
- package/examples/capabilities/scale.yaml +234 -252
- package/examples/copilot-setup-steps.yaml +25 -0
- package/examples/devcontainer.yaml +21 -0
- package/examples/disciplines/_index.yaml +1 -0
- package/examples/disciplines/data_engineering.yaml +14 -12
- package/examples/disciplines/engineering_management.yaml +63 -0
- package/examples/disciplines/software_engineering.yaml +14 -12
- package/examples/drivers.yaml +1 -4
- package/examples/framework.yaml +1 -2
- package/examples/grades.yaml +1 -3
- package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
- package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
- package/examples/questions/behaviours/precise_communication.yaml +1 -2
- package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
- package/examples/questions/behaviours/systems_thinking.yaml +1 -2
- package/examples/questions/skills/architecture_design.yaml +1 -2
- package/examples/questions/skills/cloud_platforms.yaml +1 -2
- package/examples/questions/skills/code_quality.yaml +1 -2
- package/examples/questions/skills/data_modeling.yaml +1 -2
- package/examples/questions/skills/devops.yaml +1 -2
- package/examples/questions/skills/full_stack_development.yaml +1 -2
- package/examples/questions/skills/sre_practices.yaml +1 -2
- package/examples/questions/skills/stakeholder_management.yaml +1 -2
- package/examples/questions/skills/team_collaboration.yaml +1 -2
- package/examples/questions/skills/technical_writing.yaml +1 -2
- package/examples/self-assessments.yaml +1 -3
- package/examples/stages.yaml +101 -46
- package/examples/tracks/_index.yaml +0 -1
- package/examples/tracks/platform.yaml +8 -13
- package/examples/tracks/sre.yaml +8 -18
- package/examples/vscode-settings.yaml +2 -7
- package/package.json +9 -3
- package/templates/agent.template.md +65 -0
- package/templates/skill.template.md +28 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
- package/examples/tracks/manager.yaml +0 -53
package/app/model/derivation.js
CHANGED
|
@@ -85,7 +85,7 @@ export function findMaxBaseSkillLevel(grade) {
|
|
|
85
85
|
*
|
|
86
86
|
* @param {Object} params
|
|
87
87
|
* @param {import('./levels.js').Discipline} params.discipline - The discipline
|
|
88
|
-
* @param {import('./levels.js').Track} params.track - The track
|
|
88
|
+
* @param {import('./levels.js').Track} [params.track] - The track (optional)
|
|
89
89
|
* @param {import('./levels.js').Grade} params.grade - The grade
|
|
90
90
|
* @param {string} params.skillId - The skill ID
|
|
91
91
|
* @param {import('./levels.js').Skill[]} params.skills - All available skills (for capability lookup)
|
|
@@ -94,7 +94,7 @@ export function findMaxBaseSkillLevel(grade) {
|
|
|
94
94
|
export function deriveSkillLevel({
|
|
95
95
|
discipline,
|
|
96
96
|
grade,
|
|
97
|
-
track,
|
|
97
|
+
track = null,
|
|
98
98
|
skillId,
|
|
99
99
|
skills,
|
|
100
100
|
}) {
|
|
@@ -107,8 +107,13 @@ export function deriveSkillLevel({
|
|
|
107
107
|
const baseLevel = grade.baseSkillLevels[effectiveType];
|
|
108
108
|
const baseIndex = getSkillLevelIndex(baseLevel);
|
|
109
109
|
|
|
110
|
-
// 3. Apply track modifier via capability lookup
|
|
111
|
-
const
|
|
110
|
+
// 3. Apply track modifier via capability lookup (if track provided)
|
|
111
|
+
const effectiveTrack = track || { skillModifiers: {} };
|
|
112
|
+
const modifier = resolveSkillModifier(
|
|
113
|
+
skillId,
|
|
114
|
+
effectiveTrack.skillModifiers,
|
|
115
|
+
skills,
|
|
116
|
+
);
|
|
112
117
|
|
|
113
118
|
// Track-added skills require a positive modifier to be included
|
|
114
119
|
if (!skillType && modifier <= 0) {
|
|
@@ -133,7 +138,7 @@ export function deriveSkillLevel({
|
|
|
133
138
|
* Derive the behaviour maturity for a specific behaviour given discipline, track, and grade
|
|
134
139
|
* @param {Object} params
|
|
135
140
|
* @param {import('./levels.js').Discipline} params.discipline - The discipline
|
|
136
|
-
* @param {import('./levels.js').Track} params.track - The track
|
|
141
|
+
* @param {import('./levels.js').Track} [params.track] - The track (optional)
|
|
137
142
|
* @param {import('./levels.js').Grade} params.grade - The grade
|
|
138
143
|
* @param {string} params.behaviourId - The behaviour ID
|
|
139
144
|
* @returns {string} The derived maturity level
|
|
@@ -141,7 +146,7 @@ export function deriveSkillLevel({
|
|
|
141
146
|
export function deriveBehaviourMaturity({
|
|
142
147
|
discipline,
|
|
143
148
|
grade,
|
|
144
|
-
track,
|
|
149
|
+
track = null,
|
|
145
150
|
behaviourId,
|
|
146
151
|
}) {
|
|
147
152
|
// 1. Get base maturity from grade
|
|
@@ -150,7 +155,8 @@ export function deriveBehaviourMaturity({
|
|
|
150
155
|
|
|
151
156
|
// 2. Calculate behaviour modifiers (additive from discipline and track)
|
|
152
157
|
const disciplineModifier = discipline.behaviourModifiers?.[behaviourId] ?? 0;
|
|
153
|
-
const
|
|
158
|
+
const effectiveTrack = track || { behaviourModifiers: {} };
|
|
159
|
+
const trackModifier = effectiveTrack.behaviourModifiers?.[behaviourId] ?? 0;
|
|
154
160
|
const totalModifier = disciplineModifier + trackModifier;
|
|
155
161
|
|
|
156
162
|
// 3. Apply modifier and clamp
|
|
@@ -163,12 +169,13 @@ export function deriveBehaviourMaturity({
|
|
|
163
169
|
* @param {Object} params
|
|
164
170
|
* @param {import('./levels.js').Discipline} params.discipline - The discipline
|
|
165
171
|
* @param {import('./levels.js').Grade} params.grade - The grade
|
|
166
|
-
* @param {import('./levels.js').Track} params.track - The track
|
|
172
|
+
* @param {import('./levels.js').Track} [params.track] - The track (optional)
|
|
167
173
|
* @param {import('./levels.js').Skill[]} params.skills - All available skills
|
|
168
174
|
* @returns {import('./levels.js').SkillMatrixEntry[]} Complete skill matrix
|
|
169
175
|
*/
|
|
170
|
-
export function deriveSkillMatrix({ discipline, grade, track, skills }) {
|
|
176
|
+
export function deriveSkillMatrix({ discipline, grade, track = null, skills }) {
|
|
171
177
|
const matrix = [];
|
|
178
|
+
const effectiveTrack = track || { skillModifiers: {} };
|
|
172
179
|
|
|
173
180
|
// Collect all skills for this discipline
|
|
174
181
|
const allDisciplineSkills = new Set([
|
|
@@ -179,7 +186,7 @@ export function deriveSkillMatrix({ discipline, grade, track, skills }) {
|
|
|
179
186
|
|
|
180
187
|
// Collect capabilities with positive track modifiers
|
|
181
188
|
const trackCapabilities = new Set(
|
|
182
|
-
Object.entries(
|
|
189
|
+
Object.entries(effectiveTrack.skillModifiers || {})
|
|
183
190
|
.filter(([_, modifier]) => modifier > 0)
|
|
184
191
|
.map(([capability]) => capability),
|
|
185
192
|
);
|
|
@@ -239,14 +246,14 @@ export function deriveSkillMatrix({ discipline, grade, track, skills }) {
|
|
|
239
246
|
* @param {Object} params
|
|
240
247
|
* @param {import('./levels.js').Discipline} params.discipline - The discipline
|
|
241
248
|
* @param {import('./levels.js').Grade} params.grade - The grade
|
|
242
|
-
* @param {import('./levels.js').Track} params.track - The track
|
|
249
|
+
* @param {import('./levels.js').Track} [params.track] - The track (optional)
|
|
243
250
|
* @param {import('./levels.js').Behaviour[]} params.behaviours - All available behaviours
|
|
244
251
|
* @returns {import('./levels.js').BehaviourProfileEntry[]} Complete behaviour profile
|
|
245
252
|
*/
|
|
246
253
|
export function deriveBehaviourProfile({
|
|
247
254
|
discipline,
|
|
248
255
|
grade,
|
|
249
|
-
track,
|
|
256
|
+
track = null,
|
|
250
257
|
behaviours,
|
|
251
258
|
}) {
|
|
252
259
|
const profile = [];
|
|
@@ -278,7 +285,7 @@ export function deriveBehaviourProfile({
|
|
|
278
285
|
* @param {Object} params
|
|
279
286
|
* @param {import('./levels.js').Discipline} params.discipline - The discipline
|
|
280
287
|
* @param {import('./levels.js').Grade} params.grade - The grade
|
|
281
|
-
* @param {import('./levels.js').Track} params.track - The track
|
|
288
|
+
* @param {import('./levels.js').Track} [params.track] - The track (optional)
|
|
282
289
|
* @param {import('./levels.js').JobValidationRules} [params.validationRules] - Optional validation rules
|
|
283
290
|
* @param {Array<import('./levels.js').Grade>} [params.grades] - Optional array of all grades for minGrade validation
|
|
284
291
|
* @returns {boolean} True if the combination is valid
|
|
@@ -286,18 +293,50 @@ export function deriveBehaviourProfile({
|
|
|
286
293
|
export function isValidJobCombination({
|
|
287
294
|
discipline,
|
|
288
295
|
grade,
|
|
289
|
-
track,
|
|
296
|
+
track = null,
|
|
290
297
|
validationRules,
|
|
291
298
|
grades,
|
|
292
299
|
}) {
|
|
293
|
-
// Check
|
|
294
|
-
if (
|
|
295
|
-
|
|
300
|
+
// 1. Check discipline's minGrade constraint
|
|
301
|
+
if (discipline.minGrade && grades) {
|
|
302
|
+
const minGradeObj = grades.find((g) => g.id === discipline.minGrade);
|
|
303
|
+
if (minGradeObj && grade.ordinalRank < minGradeObj.ordinalRank) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 2. Handle trackless vs tracked jobs based on validTracks
|
|
309
|
+
// validTracks semantics:
|
|
310
|
+
// - null in array means "allow trackless (generalist)"
|
|
311
|
+
// - string values mean "allow this specific track"
|
|
312
|
+
// - empty array = discipline cannot have any jobs
|
|
313
|
+
if (!track) {
|
|
314
|
+
// Trackless job: only valid if null is in validTracks
|
|
315
|
+
// Note: for backwards compatibility, empty array also allows trackless
|
|
316
|
+
const validTracks = discipline.validTracks ?? [];
|
|
317
|
+
if (validTracks.length === 0) {
|
|
318
|
+
// Empty array = allow trackless (legacy behavior)
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
// Check if null is explicitly in the array
|
|
322
|
+
return validTracks.includes(null);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 3. Check discipline's validTracks constraint for tracked jobs
|
|
326
|
+
// Only string entries matter here (null = trackless, not a track ID)
|
|
327
|
+
const validTracks = discipline.validTracks ?? [];
|
|
328
|
+
if (validTracks.length > 0) {
|
|
329
|
+
const trackIds = validTracks.filter((t) => t !== null);
|
|
330
|
+
if (trackIds.length > 0 && !trackIds.includes(track.id)) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
// If validTracks only contains null (no track IDs), reject all tracks
|
|
334
|
+
if (trackIds.length === 0) {
|
|
296
335
|
return false;
|
|
297
336
|
}
|
|
298
337
|
}
|
|
299
338
|
|
|
300
|
-
// Check track's minGrade constraint
|
|
339
|
+
// 4. Check track's minGrade constraint
|
|
301
340
|
if (track.minGrade && grades) {
|
|
302
341
|
const minGradeObj = grades.find((g) => g.id === track.minGrade);
|
|
303
342
|
if (minGradeObj && grade.ordinalRank < minGradeObj.ordinalRank) {
|
|
@@ -305,12 +344,8 @@ export function isValidJobCombination({
|
|
|
305
344
|
}
|
|
306
345
|
}
|
|
307
346
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Check invalid combinations
|
|
313
|
-
if (validationRules.invalidCombinations) {
|
|
347
|
+
// 5. Apply framework-level validation rules
|
|
348
|
+
if (validationRules?.invalidCombinations) {
|
|
314
349
|
for (const combo of validationRules.invalidCombinations) {
|
|
315
350
|
const disciplineMatch =
|
|
316
351
|
!combo.discipline || combo.discipline === discipline.id;
|
|
@@ -323,14 +358,6 @@ export function isValidJobCombination({
|
|
|
323
358
|
}
|
|
324
359
|
}
|
|
325
360
|
|
|
326
|
-
// Check valid tracks by discipline
|
|
327
|
-
if (validationRules.validTracksByDiscipline) {
|
|
328
|
-
const validTracks = validationRules.validTracksByDiscipline[discipline.id];
|
|
329
|
-
if (validTracks && !validTracks.includes(track.id)) {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
361
|
return true;
|
|
335
362
|
}
|
|
336
363
|
|
|
@@ -338,48 +365,59 @@ export function isValidJobCombination({
|
|
|
338
365
|
* Generate a job title from discipline, track, and grade
|
|
339
366
|
*
|
|
340
367
|
* Rules:
|
|
341
|
-
* -
|
|
342
|
-
* -
|
|
343
|
-
* -
|
|
368
|
+
* - Management discipline without track: ${grade.managementTitle}, ${discipline.specialization}
|
|
369
|
+
* - Management discipline with track: ${grade.managementTitle}, ${track.name}
|
|
370
|
+
* - IC discipline with track: ${grade.professionalTitle} ${discipline.roleTitle} - ${track.name}
|
|
371
|
+
* - IC discipline without track: ${grade.professionalTitle} ${discipline.roleTitle}
|
|
344
372
|
*
|
|
345
373
|
* @param {import('./levels.js').Discipline} discipline - The discipline
|
|
346
|
-
* @param {import('./levels.js').Track} track - The track
|
|
347
374
|
* @param {import('./levels.js').Grade} grade - The grade
|
|
375
|
+
* @param {import('./levels.js').Track} [track] - The track (optional)
|
|
348
376
|
* @returns {string} Generated job title
|
|
349
377
|
*/
|
|
350
|
-
export function generateJobTitle(discipline, grade, track) {
|
|
351
|
-
const { roleTitle, specialization } = discipline;
|
|
378
|
+
export function generateJobTitle(discipline, grade, track = null) {
|
|
379
|
+
const { roleTitle, specialization, isManagement } = discipline;
|
|
352
380
|
const { professionalTitle, managementTitle } = grade;
|
|
353
381
|
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
382
|
+
// Management discipline (no track needed)
|
|
383
|
+
if (isManagement && !track) {
|
|
384
|
+
return `${managementTitle}, ${specialization}`;
|
|
385
|
+
}
|
|
357
386
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (track.id == "manager") {
|
|
361
|
-
return `${managementTitle}, ${specialization}`;
|
|
362
|
-
}
|
|
363
|
-
// Other management tracks: "Director, Developer Experience"
|
|
387
|
+
// Management discipline with track
|
|
388
|
+
if (isManagement && track) {
|
|
364
389
|
return `${managementTitle}, ${track.name}`;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// IC discipline with track
|
|
393
|
+
if (track) {
|
|
394
|
+
if (professionalTitle.startsWith("Level")) {
|
|
395
|
+
// Professional track with Level grade: "Software Engineer Level II - Platform"
|
|
396
|
+
return `${roleTitle} ${professionalTitle} - ${track.name}`;
|
|
397
|
+
}
|
|
369
398
|
// Professional track with non-Level grade: "Staff Software Engineer - Platform"
|
|
370
399
|
return `${professionalTitle} ${roleTitle} - ${track.name}`;
|
|
371
400
|
}
|
|
401
|
+
|
|
402
|
+
// IC discipline without track (generalist)
|
|
403
|
+
if (professionalTitle.startsWith("Level")) {
|
|
404
|
+
return `${roleTitle} ${professionalTitle}`;
|
|
405
|
+
}
|
|
406
|
+
return `${professionalTitle} ${roleTitle}`;
|
|
372
407
|
}
|
|
373
408
|
|
|
374
409
|
/**
|
|
375
|
-
* Generate a job ID from discipline,
|
|
410
|
+
* Generate a job ID from discipline, grade, and track
|
|
376
411
|
* @param {import('./levels.js').Discipline} discipline - The discipline
|
|
377
|
-
* @param {import('./levels.js').Track} track - The track
|
|
378
412
|
* @param {import('./levels.js').Grade} grade - The grade
|
|
413
|
+
* @param {import('./levels.js').Track} [track] - The track (optional)
|
|
379
414
|
* @returns {string} Generated job ID
|
|
380
415
|
*/
|
|
381
|
-
function generateJobId(discipline,
|
|
382
|
-
|
|
416
|
+
function generateJobId(discipline, grade, track = null) {
|
|
417
|
+
if (track) {
|
|
418
|
+
return `${discipline.id}_${grade.id}_${track.id}`;
|
|
419
|
+
}
|
|
420
|
+
return `${discipline.id}_${grade.id}`;
|
|
383
421
|
}
|
|
384
422
|
|
|
385
423
|
/**
|
|
@@ -392,23 +430,27 @@ function generateJobId(discipline, track, grade) {
|
|
|
392
430
|
* Capabilities are sorted by their maximum skill level (descending),
|
|
393
431
|
* so Expert-level capabilities appear before Practitioner-level, etc.
|
|
394
432
|
*
|
|
395
|
-
* Uses professionalResponsibilities for professional
|
|
396
|
-
* and managementResponsibilities for management
|
|
433
|
+
* Uses professionalResponsibilities for professional disciplines (isProfessional: true)
|
|
434
|
+
* and managementResponsibilities for management disciplines (isManagement: true).
|
|
397
435
|
*
|
|
398
436
|
* @param {Object} params
|
|
399
437
|
* @param {import('./levels.js').SkillMatrixEntry[]} params.skillMatrix - Derived skill matrix for the job
|
|
400
438
|
* @param {Object[]} params.capabilities - Capability definitions with responsibilities
|
|
401
|
-
* @param {import('./levels.js').
|
|
439
|
+
* @param {import('./levels.js').Discipline} params.discipline - The discipline (determines which responsibilities to use)
|
|
402
440
|
* @returns {Array<{capability: string, capabilityName: string, emoji: string, responsibility: string, level: string}>}
|
|
403
441
|
*/
|
|
404
|
-
export function deriveResponsibilities({
|
|
442
|
+
export function deriveResponsibilities({
|
|
443
|
+
skillMatrix,
|
|
444
|
+
capabilities,
|
|
445
|
+
discipline,
|
|
446
|
+
}) {
|
|
405
447
|
if (!capabilities || capabilities.length === 0) {
|
|
406
448
|
return [];
|
|
407
449
|
}
|
|
408
450
|
|
|
409
|
-
// Determine which responsibility set to use based on
|
|
410
|
-
// Management
|
|
411
|
-
const responsibilityKey =
|
|
451
|
+
// Determine which responsibility set to use based on discipline type
|
|
452
|
+
// Management disciplines use managementResponsibilities, professional disciplines use professionalResponsibilities
|
|
453
|
+
const responsibilityKey = discipline?.isManagement
|
|
412
454
|
? "managementResponsibilities"
|
|
413
455
|
: "professionalResponsibilities";
|
|
414
456
|
|
|
@@ -464,11 +506,11 @@ export function deriveResponsibilities({ skillMatrix, capabilities, track }) {
|
|
|
464
506
|
}
|
|
465
507
|
|
|
466
508
|
/**
|
|
467
|
-
* Create a complete job definition from discipline,
|
|
509
|
+
* Create a complete job definition from discipline, grade, and optional track
|
|
468
510
|
* @param {Object} params
|
|
469
511
|
* @param {import('./levels.js').Discipline} params.discipline - The discipline
|
|
470
512
|
* @param {import('./levels.js').Grade} params.grade - The grade
|
|
471
|
-
* @param {import('./levels.js').Track} params.track - The track
|
|
513
|
+
* @param {import('./levels.js').Track} [params.track] - The track (optional)
|
|
472
514
|
* @param {import('./levels.js').Skill[]} params.skills - All available skills
|
|
473
515
|
* @param {import('./levels.js').Behaviour[]} params.behaviours - All available behaviours
|
|
474
516
|
* @param {Object[]} [params.capabilities] - Optional capabilities for responsibility derivation
|
|
@@ -478,7 +520,7 @@ export function deriveResponsibilities({ skillMatrix, capabilities, track }) {
|
|
|
478
520
|
export function deriveJob({
|
|
479
521
|
discipline,
|
|
480
522
|
grade,
|
|
481
|
-
track,
|
|
523
|
+
track = null,
|
|
482
524
|
skills,
|
|
483
525
|
behaviours,
|
|
484
526
|
capabilities,
|
|
@@ -511,7 +553,7 @@ export function deriveJob({
|
|
|
511
553
|
derivedResponsibilities = deriveResponsibilities({
|
|
512
554
|
skillMatrix,
|
|
513
555
|
capabilities,
|
|
514
|
-
|
|
556
|
+
discipline,
|
|
515
557
|
});
|
|
516
558
|
}
|
|
517
559
|
|
|
@@ -645,6 +687,7 @@ export function isSeniorGrade(grade) {
|
|
|
645
687
|
|
|
646
688
|
/**
|
|
647
689
|
* Generate all valid job definitions from the data
|
|
690
|
+
* Generates both trackless jobs and jobs with tracks based on discipline.validTracks
|
|
648
691
|
* @param {Object} params
|
|
649
692
|
* @param {import('./levels.js').Discipline[]} params.disciplines - All disciplines
|
|
650
693
|
* @param {import('./levels.js').Grade[]} params.grades - All grades
|
|
@@ -665,8 +708,32 @@ export function generateAllJobs({
|
|
|
665
708
|
const jobs = [];
|
|
666
709
|
|
|
667
710
|
for (const discipline of disciplines) {
|
|
668
|
-
for (const
|
|
669
|
-
|
|
711
|
+
for (const grade of grades) {
|
|
712
|
+
// First, generate trackless job for this discipline/grade
|
|
713
|
+
if (
|
|
714
|
+
isValidJobCombination({
|
|
715
|
+
discipline,
|
|
716
|
+
grade,
|
|
717
|
+
track: null,
|
|
718
|
+
validationRules,
|
|
719
|
+
grades,
|
|
720
|
+
})
|
|
721
|
+
) {
|
|
722
|
+
const tracklessJob = deriveJob({
|
|
723
|
+
discipline,
|
|
724
|
+
grade,
|
|
725
|
+
track: null,
|
|
726
|
+
skills,
|
|
727
|
+
behaviours,
|
|
728
|
+
validationRules,
|
|
729
|
+
});
|
|
730
|
+
if (tracklessJob) {
|
|
731
|
+
jobs.push(tracklessJob);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Then, generate jobs with valid tracks
|
|
736
|
+
for (const track of tracks) {
|
|
670
737
|
if (
|
|
671
738
|
!isValidJobCombination({
|
|
672
739
|
discipline,
|
|
@@ -47,13 +47,7 @@ ${content}`;
|
|
|
47
47
|
* @returns {Promise<Object>} Summary of generated indexes
|
|
48
48
|
*/
|
|
49
49
|
export async function generateAllIndexes(dataDir) {
|
|
50
|
-
const directories = [
|
|
51
|
-
"skills",
|
|
52
|
-
"behaviours",
|
|
53
|
-
"disciplines",
|
|
54
|
-
"tracks",
|
|
55
|
-
"capabilities",
|
|
56
|
-
];
|
|
50
|
+
const directories = ["behaviours", "disciplines", "tracks", "capabilities"];
|
|
57
51
|
|
|
58
52
|
const results = {};
|
|
59
53
|
|
package/app/model/job.js
CHANGED
|
@@ -52,7 +52,8 @@ export function prepareJobDetail({
|
|
|
52
52
|
drivers,
|
|
53
53
|
capabilities,
|
|
54
54
|
}) {
|
|
55
|
-
|
|
55
|
+
// Track is optional (null = generalist)
|
|
56
|
+
if (!discipline || !grade) return null;
|
|
56
57
|
|
|
57
58
|
const job = getOrCreateJob({
|
|
58
59
|
discipline,
|
|
@@ -70,14 +71,15 @@ export function prepareJobDetail({
|
|
|
70
71
|
drivers,
|
|
71
72
|
});
|
|
72
73
|
|
|
73
|
-
// Derive checklists for each
|
|
74
|
+
// Derive checklists for each stage
|
|
74
75
|
const checklists = {};
|
|
75
76
|
if (capabilities) {
|
|
76
|
-
const
|
|
77
|
-
for (const
|
|
78
|
-
checklists[
|
|
79
|
-
|
|
77
|
+
const stageIds = ["plan", "code"];
|
|
78
|
+
for (const stageId of stageIds) {
|
|
79
|
+
checklists[stageId] = deriveChecklist({
|
|
80
|
+
stageId,
|
|
80
81
|
skillMatrix: job.skillMatrix,
|
|
82
|
+
skills,
|
|
81
83
|
capabilities,
|
|
82
84
|
});
|
|
83
85
|
}
|
|
@@ -89,8 +91,8 @@ export function prepareJobDetail({
|
|
|
89
91
|
disciplineName: discipline.specialization || discipline.name,
|
|
90
92
|
gradeId: grade.id,
|
|
91
93
|
gradeName: grade.professionalTitle || grade.id,
|
|
92
|
-
trackId: track
|
|
93
|
-
trackName: track
|
|
94
|
+
trackId: track?.id || null,
|
|
95
|
+
trackName: track?.name || null,
|
|
94
96
|
expectations: job.expectations || {},
|
|
95
97
|
// Raw model data for components that need the original shape
|
|
96
98
|
skillMatrix: job.skillMatrix,
|
|
@@ -130,7 +132,7 @@ export function prepareJobSummary({
|
|
|
130
132
|
skills,
|
|
131
133
|
behaviours,
|
|
132
134
|
}) {
|
|
133
|
-
if (!discipline || !grade
|
|
135
|
+
if (!discipline || !grade) return null;
|
|
134
136
|
|
|
135
137
|
const job = getOrCreateJob({
|
|
136
138
|
discipline,
|
|
@@ -147,8 +149,8 @@ export function prepareJobSummary({
|
|
|
147
149
|
disciplineId: discipline.id,
|
|
148
150
|
disciplineName: discipline.specialization || discipline.name,
|
|
149
151
|
gradeId: grade.id,
|
|
150
|
-
trackId: track
|
|
151
|
-
trackName: track
|
|
152
|
+
trackId: track?.id || null,
|
|
153
|
+
trackName: track?.name || null,
|
|
152
154
|
skillCount: job.skillMatrix.length,
|
|
153
155
|
behaviourCount: job.behaviourProfile.length,
|
|
154
156
|
primarySkillCount: job.skillMatrix.filter((s) => s.type === "primary")
|
|
@@ -182,7 +184,8 @@ export function prepareJobBuilderPreview({
|
|
|
182
184
|
behaviourCount,
|
|
183
185
|
grades,
|
|
184
186
|
}) {
|
|
185
|
-
|
|
187
|
+
// Track is optional (null = generalist)
|
|
188
|
+
if (!discipline || !grade) {
|
|
186
189
|
return {
|
|
187
190
|
isValid: false,
|
|
188
191
|
title: null,
|
|
@@ -200,12 +203,15 @@ export function prepareJobBuilderPreview({
|
|
|
200
203
|
});
|
|
201
204
|
|
|
202
205
|
if (!validCombination) {
|
|
206
|
+
const reason = track
|
|
207
|
+
? `The ${track.name} track is not available for ${discipline.specialization}.`
|
|
208
|
+
: `${discipline.specialization} requires a track specialization.`;
|
|
203
209
|
return {
|
|
204
210
|
isValid: false,
|
|
205
211
|
title: null,
|
|
206
212
|
totalSkills: 0,
|
|
207
213
|
totalBehaviours: 0,
|
|
208
|
-
invalidReason:
|
|
214
|
+
invalidReason: reason,
|
|
209
215
|
};
|
|
210
216
|
}
|
|
211
217
|
|
package/app/model/levels.js
CHANGED
|
@@ -60,16 +60,24 @@ export const BEHAVIOUR_MATURITY_ORDER = [
|
|
|
60
60
|
* @enum {string}
|
|
61
61
|
*/
|
|
62
62
|
export const Stage = {
|
|
63
|
+
SPECIFY: "specify",
|
|
63
64
|
PLAN: "plan",
|
|
64
65
|
CODE: "code",
|
|
65
66
|
REVIEW: "review",
|
|
67
|
+
DEPLOY: "deploy",
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* Ordered array of stages for lifecycle progression
|
|
70
72
|
* @type {string[]}
|
|
71
73
|
*/
|
|
72
|
-
export const STAGE_ORDER = [
|
|
74
|
+
export const STAGE_ORDER = [
|
|
75
|
+
Stage.SPECIFY,
|
|
76
|
+
Stage.PLAN,
|
|
77
|
+
Stage.CODE,
|
|
78
|
+
Stage.REVIEW,
|
|
79
|
+
Stage.DEPLOY,
|
|
80
|
+
];
|
|
73
81
|
|
|
74
82
|
/**
|
|
75
83
|
* Skill capabilities (what capability area)
|
|
@@ -86,6 +94,7 @@ export const Capability = {
|
|
|
86
94
|
BUSINESS: "business",
|
|
87
95
|
PEOPLE: "people",
|
|
88
96
|
DOCUMENTATION: "documentation",
|
|
97
|
+
PRODUCT: "product",
|
|
89
98
|
};
|
|
90
99
|
|
|
91
100
|
/**
|
|
@@ -95,7 +104,7 @@ export const Capability = {
|
|
|
95
104
|
* 2. Data & AI capabilities
|
|
96
105
|
* 3. Scale & reliability
|
|
97
106
|
* 4. People & process
|
|
98
|
-
* 5. Business &
|
|
107
|
+
* 5. Business, documentation & product
|
|
99
108
|
* @type {string[]}
|
|
100
109
|
*/
|
|
101
110
|
export const CAPABILITY_ORDER = [
|
|
@@ -108,6 +117,7 @@ export const CAPABILITY_ORDER = [
|
|
|
108
117
|
Capability.PROCESS,
|
|
109
118
|
Capability.BUSINESS,
|
|
110
119
|
Capability.DOCUMENTATION,
|
|
120
|
+
Capability.PRODUCT,
|
|
111
121
|
];
|
|
112
122
|
|
|
113
123
|
/**
|
|
@@ -214,24 +224,24 @@ export function getCapabilityEmoji(capabilities, capabilityId) {
|
|
|
214
224
|
/**
|
|
215
225
|
* Get responsibility statement for a capability at a specific skill level
|
|
216
226
|
*
|
|
217
|
-
* Uses professionalResponsibilities for professional
|
|
218
|
-
* managementResponsibilities for management
|
|
227
|
+
* Uses professionalResponsibilities for professional disciplines and
|
|
228
|
+
* managementResponsibilities for management disciplines.
|
|
219
229
|
*
|
|
220
230
|
* @param {Object[]} capabilities - Loaded capabilities array
|
|
221
231
|
* @param {string} capabilityId - The capability ID
|
|
222
232
|
* @param {string} level - The skill level (awareness, foundational, working, practitioner, expert)
|
|
223
|
-
* @param {Object} [
|
|
224
|
-
* @param {boolean} [
|
|
233
|
+
* @param {Object} [discipline] - Optional discipline to determine which responsibilities to use
|
|
234
|
+
* @param {boolean} [discipline.isManagement] - Whether this is a management discipline
|
|
225
235
|
* @returns {string|undefined} The responsibility statement or undefined
|
|
226
236
|
*/
|
|
227
237
|
export function getCapabilityResponsibility(
|
|
228
238
|
capabilities,
|
|
229
239
|
capabilityId,
|
|
230
240
|
level,
|
|
231
|
-
|
|
241
|
+
discipline,
|
|
232
242
|
) {
|
|
233
243
|
const capability = getCapabilityById(capabilities, capabilityId);
|
|
234
|
-
const responsibilityKey =
|
|
244
|
+
const responsibilityKey = discipline?.isManagement
|
|
235
245
|
? "managementResponsibilities"
|
|
236
246
|
: "professionalResponsibilities";
|
|
237
247
|
return capability?.[responsibilityKey]?.[level];
|
|
@@ -288,6 +298,7 @@ export const SkillType = {
|
|
|
288
298
|
* @property {string} roleTitle - Display name for a person in this role (e.g., "Software Engineer")
|
|
289
299
|
* @property {string} [name] - Legacy display name (deprecated, use specialization/roleTitle)
|
|
290
300
|
* @property {string} description - Description of the discipline
|
|
301
|
+
* @property {Array<string|null>} validTracks - Valid track configurations. null = allow trackless (generalist), string = track ID
|
|
291
302
|
* @property {string[]} coreSkills - Skill IDs requiring deep expertise (Practitioner/Expert)
|
|
292
303
|
* @property {string[]} supportingSkills - Skill IDs requiring solid competence (Working/Practitioner)
|
|
293
304
|
* @property {string[]} broadSkills - Skill IDs requiring awareness (Awareness/Foundational)
|
|
@@ -305,12 +316,9 @@ export const SkillType = {
|
|
|
305
316
|
* @property {string} id - Unique identifier
|
|
306
317
|
* @property {string} name - Display name
|
|
307
318
|
* @property {string} description - Description of the track focus
|
|
308
|
-
* @property {
|
|
309
|
-
* @property {boolean} [isManagement=false] - Whether this is a management/people manager track
|
|
310
|
-
* @property {Object<string, number>} skillModifiers - Map of skill ID to level modifier (positive or negative integer)
|
|
319
|
+
* @property {Object<string, number>} skillModifiers - Map of capability/skill ID to level modifier (positive or negative integer)
|
|
311
320
|
* @property {Object<string, number>} behaviourModifiers - Map of behaviour ID to maturity modifier (positive or negative integer)
|
|
312
321
|
* @property {AssessmentWeights} [assessmentWeights] - Optional custom weights for job matching
|
|
313
|
-
* @property {string[]} [validDisciplines] - Optional array of discipline IDs this track is valid for
|
|
314
322
|
* @property {string} [minGrade] - Optional minimum grade ID this track is valid for
|
|
315
323
|
*/
|
|
316
324
|
|