@forwardimpact/pathway 0.11.0 → 0.13.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.
@@ -18,7 +18,7 @@
18
18
  * stage [<id>] Show stages
19
19
  * tool [<name>] Show tools
20
20
  * job [<discipline> <grade>] [--track=TRACK] Generate job definition
21
- * interview <discipline> <grade> [--track=TRACK] [--type=TYPE] Generate interview
21
+ * interview <discipline> <grade> [--track=TRACK] [--type=mission|decomposition|stakeholder] Generate interview
22
22
  * progress <discipline> <grade> [--track=TRACK] [--compare=GRADE] Career progression
23
23
  * questions [options] Browse interview questions
24
24
  * agent [<discipline> <track>] [--output=PATH] Generate AI agent
@@ -167,13 +167,14 @@ INTERVIEW COMMAND
167
167
  Generate interview question sets based on job requirements.
168
168
 
169
169
  Usage:
170
- npx fit-pathway interview <discipline> <grade>
171
- npx fit-pathway interview <d> <g> --track=<track>
172
- npx fit-pathway interview <d> <g> --type=<type>
170
+ npx fit-pathway interview <discipline> <grade> All types
171
+ npx fit-pathway interview <d> <g> --track=<track> With track
172
+ npx fit-pathway interview <d> <g> --track=<t> --type=<type> Single type
173
173
 
174
174
  Options:
175
175
  --track=TRACK Track specialization
176
- --type=TYPE Interview type: full (default), short
176
+ --type=TYPE Interview type: mission, decomposition, stakeholder
177
+ (omit for all types)
177
178
 
178
179
  ────────────────────────────────────────────────────────────────────────────────
179
180
  PROGRESS COMMAND
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Career progression web app and CLI for exploring roles and generating agents",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -40,8 +40,8 @@
40
40
  "./commands": "./src/commands/index.js"
41
41
  },
42
42
  "dependencies": {
43
- "@forwardimpact/schema": "^0.4.0",
44
- "@forwardimpact/model": "^0.5.0",
43
+ "@forwardimpact/schema": "^0.6.0",
44
+ "@forwardimpact/model": "^0.7.0",
45
45
  "mustache": "^4.2.0",
46
46
  "simple-icons": "^16.7.0",
47
47
  "yaml": "^2.3.4"
@@ -37,7 +37,7 @@ import {
37
37
  validateAgentSkill,
38
38
  deriveReferenceGrade,
39
39
  deriveAgentSkills,
40
- generateSkillMd,
40
+ generateSkillMarkdown,
41
41
  deriveToolkit,
42
42
  buildAgentIndex,
43
43
  } from "@forwardimpact/model";
@@ -502,7 +502,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
502
502
  const skillFiles = derivedSkills
503
503
  .map((derived) => skillsWithAgent.find((s) => s.id === derived.skillId))
504
504
  .filter((skill) => skill?.agent)
505
- .map((skill) => generateSkillMd(skill, data.stages));
505
+ .map((skill) => generateSkillMarkdown(skill, data.stages));
506
506
 
507
507
  // Validate all profiles
508
508
  for (const profile of profiles) {
@@ -4,9 +4,9 @@
4
4
  * Generates and displays interview questions in the terminal.
5
5
  *
6
6
  * Usage:
7
- * npx pathway interview <discipline> <grade> # Interview for trackless job
8
- * npx pathway interview <discipline> <grade> --track=<track> # Interview with track
9
- * npx pathway interview <discipline> <grade> --track=<track> --type=short
7
+ * npx fit-pathway interview <discipline> <grade> # All interview types
8
+ * npx fit-pathway interview <discipline> <grade> --track=<track> # With track
9
+ * npx fit-pathway interview <discipline> <grade> --track=<track> --type=mission # Single type
10
10
  */
11
11
 
12
12
  import { createCompositeCommand } from "./command-factory.js";
@@ -16,8 +16,10 @@ import {
16
16
  } from "../formatters/interview/shared.js";
17
17
  import { interviewToMarkdown } from "../formatters/interview/markdown.js";
18
18
 
19
+ const VALID_TYPES = Object.keys(INTERVIEW_TYPES);
20
+
19
21
  /**
20
- * Format interview output
22
+ * Format a single interview type as markdown
21
23
  * @param {Object} view - Presenter view
22
24
  * @param {Object} options - Options including framework
23
25
  */
@@ -25,15 +27,31 @@ function formatInterview(view, options) {
25
27
  console.log(interviewToMarkdown(view, { framework: options.framework }));
26
28
  }
27
29
 
30
+ /**
31
+ * Format all interview types as markdown with separators
32
+ * @param {Array<Object>} views - Array of presenter views
33
+ * @param {Object} options - Options including framework
34
+ */
35
+ function formatAllInterviews(views, options) {
36
+ for (let i = 0; i < views.length; i++) {
37
+ if (i > 0) {
38
+ console.log("\n" + "─".repeat(80) + "\n");
39
+ }
40
+ console.log(
41
+ interviewToMarkdown(views[i], { framework: options.framework }),
42
+ );
43
+ }
44
+ }
45
+
28
46
  export const runInterviewCommand = createCompositeCommand({
29
47
  commandName: "interview",
30
48
  requiredArgs: ["discipline_id", "grade_id"],
31
49
  findEntities: (data, args, options) => {
32
- const interviewType = options.type || "full";
50
+ const interviewType = options.type === "full" ? null : options.type;
33
51
 
34
- if (!INTERVIEW_TYPES[interviewType]) {
52
+ if (interviewType && !INTERVIEW_TYPES[interviewType]) {
35
53
  console.error(`Unknown interview type: ${interviewType}`);
36
- console.error("Available types: full, short, behaviour");
54
+ console.error(`Available types: ${VALID_TYPES.join(", ")}`);
37
55
  process.exit(1);
38
56
  }
39
57
 
@@ -58,18 +76,38 @@ export const runInterviewCommand = createCompositeCommand({
58
76
  }
59
77
  return null;
60
78
  },
61
- presenter: (entities, data, _options) =>
62
- prepareInterviewDetail({
79
+ presenter: (entities, data, _options) => {
80
+ const params = {
63
81
  discipline: entities.discipline,
64
82
  grade: entities.grade,
65
83
  track: entities.track,
66
84
  skills: data.skills,
67
85
  behaviours: data.behaviours,
68
86
  questions: data.questions,
69
- interviewType: entities.interviewType,
70
- }),
71
- formatter: (view, options, data) =>
72
- formatInterview(view, { ...options, framework: data.framework }),
87
+ };
88
+
89
+ // Single type: return one view
90
+ if (entities.interviewType) {
91
+ return prepareInterviewDetail({
92
+ ...params,
93
+ interviewType: entities.interviewType,
94
+ });
95
+ }
96
+
97
+ // All types: return array of views
98
+ return VALID_TYPES.map((type) =>
99
+ prepareInterviewDetail({ ...params, interviewType: type }),
100
+ ).filter(Boolean);
101
+ },
102
+ formatter: (view, options, data) => {
103
+ const opts = { ...options, framework: data.framework };
104
+
105
+ if (Array.isArray(view)) {
106
+ formatAllInterviews(view, opts);
107
+ } else {
108
+ formatInterview(view, opts);
109
+ }
110
+ },
73
111
  usageExample:
74
- "npx pathway interview software_engineering L4 --track=platform --type=short",
112
+ "npx fit-pathway interview software_engineering J090 --track=platform --type=mission",
75
113
  });
@@ -56,12 +56,15 @@ function showQuestionsSummary(data) {
56
56
  "practitioner",
57
57
  "expert",
58
58
  ];
59
+ const roleTypes = ["professionalQuestions", "managementQuestions"];
59
60
  const skillRows = skillLevels.map((level) => {
60
61
  let count = 0;
61
62
  for (const skill of skills) {
62
- const sq = questions.skills?.[skill.id];
63
- if (sq?.[level]) {
64
- count += sq[level].length;
63
+ const sq = questions.skillLevels?.[skill.id];
64
+ if (sq) {
65
+ for (const roleType of roleTypes) {
66
+ count += (sq[roleType]?.[level] || []).length;
67
+ }
65
68
  }
66
69
  }
67
70
  return [level, count];
@@ -81,9 +84,11 @@ function showQuestionsSummary(data) {
81
84
  const behaviourRows = maturities.map((maturity) => {
82
85
  let count = 0;
83
86
  for (const behaviour of behaviours) {
84
- const bq = questions.behaviours?.[behaviour.id];
85
- if (bq?.[maturity]) {
86
- count += bq[maturity].length;
87
+ const bq = questions.behaviourMaturities?.[behaviour.id];
88
+ if (bq) {
89
+ for (const roleType of roleTypes) {
90
+ count += (bq[roleType]?.[maturity] || []).length;
91
+ }
87
92
  }
88
93
  }
89
94
  return [maturity.replace(/_/g, " "), count];
@@ -138,10 +143,8 @@ export async function runQuestionsCommand({
138
143
  behaviours: data.behaviours,
139
144
  filter,
140
145
  });
141
- for (const section of view.sections) {
142
- for (const q of section.questions) {
143
- console.log(q.id);
144
- }
146
+ for (const q of view.questions) {
147
+ console.log(q.id);
145
148
  }
146
149
  return;
147
150
  }
@@ -16,7 +16,7 @@ import { skillToMarkdown } from "../formatters/skill/markdown.js";
16
16
  import { prepareSkillsList } from "../formatters/skill/shared.js";
17
17
  import { getConceptEmoji } from "@forwardimpact/schema/levels";
18
18
  import { formatTable, formatError } from "../lib/cli-output.js";
19
- import { generateSkillMd } from "@forwardimpact/model/agent";
19
+ import { generateSkillMarkdown } from "@forwardimpact/model/agent";
20
20
  import { formatAgentSkill } from "../formatters/agent/skill.js";
21
21
  import { loadSkillTemplate } from "../lib/template-loader.js";
22
22
 
@@ -80,7 +80,7 @@ async function formatAgentDetail(skill, stages, dataDir) {
80
80
  }
81
81
 
82
82
  const template = await loadSkillTemplate(dataDir);
83
- const skillMd = generateSkillMd(skill, stages);
83
+ const skillMd = generateSkillMarkdown(skill, stages);
84
84
  const output = formatAgentSkill(skillMd, template);
85
85
  console.log(output);
86
86
  }
@@ -13,7 +13,7 @@ import {
13
13
  getBehaviourMaturityIndex,
14
14
  formatLevel,
15
15
  } from "../lib/render.js";
16
- import { getCapabilityIndex } from "@forwardimpact/schema/levels";
16
+ import { compareByCapability } from "@forwardimpact/model/policies";
17
17
 
18
18
  /**
19
19
  * Create a comparison skill radar chart
@@ -24,7 +24,7 @@ import { getCapabilityIndex } from "@forwardimpact/schema/levels";
24
24
  */
25
25
  export function createComparisonSkillRadar(
26
26
  currentMatrix,
27
- targetMatrix,
27
+ targetMatrix = [],
28
28
  options = {},
29
29
  ) {
30
30
  const container = div(
@@ -80,9 +80,11 @@ export function createComparisonSkillRadar(
80
80
  }
81
81
 
82
82
  // Sort by capability order, then by skill name within capability
83
+ const capabilityComparator = options.capabilities
84
+ ? compareByCapability(options.capabilities)
85
+ : (a, b) => a.capability.localeCompare(b.capability);
83
86
  skillEntries.sort((a, b) => {
84
- const capDiff =
85
- getCapabilityIndex(a.capability) - getCapabilityIndex(b.capability);
87
+ const capDiff = capabilityComparator(a, b);
86
88
  if (capDiff !== 0) return capDiff;
87
89
  return a.skillName.localeCompare(b.skillName);
88
90
  });
@@ -141,7 +143,7 @@ export function createComparisonSkillRadar(
141
143
  */
142
144
  export function createComparisonBehaviourRadar(
143
145
  currentProfile,
144
- targetProfile,
146
+ targetProfile = [],
145
147
  options = {},
146
148
  ) {
147
149
  const container = div(
@@ -93,50 +93,60 @@
93
93
  font-weight: 500;
94
94
  }
95
95
 
96
- /* Capability badges */
97
- .badge-delivery {
98
- background: var(--color-cat-delivery-light);
99
- color: var(--color-cat-delivery);
96
+ /* Capability badges - each capability ID maps to its own distinct color */
97
+ .badge-ai {
98
+ background: var(--color-cap-ai-light);
99
+ color: var(--color-cap-ai);
100
100
  }
101
101
 
102
- .badge-scale {
103
- background: var(--color-cat-scale-light);
104
- color: var(--color-cat-scale);
102
+ .badge-business {
103
+ background: var(--color-cap-business-light);
104
+ color: var(--color-cap-business);
105
105
  }
106
106
 
107
- .badge-reliability {
108
- background: var(--color-cat-reliability-light);
109
- color: var(--color-cat-reliability);
107
+ .badge-data {
108
+ background: var(--color-cap-data-light);
109
+ color: var(--color-cap-data);
110
110
  }
111
111
 
112
- .badge-data {
113
- background: var(--color-cat-data-light);
114
- color: var(--color-cat-data);
112
+ .badge-delivery {
113
+ background: var(--color-cap-delivery-light);
114
+ color: var(--color-cap-delivery);
115
115
  }
116
116
 
117
- .badge-ai {
118
- background: var(--color-cat-ai-light);
119
- color: var(--color-cat-ai);
117
+ .badge-documentation {
118
+ background: var(--color-cap-documentation-light);
119
+ color: var(--color-cap-documentation);
120
+ }
121
+
122
+ .badge-ml {
123
+ background: var(--color-cap-ml-light);
124
+ color: var(--color-cap-ml);
125
+ }
126
+
127
+ .badge-people {
128
+ background: var(--color-cap-people-light);
129
+ color: var(--color-cap-people);
120
130
  }
121
131
 
122
132
  .badge-process {
123
- background: var(--color-cat-process-light);
124
- color: var(--color-cat-process);
133
+ background: var(--color-cap-process-light);
134
+ color: var(--color-cap-process);
125
135
  }
126
136
 
127
- .badge-business {
128
- background: var(--color-cat-business-light);
129
- color: var(--color-cat-business);
137
+ .badge-product {
138
+ background: var(--color-cap-product-light);
139
+ color: var(--color-cap-product);
130
140
  }
131
141
 
132
- .badge-people {
133
- background: var(--color-cat-people-light);
134
- color: var(--color-cat-people);
142
+ .badge-reliability {
143
+ background: var(--color-cap-reliability-light);
144
+ color: var(--color-cap-reliability);
135
145
  }
136
146
 
137
- .badge-documentation {
138
- background: var(--color-cat-documentation-light);
139
- color: var(--color-cat-documentation);
147
+ .badge-scale {
148
+ background: var(--color-cap-scale-light);
149
+ color: var(--color-cap-scale);
140
150
  }
141
151
 
142
152
  /* Tool badge */
@@ -43,30 +43,34 @@
43
43
  --color-maturity-5: #c4b5fd;
44
44
 
45
45
  /* --------------------------------------------------------------------------
46
- Colors - Categories (text)
46
+ Colors - Capabilities (text) - matches data/capabilities/*.yaml IDs
47
47
  -------------------------------------------------------------------------- */
48
- --color-cat-delivery: #dc2626; /* red - rapid delivery focus */
49
- --color-cat-scale: #1d4ed8; /* blue - infrastructure/platform */
50
- --color-cat-reliability: #0369a1; /* sky - SRE/operations */
51
- --color-cat-data: #0f766e; /* teal */
52
- --color-cat-ai: #7c3aed; /* violet */
53
- --color-cat-process: #c2410c; /* orange */
54
- --color-cat-business: #a16207; /* amber */
55
- --color-cat-people: #be185d; /* pink */
56
- --color-cat-documentation: #15803d; /* green */
48
+ --color-cap-ai: #7c3aed; /* violet */
49
+ --color-cap-business: #b45309; /* amber */
50
+ --color-cap-data: #0f766e; /* teal */
51
+ --color-cap-delivery: #dc2626; /* red */
52
+ --color-cap-documentation: #15803d; /* green */
53
+ --color-cap-ml: #9333ea; /* purple */
54
+ --color-cap-people: #be185d; /* pink */
55
+ --color-cap-process: #c2410c; /* orange */
56
+ --color-cap-product: #4f46e5; /* indigo */
57
+ --color-cap-reliability: #0891b2; /* cyan */
58
+ --color-cap-scale: #1d4ed8; /* blue */
57
59
 
58
60
  /* --------------------------------------------------------------------------
59
- Colors - Categories (backgrounds)
61
+ Colors - Capabilities (backgrounds)
60
62
  -------------------------------------------------------------------------- */
61
- --color-cat-delivery-light: #fecaca; /* red */
62
- --color-cat-scale-light: #dbeafe; /* blue */
63
- --color-cat-reliability-light: #e0f2fe; /* sky */
64
- --color-cat-data-light: #ccfbf1; /* teal */
65
- --color-cat-ai-light: #ede9fe; /* violet */
66
- --color-cat-process-light: #ffedd5; /* orange */
67
- --color-cat-business-light: #fef3c7; /* amber */
68
- --color-cat-people-light: #fce7f3; /* pink */
69
- --color-cat-documentation-light: #dcfce7; /* green */
63
+ --color-cap-ai-light: #ede9fe; /* violet */
64
+ --color-cap-business-light: #fef3c7; /* amber */
65
+ --color-cap-data-light: #ccfbf1; /* teal */
66
+ --color-cap-delivery-light: #fecaca; /* red */
67
+ --color-cap-documentation-light: #dcfce7; /* green */
68
+ --color-cap-ml-light: #f3e8ff; /* purple */
69
+ --color-cap-people-light: #fce7f3; /* pink */
70
+ --color-cap-process-light: #ffedd5; /* orange */
71
+ --color-cap-product-light: #e0e7ff; /* indigo */
72
+ --color-cap-reliability-light: #cffafe; /* cyan */
73
+ --color-cap-scale-light: #dbeafe; /* blue */
70
74
 
71
75
  /* --------------------------------------------------------------------------
72
76
  Colors - Neutrals
@@ -27,6 +27,9 @@ export function interviewToMarkdown(view, { framework } = {}) {
27
27
  // Group sections by type
28
28
  const skillSections = view.sections.filter((s) => s.type === "skill");
29
29
  const behaviourSections = view.sections.filter((s) => s.type === "behaviour");
30
+ const capabilitySections = view.sections.filter(
31
+ (s) => s.type === "capability",
32
+ );
30
33
 
31
34
  // Skill questions
32
35
  if (skillSections.length > 0) {
@@ -36,27 +39,83 @@ export function interviewToMarkdown(view, { framework } = {}) {
36
39
  for (const q of section.questions) {
37
40
  lines.push(`**Q**: ${q.question}`);
38
41
  if (q.followUps.length > 0) {
42
+ lines.push("", "**Follow-ups:**");
39
43
  for (const followUp of q.followUps) {
40
44
  lines.push(` → ${followUp}`);
41
45
  }
42
46
  }
47
+ if (q.lookingFor && q.lookingFor.length > 0) {
48
+ lines.push("", "**What to look for:**");
49
+ for (const item of q.lookingFor) {
50
+ lines.push(`- ${item}`);
51
+ }
52
+ }
43
53
  lines.push("");
44
54
  }
45
55
  }
46
56
  }
47
57
 
48
- // Behaviour questions
58
+ // Capability decomposition questions
59
+ if (capabilitySections.length > 0) {
60
+ lines.push(`## 🧩 Decomposition Questions`, "");
61
+ for (const section of capabilitySections) {
62
+ lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
63
+ for (const q of section.questions) {
64
+ lines.push(`**Scenario**: ${q.question}`);
65
+ if (q.context) {
66
+ lines.push(`> ${q.context}`);
67
+ }
68
+ if (q.decompositionPrompts && q.decompositionPrompts.length > 0) {
69
+ lines.push("", "**Guide the candidate through:**");
70
+ for (const prompt of q.decompositionPrompts) {
71
+ lines.push(`- ${prompt}`);
72
+ }
73
+ }
74
+ if (q.followUps.length > 0) {
75
+ lines.push("", "**Follow-ups:**");
76
+ for (const followUp of q.followUps) {
77
+ lines.push(` → ${followUp}`);
78
+ }
79
+ }
80
+ if (q.lookingFor && q.lookingFor.length > 0) {
81
+ lines.push("", "**What to look for:**");
82
+ for (const item of q.lookingFor) {
83
+ lines.push(`- ${item}`);
84
+ }
85
+ }
86
+ lines.push("");
87
+ }
88
+ }
89
+ }
90
+
91
+ // Behaviour stakeholder simulation questions
49
92
  if (behaviourSections.length > 0) {
50
- lines.push(`## ${behaviourEmoji} Behaviour Questions`, "");
93
+ lines.push(`## ${behaviourEmoji} Stakeholder Simulation`, "");
51
94
  for (const section of behaviourSections) {
52
95
  lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
53
96
  for (const q of section.questions) {
54
- lines.push(`**Q**: ${q.question}`);
97
+ lines.push(`**Scenario**: ${q.question}`);
98
+ if (q.context) {
99
+ lines.push(`> ${q.context}`);
100
+ }
101
+ if (q.simulationPrompts && q.simulationPrompts.length > 0) {
102
+ lines.push("", "**Steer the simulation:**");
103
+ for (const prompt of q.simulationPrompts) {
104
+ lines.push(`- ${prompt}`);
105
+ }
106
+ }
55
107
  if (q.followUps.length > 0) {
108
+ lines.push("", "**Follow-ups:**");
56
109
  for (const followUp of q.followUps) {
57
110
  lines.push(` → ${followUp}`);
58
111
  }
59
112
  }
113
+ if (q.lookingFor && q.lookingFor.length > 0) {
114
+ lines.push("", "**What to look for:**");
115
+ for (const item of q.lookingFor) {
116
+ lines.push(`- ${item}`);
117
+ }
118
+ }
60
119
  lines.push("");
61
120
  }
62
121
  }