@forwardimpact/pathway 0.25.15 → 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.
@@ -111,6 +111,119 @@ function getSkillCapability(skillId, skills) {
111
111
  */
112
112
  const ROLE_TYPES = ["professionalQuestions", "managementQuestions"];
113
113
 
114
+ /**
115
+ * Check whether skill questions should be included given the current filter
116
+ * @param {string} skillId
117
+ * @param {Array} skills
118
+ * @param {QuestionsFilter} filter
119
+ * @returns {boolean}
120
+ */
121
+ function shouldIncludeSkill(skillId, skills, filter) {
122
+ if (filter.skills && !filter.skills.includes(skillId)) return false;
123
+ if (filter.behaviours) return false;
124
+ if (filter.maturity) return false;
125
+ if (filter.capability) {
126
+ const capability = getSkillCapability(skillId, skills);
127
+ if (capability !== filter.capability) return false;
128
+ }
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Check whether behaviour questions should be included given the current filter
134
+ * @param {string} behaviourId
135
+ * @param {QuestionsFilter} filter
136
+ * @returns {boolean}
137
+ */
138
+ function shouldIncludeBehaviour(behaviourId, filter) {
139
+ if (filter.behaviours && !filter.behaviours.includes(behaviourId))
140
+ return false;
141
+ if (filter.capability) return false;
142
+ if (filter.skills) return false;
143
+ if (filter.level) return false;
144
+ return true;
145
+ }
146
+
147
+ /**
148
+ * Flatten skill questions from a single skill entry
149
+ * @param {string} skillId
150
+ * @param {Object} roleTypes
151
+ * @param {Array} skills
152
+ * @param {QuestionsFilter} filter
153
+ * @param {FlattenedQuestion[]} out
154
+ */
155
+ function flattenSkillQuestions(skillId, roleTypes, skills, filter, out) {
156
+ const skillName = getSkillName(skillId, skills);
157
+
158
+ for (const roleType of ROLE_TYPES) {
159
+ const levels = roleTypes[roleType];
160
+ if (!levels) continue;
161
+
162
+ for (const [level, levelQuestions] of Object.entries(levels)) {
163
+ if (filter.level && level !== filter.level) continue;
164
+
165
+ for (const q of levelQuestions) {
166
+ out.push({
167
+ source: skillId,
168
+ sourceName: skillName,
169
+ sourceType: "skill",
170
+ level,
171
+ id: q.id,
172
+ text: q.text,
173
+ lookingFor: q.lookingFor || [],
174
+ expectedDurationMinutes: q.expectedDurationMinutes || 5,
175
+ followUps: q.followUps || [],
176
+ context: q.context || null,
177
+ decompositionPrompts: q.decompositionPrompts || [],
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Flatten behaviour questions from a single behaviour entry
186
+ * @param {string} behaviourId
187
+ * @param {Object} roleTypes
188
+ * @param {Array} behaviours
189
+ * @param {QuestionsFilter} filter
190
+ * @param {FlattenedQuestion[]} out
191
+ */
192
+ function flattenBehaviourQuestions(
193
+ behaviourId,
194
+ roleTypes,
195
+ behaviours,
196
+ filter,
197
+ out,
198
+ ) {
199
+ const behaviourName = getBehaviourName(behaviourId, behaviours);
200
+
201
+ for (const roleType of ROLE_TYPES) {
202
+ const maturities = roleTypes[roleType];
203
+ if (!maturities) continue;
204
+
205
+ for (const [maturity, maturityQuestions] of Object.entries(maturities)) {
206
+ if (filter.maturity && maturity !== filter.maturity) continue;
207
+
208
+ for (const q of maturityQuestions) {
209
+ out.push({
210
+ source: behaviourId,
211
+ sourceName: behaviourName,
212
+ sourceType: "behaviour",
213
+ level: maturity,
214
+ id: q.id,
215
+ text: q.text,
216
+ lookingFor: q.lookingFor || [],
217
+ expectedDurationMinutes: q.expectedDurationMinutes || 5,
218
+ followUps: q.followUps || [],
219
+ context: q.context || null,
220
+ simulationPrompts: q.simulationPrompts || [],
221
+ });
222
+ }
223
+ }
224
+ }
225
+ }
226
+
114
227
  /**
115
228
  * Flatten all questions from question bank
116
229
  * @param {Object} questionBank
@@ -122,84 +235,43 @@ const ROLE_TYPES = ["professionalQuestions", "managementQuestions"];
122
235
  export function flattenQuestions(questionBank, skills, behaviours, filter) {
123
236
  const questions = [];
124
237
 
125
- // Process skill questions
126
238
  for (const [skillId, roleTypes] of Object.entries(
127
239
  questionBank.skillProficiencies || {},
128
240
  )) {
129
- const skillName = getSkillName(skillId, skills);
130
- const capability = getSkillCapability(skillId, skills);
131
-
132
- if (filter.skills && !filter.skills.includes(skillId)) continue;
133
- if (filter.behaviours) continue;
134
- if (filter.capability && capability !== filter.capability) continue;
135
- if (filter.maturity) continue;
136
-
137
- for (const roleType of ROLE_TYPES) {
138
- const levels = roleTypes[roleType];
139
- if (!levels) continue;
140
-
141
- for (const [level, levelQuestions] of Object.entries(levels)) {
142
- if (filter.level && level !== filter.level) continue;
143
-
144
- for (const q of levelQuestions) {
145
- questions.push({
146
- source: skillId,
147
- sourceName: skillName,
148
- sourceType: "skill",
149
- level,
150
- id: q.id,
151
- text: q.text,
152
- lookingFor: q.lookingFor || [],
153
- expectedDurationMinutes: q.expectedDurationMinutes || 5,
154
- followUps: q.followUps || [],
155
- context: q.context || null,
156
- decompositionPrompts: q.decompositionPrompts || [],
157
- });
158
- }
159
- }
160
- }
241
+ if (!shouldIncludeSkill(skillId, skills, filter)) continue;
242
+ flattenSkillQuestions(skillId, roleTypes, skills, filter, questions);
161
243
  }
162
244
 
163
- // Process behaviour questions
164
245
  for (const [behaviourId, roleTypes] of Object.entries(
165
246
  questionBank.behaviourMaturities || {},
166
247
  )) {
167
- const behaviourName = getBehaviourName(behaviourId, behaviours);
168
-
169
- if (filter.behaviours && !filter.behaviours.includes(behaviourId)) continue;
170
- if (filter.capability) continue;
171
- if (filter.skills) continue;
172
- if (filter.level) continue;
173
-
174
- for (const roleType of ROLE_TYPES) {
175
- const maturities = roleTypes[roleType];
176
- if (!maturities) continue;
177
-
178
- for (const [maturity, maturityQuestions] of Object.entries(maturities)) {
179
- if (filter.maturity && maturity !== filter.maturity) continue;
180
-
181
- for (const q of maturityQuestions) {
182
- questions.push({
183
- source: behaviourId,
184
- sourceName: behaviourName,
185
- sourceType: "behaviour",
186
- level: maturity,
187
- id: q.id,
188
- text: q.text,
189
- lookingFor: q.lookingFor || [],
190
- expectedDurationMinutes: q.expectedDurationMinutes || 5,
191
- followUps: q.followUps || [],
192
- context: q.context || null,
193
- simulationPrompts: q.simulationPrompts || [],
194
- });
195
- }
196
- }
197
- }
248
+ if (!shouldIncludeBehaviour(behaviourId, filter)) continue;
249
+ flattenBehaviourQuestions(
250
+ behaviourId,
251
+ roleTypes,
252
+ behaviours,
253
+ filter,
254
+ questions,
255
+ );
198
256
  }
199
257
 
200
258
  return questions;
201
259
  }
202
260
 
261
+ /**
262
+ * Count questions across role types for a given level/maturity
263
+ * @param {Object} roleTypes - Role type map
264
+ * @param {string} levelKey - Level or maturity key
265
+ * @returns {number}
266
+ */
267
+ function countQuestionsForLevel(roleTypes, levelKey) {
268
+ let count = 0;
269
+ for (const roleType of ROLE_TYPES) {
270
+ count += (roleTypes[roleType]?.[levelKey] || []).length;
271
+ }
272
+ return count;
273
+ }
274
+
203
275
  /**
204
276
  * Calculate question statistics
205
277
  * @param {FlattenedQuestion[]} questions
@@ -222,11 +294,7 @@ export function calculateStats(questions, questionBank) {
222
294
  )) {
223
295
  skillStats[skillId] = {};
224
296
  for (const level of SKILL_PROFICIENCIES) {
225
- let count = 0;
226
- for (const roleType of ROLE_TYPES) {
227
- count += (roleTypes[roleType]?.[level] || []).length;
228
- }
229
- skillStats[skillId][level] = count;
297
+ skillStats[skillId][level] = countQuestionsForLevel(roleTypes, level);
230
298
  }
231
299
  skillStats[skillId].total = Object.values(skillStats[skillId]).reduce(
232
300
  (a, b) => a + b,
@@ -240,11 +308,10 @@ export function calculateStats(questions, questionBank) {
240
308
  )) {
241
309
  behaviourStats[behaviourId] = {};
242
310
  for (const maturity of BEHAVIOUR_MATURITIES) {
243
- let count = 0;
244
- for (const roleType of ROLE_TYPES) {
245
- count += (roleTypes[roleType]?.[maturity] || []).length;
246
- }
247
- behaviourStats[behaviourId][maturity] = count;
311
+ behaviourStats[behaviourId][maturity] = countQuestionsForLevel(
312
+ roleTypes,
313
+ maturity,
314
+ );
248
315
  }
249
316
  behaviourStats[behaviourId].total = Object.values(
250
317
  behaviourStats[behaviourId],
@@ -37,6 +37,106 @@ import { createJsonLdScript, skillToJsonLd } from "../json-ld.js";
37
37
  * @param {string} [context.agentSkillContent] - Pre-generated SKILL.md content for agent file viewer
38
38
  * @returns {HTMLElement}
39
39
  */
40
+ /**
41
+ * Create the required tools section
42
+ * @param {Object} view
43
+ * @param {boolean} showBackLink
44
+ * @returns {HTMLElement}
45
+ */
46
+ function createToolsSection(view, showBackLink) {
47
+ return div(
48
+ { className: "detail-section" },
49
+ heading2({ className: "section-title" }, "Required Tools"),
50
+ table(
51
+ { className: "tools-table" },
52
+ thead({}, tr({}, th({}, "Tool"), th({}, "Use When"))),
53
+ tbody(
54
+ {},
55
+ ...view.toolReferences.map((tool) =>
56
+ tr(
57
+ {},
58
+ td(
59
+ { className: "tool-name-cell" },
60
+ tool.simpleIcon
61
+ ? createToolIcon(tool.simpleIcon, tool.name)
62
+ : null,
63
+ tool.url
64
+ ? a(
65
+ {
66
+ href: tool.url,
67
+ target: "_blank",
68
+ rel: "noopener noreferrer",
69
+ },
70
+ tool.name,
71
+ span({ className: "external-icon" }, " ↗"),
72
+ )
73
+ : tool.name,
74
+ ),
75
+ td({}, tool.useWhen),
76
+ ),
77
+ ),
78
+ ),
79
+ ),
80
+ showBackLink
81
+ ? p(
82
+ { className: "see-all-link" },
83
+ a({ href: "#/tool" }, "See all tools →"),
84
+ )
85
+ : null,
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Create the related disciplines and drivers section
91
+ * @param {Object} view
92
+ * @param {boolean} showBackLink
93
+ * @returns {HTMLElement|null}
94
+ */
95
+ function createRelationsSection(view, showBackLink) {
96
+ if (
97
+ view.relatedDisciplines.length === 0 &&
98
+ view.relatedDrivers.length === 0
99
+ ) {
100
+ return null;
101
+ }
102
+ return div(
103
+ { className: "detail-section" },
104
+ div(
105
+ { className: "content-columns" },
106
+ view.relatedDisciplines.length > 0
107
+ ? div(
108
+ { className: "column" },
109
+ heading2({ className: "section-title" }, "Used in Disciplines"),
110
+ ...view.relatedDisciplines.map((d) =>
111
+ div(
112
+ { className: "list-item" },
113
+ showBackLink
114
+ ? a({ href: `#/discipline/${d.id}` }, d.name)
115
+ : span({}, d.name),
116
+ " ",
117
+ span({ className: `badge badge-${d.skillType}` }, d.skillType),
118
+ ),
119
+ ),
120
+ )
121
+ : null,
122
+ view.relatedDrivers.length > 0
123
+ ? div(
124
+ { className: "column" },
125
+ heading2({ className: "section-title" }, "Linked to Drivers"),
126
+ ...view.relatedDrivers.map((d) =>
127
+ div(
128
+ { className: "list-item" },
129
+ showBackLink
130
+ ? a({ href: `#/driver/${d.id}` }, d.name)
131
+ : span({}, d.name),
132
+ ),
133
+ ),
134
+ )
135
+ : null,
136
+ ),
137
+ );
138
+ }
139
+
40
140
  export function skillToDOM(
41
141
  skill,
42
142
  {
@@ -57,9 +157,7 @@ export function skillToDOM(
57
157
  });
58
158
  return div(
59
159
  { className: "detail-page skill-detail" },
60
- // JSON-LD structured data
61
160
  createJsonLdScript(skillToJsonLd(skill, { capabilities })),
62
- // Header
63
161
  div(
64
162
  { className: "page-header" },
65
163
  showBackLink ? createBackLink("/skill", "← Back to Skills") : null,
@@ -88,7 +186,6 @@ export function skillToDOM(
88
186
  p({ className: "page-description" }, view.description),
89
187
  ),
90
188
 
91
- // Level descriptions
92
189
  div(
93
190
  { className: "detail-section" },
94
191
  heading2({ className: "section-title" }, "Level Descriptions"),
@@ -109,55 +206,8 @@ export function skillToDOM(
109
206
  ),
110
207
  ),
111
208
 
112
- // Used in Disciplines and Linked to Drivers in two columns
113
- view.relatedDisciplines.length > 0 || view.relatedDrivers.length > 0
114
- ? div(
115
- { className: "detail-section" },
116
- div(
117
- { className: "content-columns" },
118
- // Used in Disciplines column
119
- view.relatedDisciplines.length > 0
120
- ? div(
121
- { className: "column" },
122
- heading2(
123
- { className: "section-title" },
124
- "Used in Disciplines",
125
- ),
126
- ...view.relatedDisciplines.map((d) =>
127
- div(
128
- { className: "list-item" },
129
- showBackLink
130
- ? a({ href: `#/discipline/${d.id}` }, d.name)
131
- : span({}, d.name),
132
- " ",
133
- span(
134
- { className: `badge badge-${d.skillType}` },
135
- d.skillType,
136
- ),
137
- ),
138
- ),
139
- )
140
- : null,
141
- // Linked to Drivers column
142
- view.relatedDrivers.length > 0
143
- ? div(
144
- { className: "column" },
145
- heading2({ className: "section-title" }, "Linked to Drivers"),
146
- ...view.relatedDrivers.map((d) =>
147
- div(
148
- { className: "list-item" },
149
- showBackLink
150
- ? a({ href: `#/driver/${d.id}` }, d.name)
151
- : span({}, d.name),
152
- ),
153
- ),
154
- )
155
- : null,
156
- ),
157
- )
158
- : null,
209
+ createRelationsSection(view, showBackLink),
159
210
 
160
- // Related tracks
161
211
  view.relatedTracks.length > 0
162
212
  ? div(
163
213
  { className: "detail-section" },
@@ -180,51 +230,10 @@ export function skillToDOM(
180
230
  )
181
231
  : null,
182
232
 
183
- // Required Tools
184
233
  showToolsAndPatterns && view.toolReferences.length > 0
185
- ? div(
186
- { className: "detail-section" },
187
- heading2({ className: "section-title" }, "Required Tools"),
188
- table(
189
- { className: "tools-table" },
190
- thead({}, tr({}, th({}, "Tool"), th({}, "Use When"))),
191
- tbody(
192
- {},
193
- ...view.toolReferences.map((tool) =>
194
- tr(
195
- {},
196
- td(
197
- { className: "tool-name-cell" },
198
- tool.simpleIcon
199
- ? createToolIcon(tool.simpleIcon, tool.name)
200
- : null,
201
- tool.url
202
- ? a(
203
- {
204
- href: tool.url,
205
- target: "_blank",
206
- rel: "noopener noreferrer",
207
- },
208
- tool.name,
209
- span({ className: "external-icon" }, " ↗"),
210
- )
211
- : tool.name,
212
- ),
213
- td({}, tool.useWhen),
214
- ),
215
- ),
216
- ),
217
- ),
218
- showBackLink
219
- ? p(
220
- { className: "see-all-link" },
221
- a({ href: "#/tool" }, "See all tools →"),
222
- )
223
- : null,
224
- )
234
+ ? createToolsSection(view, showBackLink)
225
235
  : null,
226
236
 
227
- // Agent Skill Files
228
237
  showToolsAndPatterns &&
229
238
  (agentSkillContent || view.implementationReference || view.installScript)
230
239
  ? div(