@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.
Files changed (131) hide show
  1. package/app/commands/agent.js +109 -21
  2. package/app/commands/command-factory.js +3 -3
  3. package/app/commands/interview.js +14 -7
  4. package/app/commands/job.js +43 -29
  5. package/app/commands/progress.js +14 -7
  6. package/app/commands/serve.js +5 -0
  7. package/app/commands/stage.js +0 -10
  8. package/app/commands/track.js +5 -8
  9. package/app/components/builder.js +111 -27
  10. package/app/css/components/surfaces.css +16 -0
  11. package/app/formatters/agent/profile.js +113 -87
  12. package/app/formatters/agent/skill.js +64 -31
  13. package/app/formatters/behaviour/dom.js +3 -0
  14. package/app/formatters/behaviour/microdata.js +106 -0
  15. package/app/formatters/discipline/dom.js +28 -1
  16. package/app/formatters/discipline/microdata.js +117 -0
  17. package/app/formatters/discipline/shared.js +49 -8
  18. package/app/formatters/driver/dom.js +3 -0
  19. package/app/formatters/driver/microdata.js +91 -0
  20. package/app/formatters/grade/dom.js +3 -0
  21. package/app/formatters/grade/microdata.js +151 -0
  22. package/app/formatters/index.js +32 -1
  23. package/app/formatters/interview/shared.js +13 -8
  24. package/app/formatters/job/description.js +5 -3
  25. package/app/formatters/json-ld.js +242 -0
  26. package/app/formatters/microdata-shared.js +184 -0
  27. package/app/formatters/progress/shared.js +14 -11
  28. package/app/formatters/skill/dom.js +3 -0
  29. package/app/formatters/skill/microdata.js +151 -0
  30. package/app/formatters/stage/dom.js +3 -18
  31. package/app/formatters/stage/microdata.js +110 -0
  32. package/app/formatters/stage/shared.js +0 -27
  33. package/app/formatters/track/dom.js +5 -30
  34. package/app/formatters/track/markdown.js +2 -25
  35. package/app/formatters/track/microdata.js +111 -0
  36. package/app/formatters/track/shared.js +6 -58
  37. package/app/handout-main.js +26 -12
  38. package/app/index.html +11 -0
  39. package/app/lib/card-mappers.js +17 -12
  40. package/app/lib/job-cache.js +12 -9
  41. package/app/lib/template-loader.js +66 -0
  42. package/app/lib/yaml-loader.js +25 -8
  43. package/app/main.js +8 -4
  44. package/app/model/agent.js +158 -130
  45. package/app/model/checklist.js +57 -91
  46. package/app/model/derivation.js +135 -68
  47. package/app/model/index-generator.js +1 -7
  48. package/app/model/job.js +19 -13
  49. package/app/model/levels.js +20 -12
  50. package/app/model/loader.js +41 -17
  51. package/app/model/matching.js +33 -3
  52. package/app/model/profile.js +38 -45
  53. package/app/model/schema-validation.js +438 -0
  54. package/app/model/validation.js +747 -68
  55. package/app/pages/agent-builder.js +119 -25
  56. package/app/pages/assessment-results.js +10 -4
  57. package/app/pages/discipline.js +36 -6
  58. package/app/pages/driver.js +9 -47
  59. package/app/pages/interview-builder.js +3 -1
  60. package/app/pages/interview.js +15 -4
  61. package/app/pages/job-builder.js +4 -1
  62. package/app/pages/job.js +15 -4
  63. package/app/pages/landing.js +10 -10
  64. package/app/pages/progress-builder.js +3 -1
  65. package/app/pages/progress.js +72 -21
  66. package/app/pages/stage.js +3 -126
  67. package/app/slide-main.js +45 -17
  68. package/app/slides/index.js +3 -1
  69. package/app/slides/overview.js +40 -4
  70. package/app/slides/progress.js +4 -2
  71. package/bin/pathway.js +18 -64
  72. package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
  73. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
  74. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
  75. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
  76. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
  77. package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
  78. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
  79. package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
  80. package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
  81. package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
  82. package/examples/agents/.vscode/settings.json +1 -1
  83. package/examples/behaviours/outcome_ownership.yaml +1 -2
  84. package/examples/behaviours/polymathic_knowledge.yaml +1 -2
  85. package/examples/behaviours/precise_communication.yaml +1 -2
  86. package/examples/behaviours/relentless_curiosity.yaml +1 -2
  87. package/examples/behaviours/systems_thinking.yaml +1 -2
  88. package/examples/capabilities/business.yaml +80 -142
  89. package/examples/capabilities/delivery.yaml +155 -219
  90. package/examples/capabilities/people.yaml +2 -34
  91. package/examples/capabilities/reliability.yaml +161 -80
  92. package/examples/capabilities/scale.yaml +234 -252
  93. package/examples/copilot-setup-steps.yaml +25 -0
  94. package/examples/devcontainer.yaml +21 -0
  95. package/examples/disciplines/_index.yaml +1 -0
  96. package/examples/disciplines/data_engineering.yaml +14 -12
  97. package/examples/disciplines/engineering_management.yaml +63 -0
  98. package/examples/disciplines/software_engineering.yaml +14 -12
  99. package/examples/drivers.yaml +1 -4
  100. package/examples/framework.yaml +1 -2
  101. package/examples/grades.yaml +1 -3
  102. package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
  103. package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
  104. package/examples/questions/behaviours/precise_communication.yaml +1 -2
  105. package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
  106. package/examples/questions/behaviours/systems_thinking.yaml +1 -2
  107. package/examples/questions/skills/architecture_design.yaml +1 -2
  108. package/examples/questions/skills/cloud_platforms.yaml +1 -2
  109. package/examples/questions/skills/code_quality.yaml +1 -2
  110. package/examples/questions/skills/data_modeling.yaml +1 -2
  111. package/examples/questions/skills/devops.yaml +1 -2
  112. package/examples/questions/skills/full_stack_development.yaml +1 -2
  113. package/examples/questions/skills/sre_practices.yaml +1 -2
  114. package/examples/questions/skills/stakeholder_management.yaml +1 -2
  115. package/examples/questions/skills/team_collaboration.yaml +1 -2
  116. package/examples/questions/skills/technical_writing.yaml +1 -2
  117. package/examples/self-assessments.yaml +1 -3
  118. package/examples/stages.yaml +101 -46
  119. package/examples/tracks/_index.yaml +0 -1
  120. package/examples/tracks/platform.yaml +8 -13
  121. package/examples/tracks/sre.yaml +8 -18
  122. package/examples/vscode-settings.yaml +2 -7
  123. package/package.json +9 -3
  124. package/templates/agent.template.md +65 -0
  125. package/templates/skill.template.md +28 -0
  126. package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
  127. package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
  128. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
  129. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
  130. package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
  131. package/examples/tracks/manager.yaml +0 -53
@@ -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 modifier = resolveSkillModifier(skillId, track.skillModifiers, skills);
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 trackModifier = track.behaviourModifiers?.[behaviourId] ?? 0;
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(track.skillModifiers || {})
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 track's validDisciplines constraint (e.g., SRE only valid for Software Engineering and Data Engineering)
294
- if (track.validDisciplines && track.validDisciplines.length > 0) {
295
- if (!track.validDisciplines.includes(discipline.id)) {
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 (e.g., manager track only valid for senior grades)
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
- if (!validationRules) {
309
- return true;
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
- * - If professional track and grade starts with "Level": ${discipline.roleTitle} ${grade.professionalTitle} - ${track.name}
342
- * - Else if professional track: ${grade.professionalTitle} ${discipline.roleTitle} - ${track.name}
343
- * - Else if management track: ${grade.managementTitle}, ${discipline.specialization} - ${track.name}
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
- // Determine if track is professional or management (default to professional)
355
- const isProfessional = track.isProfessional !== false && !track.isManagement;
356
- const isManagement = track.isManagement === true;
382
+ // Management discipline (no track needed)
383
+ if (isManagement && !track) {
384
+ return `${managementTitle}, ${specialization}`;
385
+ }
357
386
 
358
- if (isManagement) {
359
- // Management family track: "Director, Software Engineering"
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
- } else if (isProfessional && professionalTitle.startsWith("Level")) {
366
- // Professional track with Level grade: "Software Engineer Level II - Platform"
367
- return `${roleTitle} ${professionalTitle} - ${track.name}`;
368
- } else {
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, track, and grade
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, track, grade) {
382
- return `${discipline.id}_${track.id}_${grade.id}`;
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 tracks (professional: true)
396
- * and managementResponsibilities for management tracks (management: true).
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').Track} params.track - The track (determines which responsibilities to use)
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({ skillMatrix, capabilities, track }) {
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 track type
410
- // Management tracks use managementResponsibilities, professional tracks use professionalResponsibilities
411
- const responsibilityKey = track?.isManagement
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, track, and grade
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
- track,
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 track of tracks) {
669
- for (const grade of grades) {
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
- if (!discipline || !grade || !track) return null;
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 handoff type
74
+ // Derive checklists for each stage
74
75
  const checklists = {};
75
76
  if (capabilities) {
76
- const handoffTypes = ["plan_to_code", "code_to_review"];
77
- for (const handoff of handoffTypes) {
78
- checklists[handoff] = deriveChecklist({
79
- handoff,
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.id,
93
- trackName: track.name,
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 || !track) return null;
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.id,
151
- trackName: track.name,
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
- if (!discipline || !grade || !track) {
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: `The ${track.name} track is not available for ${discipline.specialization}.`,
214
+ invalidReason: reason,
209
215
  };
210
216
  }
211
217
 
@@ -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 = [Stage.PLAN, Stage.CODE, Stage.REVIEW];
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 & documentation
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 tracks and
218
- * managementResponsibilities for management tracks.
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} [track] - Optional track to determine which responsibilities to use
224
- * @param {boolean} [track.isManagement] - Whether this is a management track
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
- track,
241
+ discipline,
232
242
  ) {
233
243
  const capability = getCapabilityById(capabilities, capabilityId);
234
- const responsibilityKey = track?.isManagement
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 {boolean} [isProfessional=true] - Whether this is a professional/individual contributor track
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