@forwardimpact/pathway 0.25.12 → 0.25.20

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.
@@ -47,8 +47,8 @@ export async function runInitCommand({ options }) {
47
47
 
48
48
  Next steps:
49
49
  1. Edit data files to match your organization
50
- 2. bunx fit-map validate
51
- 3. bunx fit-pathway dev
50
+ 2. npx fit-map validate
51
+ 3. npx fit-pathway dev
52
52
 
53
53
  Data structure:
54
54
  data/pathway/
@@ -39,137 +39,103 @@ function formatJob(view, _options, entities, jobTemplate) {
39
39
  }
40
40
 
41
41
  /**
42
- * Run job command
43
- * @param {Object} params
44
- * @param {Object} params.data - All loaded data
45
- * @param {string[]} params.args - Command arguments
46
- * @param {Object} params.options - Command options
47
- * @param {string} params.dataDir - Path to data directory
42
+ * Print job list output
43
+ * @param {Array} filteredJobs
48
44
  */
49
- export async function runJobCommand({
50
- data,
51
- args,
52
- options,
53
- dataDir,
54
- templateLoader,
55
- }) {
56
- const jobs = generateAllJobs({
57
- disciplines: data.disciplines,
58
- levels: data.levels,
59
- tracks: data.tracks,
60
- skills: data.skills,
61
- behaviours: data.behaviours,
62
- validationRules: data.framework.validationRules,
63
- });
64
-
65
- // Apply --track filter to list and summary modes
66
- const filteredJobs = options.track
67
- ? jobs.filter((j) => j.track && j.track.id === options.track)
68
- : jobs;
69
-
70
- if (options.track && filteredJobs.length === 0 && args.length === 0) {
71
- const trackExists = data.tracks.some((t) => t.id === options.track);
72
- if (!trackExists) {
73
- console.error(`Track not found: ${options.track}`);
74
- console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
45
+ function printJobList(filteredJobs) {
46
+ for (const job of filteredJobs) {
47
+ const title = generateJobTitle(job.discipline, job.level, job.track);
48
+ if (job.track) {
49
+ console.log(
50
+ `${job.discipline.id} ${job.level.id} ${job.track.id}, ${title}`,
51
+ );
75
52
  } else {
76
- console.error(`No jobs found for track: ${options.track}`);
77
- const trackDisciplines = data.disciplines
78
- .filter((d) => d.validTracks && d.validTracks.includes(options.track))
79
- .map((d) => d.id);
80
- if (trackDisciplines.length > 0) {
81
- console.error(
82
- `Disciplines with this track: ${trackDisciplines.join(", ")}`,
83
- );
84
- }
53
+ console.log(`${job.discipline.id} ${job.level.id}, ${title}`);
85
54
  }
86
- process.exit(1);
87
55
  }
56
+ }
88
57
 
89
- // --list: Output descriptive comma-separated lines for piping and AI agent discovery
90
- if (options.list) {
91
- for (const job of filteredJobs) {
92
- const title = generateJobTitle(job.discipline, job.level, job.track);
93
- if (job.track) {
94
- console.log(
95
- `${job.discipline.id} ${job.level.id} ${job.track.id}, ${title}`,
96
- );
97
- } else {
98
- console.log(`${job.discipline.id} ${job.level.id}, ${title}`);
99
- }
58
+ /**
59
+ * Print job summary table
60
+ * @param {Array} filteredJobs
61
+ * @param {Object} options
62
+ */
63
+ function printJobSummary(filteredJobs, options) {
64
+ const trackLabel = options.track ? ` — ${options.track}` : "";
65
+ console.log(`\nšŸ’¼ Jobs${trackLabel}\n`);
66
+
67
+ const byDiscipline = {};
68
+ for (const job of filteredJobs) {
69
+ const key = job.discipline.id;
70
+ if (!byDiscipline[key]) {
71
+ byDiscipline[key] = {
72
+ name: job.discipline.specialization || job.discipline.id,
73
+ roleTitle: job.discipline.roleTitle || job.discipline.id,
74
+ type: job.discipline.isProfessional ? "Professional" : "Management",
75
+ tracks: new Set(),
76
+ count: 0,
77
+ };
100
78
  }
101
- return;
79
+ if (job.track) byDiscipline[key].tracks.add(job.track.id);
80
+ byDiscipline[key].count++;
102
81
  }
103
82
 
104
- // No args: Show summary
105
- if (args.length === 0) {
106
- const trackLabel = options.track ? ` — ${options.track}` : "";
107
- console.log(`\nšŸ’¼ Jobs${trackLabel}\n`);
108
-
109
- // Count by discipline with name, grouped by track
110
- const byDiscipline = {};
111
- for (const job of filteredJobs) {
112
- const key = job.discipline.id;
113
- if (!byDiscipline[key]) {
114
- byDiscipline[key] = {
115
- name: job.discipline.specialization || job.discipline.id,
116
- roleTitle: job.discipline.roleTitle || job.discipline.id,
117
- type: job.discipline.isProfessional ? "Professional" : "Management",
118
- tracks: new Set(),
119
- count: 0,
120
- };
121
- }
122
- if (job.track) byDiscipline[key].tracks.add(job.track.id);
123
- byDiscipline[key].count++;
124
- }
83
+ const rows = Object.entries(byDiscipline).map(([id, info]) => [
84
+ id,
85
+ info.name,
86
+ info.type,
87
+ info.count,
88
+ info.tracks.size > 0 ? [...info.tracks].join(", ") : "—",
89
+ ]);
90
+ console.log(
91
+ formatTable(["ID", "Specialization", "Type", "Jobs", "Tracks"], rows),
92
+ );
93
+ console.log(`\nTotal: ${filteredJobs.length} valid job combinations`);
94
+ console.log(
95
+ `\nRun 'bunx pathway job --list' for all combinations with titles`,
96
+ );
97
+ console.log(
98
+ `Run 'bunx pathway job <discipline> <level> [--track=<track>]' for details\n`,
99
+ );
100
+ }
125
101
 
126
- const rows = Object.entries(byDiscipline).map(([id, info]) => [
127
- id,
128
- info.name,
129
- info.type,
130
- info.count,
131
- info.tracks.size > 0 ? [...info.tracks].join(", ") : "—",
132
- ]);
133
- console.log(
134
- formatTable(["ID", "Specialization", "Type", "Jobs", "Tracks"], rows),
102
+ /**
103
+ * Validate and exit when a single positional arg is provided
104
+ * @param {string} arg
105
+ * @param {Object} data
106
+ */
107
+ function handleSingleArg(arg, data) {
108
+ const isLevel = data.levels.some((g) => g.id === arg);
109
+ const isTrack = data.tracks.some((t) => t.id === arg);
110
+ if (isLevel) {
111
+ console.error(
112
+ `Missing discipline. Usage: bunx pathway job <discipline> ${arg} [--track=<track>]`,
135
113
  );
136
- console.log(`\nTotal: ${filteredJobs.length} valid job combinations`);
137
- console.log(
138
- `\nRun 'bunx pathway job --list' for all combinations with titles`,
114
+ console.error(
115
+ `Disciplines: ${data.disciplines.map((d) => d.id).join(", ")}`,
139
116
  );
140
- console.log(
141
- `Run 'bunx pathway job <discipline> <level> [--track=<track>]' for details\n`,
117
+ } else if (isTrack) {
118
+ console.error(`Track must be passed as a flag: --track=${arg}`);
119
+ console.error(
120
+ `Usage: bunx pathway job <discipline> <level> --track=${arg}`,
142
121
  );
143
- return;
144
- }
145
-
146
- // Handle job detail view - requires discipline and level
147
- if (args.length < 2) {
148
- // Check if the single arg is a level or track, hinting at what's missing
149
- const arg = args[0];
150
- const isLevel = data.levels.some((g) => g.id === arg);
151
- const isTrack = data.tracks.some((t) => t.id === arg);
152
- if (isLevel) {
153
- console.error(
154
- `Missing discipline. Usage: bunx pathway job <discipline> ${arg} [--track=<track>]`,
155
- );
156
- console.error(
157
- `Disciplines: ${data.disciplines.map((d) => d.id).join(", ")}`,
158
- );
159
- } else if (isTrack) {
160
- console.error(`Track must be passed as a flag: --track=${arg}`);
161
- console.error(
162
- `Usage: bunx pathway job <discipline> <level> --track=${arg}`,
163
- );
164
- } else {
165
- console.error(
166
- "Usage: bunx pathway job <discipline> <level> [--track=<track>]",
167
- );
168
- console.error(" bunx pathway job --list");
169
- }
170
- process.exit(1);
122
+ } else {
123
+ console.error(
124
+ "Usage: bunx pathway job <discipline> <level> [--track=<track>]",
125
+ );
126
+ console.error(" bunx pathway job --list");
171
127
  }
128
+ process.exit(1);
129
+ }
172
130
 
131
+ /**
132
+ * Resolve and validate discipline, level, track entities from args
133
+ * @param {Object} data
134
+ * @param {string[]} args
135
+ * @param {Object} options
136
+ * @returns {{discipline: Object, level: Object, track: Object|null}}
137
+ */
138
+ function resolveJobEntities(data, args, options) {
173
139
  const discipline = data.disciplines.find((d) => d.id === args[0]);
174
140
  const level = data.levels.find((g) => g.id === args[1]);
175
141
  const track = options.track
@@ -177,7 +143,6 @@ export async function runJobCommand({
177
143
  : null;
178
144
 
179
145
  if (!discipline) {
180
- // Check if args are swapped (level first, discipline second)
181
146
  const maybeLevel = data.levels.find((g) => g.id === args[0]);
182
147
  const maybeDiscipline = data.disciplines.find((d) => d.id === args[1]);
183
148
  if (maybeLevel && maybeDiscipline) {
@@ -195,7 +160,6 @@ export async function runJobCommand({
195
160
  }
196
161
 
197
162
  if (!level) {
198
- // Check if the second arg is a track ID passed as positional
199
163
  const isTrack = data.tracks.some((t) => t.id === args[1]);
200
164
  if (isTrack) {
201
165
  console.error(
@@ -216,6 +180,158 @@ export async function runJobCommand({
216
180
  process.exit(1);
217
181
  }
218
182
 
183
+ return { discipline, level, track };
184
+ }
185
+
186
+ /**
187
+ * Handle --checklist sub-command
188
+ * @param {Object} view
189
+ * @param {Object} data
190
+ * @param {string} stageId
191
+ */
192
+ function handleChecklist(view, data, stageId) {
193
+ const validStages = data.stages.map((s) => s.id);
194
+ if (!validStages.includes(stageId)) {
195
+ console.error(`Invalid stage: ${stageId}`);
196
+ console.error(`Available: ${validStages.join(", ")}`);
197
+ process.exit(1);
198
+ }
199
+
200
+ const { readChecklist, confirmChecklist } = deriveChecklist({
201
+ stageId,
202
+ skillMatrix: view.skillMatrix,
203
+ skills: data.skills,
204
+ capabilities: data.capabilities,
205
+ });
206
+
207
+ if (readChecklist.length === 0 && confirmChecklist.length === 0) {
208
+ console.log(`\nNo checklist items for ${stageId} stage\n`);
209
+ return;
210
+ }
211
+
212
+ const stageLabel = stageId.charAt(0).toUpperCase() + stageId.slice(1);
213
+ console.log(`\n# ${view.title} — ${stageLabel} Stage Checklist\n`);
214
+ if (readChecklist.length > 0) {
215
+ console.log("## Read-Then-Do\n");
216
+ console.log(formatChecklistMarkdown(readChecklist));
217
+ console.log("");
218
+ }
219
+ if (confirmChecklist.length > 0) {
220
+ console.log("## Do-Then-Confirm\n");
221
+ console.log(formatChecklistMarkdown(confirmChecklist));
222
+ console.log("");
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Run job command
228
+ * @param {Object} params
229
+ * @param {Object} params.data - All loaded data
230
+ * @param {string[]} params.args - Command arguments
231
+ * @param {Object} params.options - Command options
232
+ * @param {string} params.dataDir - Path to data directory
233
+ */
234
+ /**
235
+ * Validate track filter and exit if invalid
236
+ * @param {Array} filteredJobs
237
+ * @param {Object} data
238
+ * @param {Object} options
239
+ */
240
+ function validateTrackFilter(filteredJobs, data, options) {
241
+ if (!options.track || filteredJobs.length > 0) return;
242
+ const trackExists = data.tracks.some((t) => t.id === options.track);
243
+ if (!trackExists) {
244
+ console.error(`Track not found: ${options.track}`);
245
+ console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
246
+ } else {
247
+ console.error(`No jobs found for track: ${options.track}`);
248
+ const trackDisciplines = data.disciplines
249
+ .filter((d) => d.validTracks && d.validTracks.includes(options.track))
250
+ .map((d) => d.id);
251
+ if (trackDisciplines.length > 0) {
252
+ console.error(
253
+ `Disciplines with this track: ${trackDisciplines.join(", ")}`,
254
+ );
255
+ }
256
+ }
257
+ process.exit(1);
258
+ }
259
+
260
+ /**
261
+ * Report an invalid job combination and exit
262
+ * @param {Object} discipline
263
+ * @param {Object} level
264
+ * @param {Object|null} track
265
+ * @param {Object} data
266
+ */
267
+ function reportInvalidCombination(discipline, level, track, data) {
268
+ const combo = track
269
+ ? `${discipline.id} Ɨ ${level.id} Ɨ ${track.id}`
270
+ : `${discipline.id} Ɨ ${level.id}`;
271
+ console.error(`Invalid combination: ${combo}`);
272
+ if (track) {
273
+ const validTracks = discipline.validTracks?.filter((t) => t !== null) || [];
274
+ if (validTracks.length > 0) {
275
+ console.error(
276
+ `Valid tracks for ${discipline.id}: ${validTracks.join(", ")}`,
277
+ );
278
+ } else {
279
+ console.error(`${discipline.id} does not support tracks`);
280
+ }
281
+ }
282
+ if (discipline.minLevel) {
283
+ const levelIndex = data.levels.findIndex((g) => g.id === level.id);
284
+ const minIndex = data.levels.findIndex((g) => g.id === discipline.minLevel);
285
+ if (levelIndex >= 0 && minIndex >= 0 && levelIndex < minIndex) {
286
+ console.error(
287
+ `${discipline.id} requires minimum level: ${discipline.minLevel}`,
288
+ );
289
+ }
290
+ }
291
+ process.exit(1);
292
+ }
293
+
294
+ export async function runJobCommand({
295
+ data,
296
+ args,
297
+ options,
298
+ dataDir,
299
+ templateLoader,
300
+ }) {
301
+ const jobs = generateAllJobs({
302
+ disciplines: data.disciplines,
303
+ levels: data.levels,
304
+ tracks: data.tracks,
305
+ skills: data.skills,
306
+ behaviours: data.behaviours,
307
+ validationRules: data.framework.validationRules,
308
+ });
309
+
310
+ const filteredJobs = options.track
311
+ ? jobs.filter((j) => j.track && j.track.id === options.track)
312
+ : jobs;
313
+
314
+ if (args.length === 0 && filteredJobs.length === 0) {
315
+ validateTrackFilter(filteredJobs, data, options);
316
+ }
317
+
318
+ if (options.list) {
319
+ printJobList(filteredJobs);
320
+ return;
321
+ }
322
+
323
+ if (args.length === 0) {
324
+ printJobSummary(filteredJobs, options);
325
+ return;
326
+ }
327
+
328
+ if (args.length < 2) {
329
+ handleSingleArg(args[0], data);
330
+ return;
331
+ }
332
+
333
+ const { discipline, level, track } = resolveJobEntities(data, args, options);
334
+
219
335
  const view = prepareJobDetail({
220
336
  discipline,
221
337
  level,
@@ -228,37 +344,9 @@ export async function runJobCommand({
228
344
  });
229
345
 
230
346
  if (!view) {
231
- const combo = track
232
- ? `${discipline.id} Ɨ ${level.id} Ɨ ${track.id}`
233
- : `${discipline.id} Ɨ ${level.id}`;
234
- console.error(`Invalid combination: ${combo}`);
235
- if (track) {
236
- const validTracks =
237
- discipline.validTracks?.filter((t) => t !== null) || [];
238
- if (validTracks.length > 0) {
239
- console.error(
240
- `Valid tracks for ${discipline.id}: ${validTracks.join(", ")}`,
241
- );
242
- } else {
243
- console.error(`${discipline.id} does not support tracks`);
244
- }
245
- }
246
- // Check if it's a minLevel issue
247
- if (discipline.minLevel) {
248
- const levelIndex = data.levels.findIndex((g) => g.id === level.id);
249
- const minIndex = data.levels.findIndex(
250
- (g) => g.id === discipline.minLevel,
251
- );
252
- if (levelIndex >= 0 && minIndex >= 0 && levelIndex < minIndex) {
253
- console.error(
254
- `${discipline.id} requires minimum level: ${discipline.minLevel}`,
255
- );
256
- }
257
- }
258
- process.exit(1);
347
+ reportInvalidCombination(discipline, level, track, data);
259
348
  }
260
349
 
261
- // --skills: Output plain list of skill IDs (for piping)
262
350
  if (options.skills) {
263
351
  for (const skill of view.skillMatrix) {
264
352
  console.log(skill.skillId);
@@ -266,7 +354,6 @@ export async function runJobCommand({
266
354
  return;
267
355
  }
268
356
 
269
- // --tools: Output plain list of tool names (for piping)
270
357
  if (options.tools) {
271
358
  console.log(toolkitToPlainList(view.toolkit));
272
359
  return;
@@ -277,44 +364,11 @@ export async function runJobCommand({
277
364
  return;
278
365
  }
279
366
 
280
- // --checklist: Show checklist for a specific stage
281
367
  if (options.checklist) {
282
- const validStages = data.stages.map((s) => s.id);
283
- if (!validStages.includes(options.checklist)) {
284
- console.error(`Invalid stage: ${options.checklist}`);
285
- console.error(`Available: ${validStages.join(", ")}`);
286
- process.exit(1);
287
- }
288
-
289
- const { readChecklist, confirmChecklist } = deriveChecklist({
290
- stageId: options.checklist,
291
- skillMatrix: view.skillMatrix,
292
- skills: data.skills,
293
- capabilities: data.capabilities,
294
- });
295
-
296
- if (readChecklist.length === 0 && confirmChecklist.length === 0) {
297
- console.log(`\nNo checklist items for ${options.checklist} stage\n`);
298
- return;
299
- }
300
-
301
- const stageLabel =
302
- options.checklist.charAt(0).toUpperCase() + options.checklist.slice(1);
303
- console.log(`\n# ${view.title} — ${stageLabel} Stage Checklist\n`);
304
- if (readChecklist.length > 0) {
305
- console.log("## Read-Then-Do\n");
306
- console.log(formatChecklistMarkdown(readChecklist));
307
- console.log("");
308
- }
309
- if (confirmChecklist.length > 0) {
310
- console.log("## Do-Then-Confirm\n");
311
- console.log(formatChecklistMarkdown(confirmChecklist));
312
- console.log("");
313
- }
368
+ handleChecklist(view, data, options.checklist);
314
369
  return;
315
370
  }
316
371
 
317
- // Load job template for description formatting
318
372
  const jobTemplate = templateLoader.load("job.template.md", dataDir);
319
373
  formatJob(view, options, { discipline, level, track }, jobTemplate);
320
374
  }