@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
@@ -121,6 +121,11 @@ async function loadDisciplinesFromDir(disciplinesDir) {
121
121
  const {
122
122
  specialization,
123
123
  roleTitle,
124
+ // Track constraints
125
+ isProfessional,
126
+ isManagement,
127
+ validTracks,
128
+ minGrade,
124
129
  // Shared content - now at root level
125
130
  description,
126
131
  // Structural properties (derivation inputs) - at top level
@@ -136,6 +141,11 @@ async function loadDisciplinesFromDir(disciplinesDir) {
136
141
  id,
137
142
  specialization,
138
143
  roleTitle,
144
+ // Track constraints
145
+ isProfessional,
146
+ isManagement,
147
+ validTracks,
148
+ minGrade,
139
149
  // Shared content at top level
140
150
  description,
141
151
  // Structural properties at top level
@@ -175,12 +185,10 @@ async function loadTracksFromDir(tracksDir) {
175
185
  description,
176
186
  roleContext,
177
187
  // Structural properties (derivation inputs) - at top level
178
- isProfessional,
179
- isManagement,
180
188
  skillModifiers,
181
189
  behaviourModifiers,
182
190
  assessmentWeights,
183
- validDisciplines,
191
+ minGrade,
184
192
  // Agent section (no human section anymore for tracks)
185
193
  agent,
186
194
  } = content;
@@ -191,12 +199,10 @@ async function loadTracksFromDir(tracksDir) {
191
199
  description,
192
200
  roleContext,
193
201
  // Structural properties at top level
194
- isProfessional,
195
- isManagement,
196
202
  skillModifiers,
197
203
  behaviourModifiers,
198
204
  assessmentWeights,
199
- validDisciplines,
205
+ minGrade,
200
206
  // Preserve agent section for agent generation
201
207
  ...(agent && { agent }),
202
208
  };
@@ -486,7 +492,7 @@ export function loadAndValidate(data, options = {}) {
486
492
  * Load agent-specific data for agent profile generation
487
493
  * Uses co-located files: each entity file contains both human and agent sections
488
494
  * @param {string} dataDir - Path to the data directory
489
- * @returns {Promise<Object>} Agent data including disciplines, tracks, behaviours, vscodeSettings
495
+ * @returns {Promise<Object>} Agent data including disciplines, tracks, behaviours, vscodeSettings, devcontainer, copilotSetupSteps
490
496
  */
491
497
  export async function loadAgentData(dataDir) {
492
498
  const disciplinesDir = join(dataDir, "disciplines");
@@ -494,15 +500,27 @@ export async function loadAgentData(dataDir) {
494
500
  const behavioursDir = join(dataDir, "behaviours");
495
501
 
496
502
  // Load from co-located files
497
- const [disciplineFiles, trackFiles, behaviourFiles, vscodeSettings] =
498
- await Promise.all([
499
- loadDisciplinesFromDir(disciplinesDir),
500
- loadTracksFromDir(tracksDir),
501
- loadBehavioursFromDir(behavioursDir),
502
- fileExists(join(dataDir, "vscode-settings.yaml"))
503
- ? loadYamlFile(join(dataDir, "vscode-settings.yaml"))
504
- : {},
505
- ]);
503
+ const [
504
+ disciplineFiles,
505
+ trackFiles,
506
+ behaviourFiles,
507
+ vscodeSettings,
508
+ devcontainer,
509
+ copilotSetupSteps,
510
+ ] = await Promise.all([
511
+ loadDisciplinesFromDir(disciplinesDir),
512
+ loadTracksFromDir(tracksDir),
513
+ loadBehavioursFromDir(behavioursDir),
514
+ fileExists(join(dataDir, "vscode-settings.yaml"))
515
+ ? loadYamlFile(join(dataDir, "vscode-settings.yaml"))
516
+ : {},
517
+ fileExists(join(dataDir, "devcontainer.yaml"))
518
+ ? loadYamlFile(join(dataDir, "devcontainer.yaml"))
519
+ : {},
520
+ fileExists(join(dataDir, "copilot-setup-steps.yaml"))
521
+ ? loadYamlFile(join(dataDir, "copilot-setup-steps.yaml"))
522
+ : null,
523
+ ]);
506
524
 
507
525
  // Extract agent sections from co-located files
508
526
  const disciplines = disciplineFiles
@@ -531,6 +549,8 @@ export async function loadAgentData(dataDir) {
531
549
  tracks,
532
550
  behaviours,
533
551
  vscodeSettings,
552
+ devcontainer,
553
+ copilotSetupSteps,
534
554
  };
535
555
  }
536
556
 
@@ -551,11 +571,15 @@ export async function loadSkillsWithAgentData(dataDir) {
551
571
  const allSkills = [];
552
572
 
553
573
  for (const file of yamlFiles) {
574
+ const capabilityId = basename(file, ".yaml"); // Derive ID from filename
554
575
  const capability = await loadYamlFile(join(capabilitiesDir, file));
555
576
 
556
577
  if (capability.skills && Array.isArray(capability.skills)) {
557
578
  for (const skill of capability.skills) {
558
- allSkills.push(skill);
579
+ allSkills.push({
580
+ ...skill,
581
+ capability: capabilityId, // Add capability from parent filename
582
+ });
559
583
  }
560
584
  }
561
585
  }
@@ -275,9 +275,9 @@ function calculateExpectationsScore(selfExpectations, jobExpectations) {
275
275
  * @returns {import('./levels.js').MatchAnalysis}
276
276
  */
277
277
  export function calculateJobMatch(selfAssessment, job) {
278
- // Get weights from track or use defaults
279
- const skillWeight = job.track.assessmentWeights?.skillWeight ?? 0.5;
280
- const behaviourWeight = job.track.assessmentWeights?.behaviourWeight ?? 0.5;
278
+ // Get weights from track or use defaults (track may be null for trackless jobs)
279
+ const skillWeight = job.track?.assessmentWeights?.skillWeight ?? 0.5;
280
+ const behaviourWeight = job.track?.assessmentWeights?.behaviourWeight ?? 0.5;
281
281
 
282
282
  // Calculate skill score
283
283
  const skillResult = calculateSkillScore(
@@ -362,6 +362,36 @@ export function findMatchingJobs({
362
362
 
363
363
  // Generate all valid job combinations
364
364
  for (const discipline of disciplines) {
365
+ // First generate trackless jobs for each discipline × grade
366
+ for (const grade of grades) {
367
+ if (
368
+ !isValidJobCombination({
369
+ discipline,
370
+ grade,
371
+ track: null,
372
+ validationRules,
373
+ grades,
374
+ })
375
+ ) {
376
+ continue;
377
+ }
378
+
379
+ const job = deriveJob({
380
+ discipline,
381
+ grade,
382
+ track: null,
383
+ skills,
384
+ behaviours,
385
+ validationRules,
386
+ });
387
+
388
+ if (job) {
389
+ const analysis = calculateJobMatch(selfAssessment, job);
390
+ matches.push({ job, analysis });
391
+ }
392
+ }
393
+
394
+ // Then generate jobs with valid tracks
365
395
  for (const track of tracks) {
366
396
  for (const grade of grades) {
367
397
  // Skip invalid combinations
@@ -10,6 +10,10 @@
10
10
  *
11
11
  * The core derivation (deriveSkillMatrix, deriveBehaviourProfile) remains in
12
12
  * derivation.js. This module adds post-processing for specific use cases.
13
+ *
14
+ * Agent filtering keeps only skills at the highest derived level. This ensures
15
+ * track modifiers are respected—a broad skill boosted by a +1 track modifier
16
+ * may reach the same level as primary skills and thus be included.
13
17
  */
14
18
 
15
19
  import { SKILL_LEVEL_ORDER, BEHAVIOUR_MATURITY_ORDER } from "./levels.js";
@@ -47,44 +51,40 @@ export function filterHumanOnlySkills(skillMatrix) {
47
51
  }
48
52
 
49
53
  /**
50
- * Filter broad skills without positive track modifier
51
- * Keeps broad skills only if their capability has a positive track modifier
52
- * @param {Array} skillMatrix - Skill matrix entries
53
- * @param {Set<string>} positiveCapabilities - Capabilities with positive modifiers
54
- * @returns {Array} Filtered skill matrix
54
+ * Filter skills to keep only those at the highest derived level
55
+ * After track modifiers are applied, some skills will be at higher levels
56
+ * than others. This filter keeps only the skills at the maximum level.
57
+ * @param {Array} skillMatrix - Skill matrix entries with derived levels
58
+ * @returns {Array} Filtered skill matrix containing only highest-level skills
55
59
  */
56
- export function filterBroadWithoutPositiveModifier(
57
- skillMatrix,
58
- positiveCapabilities,
59
- ) {
60
+ export function filterByHighestLevel(skillMatrix) {
61
+ if (skillMatrix.length === 0) return [];
62
+
63
+ // Find the highest level index in the matrix
64
+ const maxLevelIndex = Math.max(
65
+ ...skillMatrix.map((entry) => SKILL_LEVEL_ORDER.indexOf(entry.level)),
66
+ );
67
+
68
+ // Keep only skills at that level
60
69
  return skillMatrix.filter(
61
- (entry) =>
62
- entry.type !== "broad" || positiveCapabilities.has(entry.capability),
70
+ (entry) => SKILL_LEVEL_ORDER.indexOf(entry.level) === maxLevelIndex,
63
71
  );
64
72
  }
65
73
 
66
74
  /**
67
75
  * Apply agent-specific skill filters
68
- * Combines humanOnly and broad skill filtering for agent profiles
69
- * @param {Array} skillMatrix - Skill matrix entries
70
- * @param {Object} track - Track definition (for modifier lookup)
76
+ * Filters to human-only skills and keeps only skills at the highest derived level.
77
+ * This approach respects track modifiers—a broad skill boosted to the same level
78
+ * as primary skills will be included.
79
+ * @param {Array} skillMatrix - Skill matrix entries with derived levels
71
80
  * @returns {Array} Filtered skill matrix
72
81
  */
73
- export function filterSkillsForAgent(skillMatrix, track) {
74
- const positiveCapabilities = getPositiveTrackCapabilities(track);
82
+ export function filterSkillsForAgent(skillMatrix) {
83
+ // First exclude human-only skills
84
+ const withoutHumanOnly = filterHumanOnlySkills(skillMatrix);
75
85
 
76
- return skillMatrix.filter((entry) => {
77
- // Exclude human-only skills
78
- if (entry.isHumanOnly) {
79
- return false;
80
- }
81
- // For broad skills, only include if capability has positive modifier
82
- if (entry.type === "broad") {
83
- return positiveCapabilities.has(entry.capability);
84
- }
85
- // Include primary, secondary, and track-added skills
86
- return true;
87
- });
86
+ // Then keep only skills at the highest level
87
+ return filterByHighestLevel(withoutHumanOnly);
88
88
  }
89
89
 
90
90
  // =============================================================================
@@ -126,7 +126,7 @@ export function sortByMaturityDescending(behaviourProfile) {
126
126
  /**
127
127
  * @typedef {Object} ProfileOptions
128
128
  * @property {boolean} [excludeHumanOnly=false] - Filter out human-only skills
129
- * @property {boolean} [excludeBroadWithoutPositiveModifier=false] - Filter broad skills
129
+ * @property {boolean} [keepHighestLevelOnly=false] - Keep only skills at the highest derived level
130
130
  * @property {boolean} [sortByLevel=false] - Sort skills by level descending
131
131
  * @property {boolean} [sortByMaturity=false] - Sort behaviours by maturity descending
132
132
  */
@@ -148,7 +148,7 @@ export function sortByMaturityDescending(behaviourProfile) {
148
148
  * and AI agents use this function, with different options:
149
149
  *
150
150
  * - Human jobs: No filtering, default sorting by type
151
- * - AI agents: Filter humanOnly, filter broad without positive modifier, sort by level
151
+ * - AI agents: Filter humanOnly, keep only highest-level skills, sort by level
152
152
  *
153
153
  * @param {Object} params
154
154
  * @param {Object} params.discipline - The discipline
@@ -171,7 +171,7 @@ export function prepareBaseProfile({
171
171
  }) {
172
172
  const {
173
173
  excludeHumanOnly = false,
174
- excludeBroadWithoutPositiveModifier = false,
174
+ keepHighestLevelOnly = false,
175
175
  sortByLevel = false,
176
176
  sortByMaturity = false,
177
177
  } = options;
@@ -186,18 +186,11 @@ export function prepareBaseProfile({
186
186
  });
187
187
 
188
188
  // Apply skill filters
189
- if (excludeHumanOnly || excludeBroadWithoutPositiveModifier) {
190
- const positiveCapabilities = getPositiveTrackCapabilities(track);
191
-
192
- if (excludeHumanOnly) {
193
- skillMatrix = filterHumanOnlySkills(skillMatrix);
194
- }
195
- if (excludeBroadWithoutPositiveModifier) {
196
- skillMatrix = filterBroadWithoutPositiveModifier(
197
- skillMatrix,
198
- positiveCapabilities,
199
- );
200
- }
189
+ if (excludeHumanOnly) {
190
+ skillMatrix = filterHumanOnlySkills(skillMatrix);
191
+ }
192
+ if (keepHighestLevelOnly) {
193
+ skillMatrix = filterByHighestLevel(skillMatrix);
201
194
  }
202
195
 
203
196
  // Apply sorting
@@ -230,12 +223,12 @@ export function prepareBaseProfile({
230
223
 
231
224
  /**
232
225
  * Preset options for agent profile derivation
233
- * Excludes human-only skills, excludes broad skills without positive modifiers,
226
+ * Excludes human-only skills, keeps only skills at the highest derived level,
234
227
  * and sorts by level/maturity descending
235
228
  */
236
229
  export const AGENT_PROFILE_OPTIONS = {
237
230
  excludeHumanOnly: true,
238
- excludeBroadWithoutPositiveModifier: true,
231
+ keepHighestLevelOnly: true,
239
232
  sortByLevel: true,
240
233
  sortByMaturity: true,
241
234
  };