@forwardimpact/pathway 0.1.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 (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. package/package.json +68 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Questions Markdown Formatter
3
+ *
4
+ * Formats questions for terminal output as tables and lists.
5
+ */
6
+
7
+ import { SKILL_LEVELS, BEHAVIOUR_MATURITIES } from "./shared.js";
8
+
9
+ /**
10
+ * Level abbreviations for compact display
11
+ */
12
+ const LEVEL_ABBREVS = {
13
+ awareness: "aware",
14
+ foundational: "found",
15
+ working: "work",
16
+ practitioner: "pract",
17
+ expert: "expert",
18
+ };
19
+
20
+ /**
21
+ * Maturity abbreviations for compact display
22
+ */
23
+ const MATURITY_ABBREVS = {
24
+ emerging: "emerg",
25
+ developing: "dev",
26
+ practicing: "pract",
27
+ role_modeling: "role",
28
+ exemplifying: "exemp",
29
+ };
30
+
31
+ /**
32
+ * Truncate text to max length with ellipsis
33
+ * @param {string} text
34
+ * @param {number} maxLen
35
+ * @returns {string}
36
+ */
37
+ function truncate(text, maxLen) {
38
+ if (text.length <= maxLen) return text;
39
+ return text.slice(0, maxLen - 1) + "…";
40
+ }
41
+
42
+ /**
43
+ * Pad string to width
44
+ * @param {string} str
45
+ * @param {number} width
46
+ * @returns {string}
47
+ */
48
+ function pad(str, width) {
49
+ return str.padEnd(width);
50
+ }
51
+
52
+ /**
53
+ * Format stats-only output
54
+ * @param {Object} view - Questions view
55
+ * @param {Array} skills - Skills data
56
+ * @returns {string}
57
+ */
58
+ function formatStats(view, skills) {
59
+ const lines = [];
60
+ const { stats } = view;
61
+
62
+ // Skill question counts
63
+ lines.push("SKILL QUESTION COUNTS");
64
+ lines.push("═".repeat(75));
65
+ lines.push("");
66
+
67
+ // Header
68
+ const skillHeader =
69
+ pad("Skill", 30) +
70
+ SKILL_LEVELS.map((l) => pad(LEVEL_ABBREVS[l], 7)).join("") +
71
+ "TOTAL";
72
+ lines.push(skillHeader);
73
+ lines.push("─".repeat(75));
74
+
75
+ // Rows
76
+ const sortedSkillIds = Object.keys(stats.skillStats).sort();
77
+ let skillTotal = 0;
78
+ const levelTotals = {};
79
+
80
+ for (const skillId of sortedSkillIds) {
81
+ const skillData = stats.skillStats[skillId];
82
+ const skill = skills.find((s) => s.id === skillId);
83
+ const name = skill ? truncate(skill.name, 28) : skillId;
84
+
85
+ const row =
86
+ pad(name, 30) +
87
+ SKILL_LEVELS.map((l) => {
88
+ const count = skillData[l] || 0;
89
+ levelTotals[l] = (levelTotals[l] || 0) + count;
90
+ return pad(String(count), 7);
91
+ }).join("") +
92
+ String(skillData.total || 0);
93
+
94
+ lines.push(row);
95
+ skillTotal += skillData.total || 0;
96
+ }
97
+
98
+ lines.push("─".repeat(75));
99
+ const totalsRow =
100
+ pad("TOTAL", 30) +
101
+ SKILL_LEVELS.map((l) => pad(String(levelTotals[l] || 0), 7)).join("") +
102
+ String(skillTotal);
103
+ lines.push(totalsRow);
104
+ lines.push("");
105
+
106
+ // Behaviour question counts
107
+ lines.push("BEHAVIOUR QUESTION COUNTS");
108
+ lines.push("═".repeat(75));
109
+ lines.push("");
110
+
111
+ const behaviourHeader =
112
+ pad("Behaviour", 30) +
113
+ BEHAVIOUR_MATURITIES.map((m) => pad(MATURITY_ABBREVS[m], 7)).join("") +
114
+ "TOTAL";
115
+ lines.push(behaviourHeader);
116
+ lines.push("─".repeat(75));
117
+
118
+ const sortedBehaviourIds = Object.keys(stats.behaviourStats).sort();
119
+ let behaviourTotal = 0;
120
+ const maturityTotals = {};
121
+
122
+ for (const behaviourId of sortedBehaviourIds) {
123
+ const behaviourData = stats.behaviourStats[behaviourId];
124
+ const name = truncate(behaviourId.replace(/_/g, " "), 28);
125
+
126
+ const row =
127
+ pad(name, 30) +
128
+ BEHAVIOUR_MATURITIES.map((m) => {
129
+ const count = behaviourData[m] || 0;
130
+ maturityTotals[m] = (maturityTotals[m] || 0) + count;
131
+ return pad(String(count), 7);
132
+ }).join("") +
133
+ String(behaviourData.total || 0);
134
+
135
+ lines.push(row);
136
+ behaviourTotal += behaviourData.total || 0;
137
+ }
138
+
139
+ lines.push("─".repeat(75));
140
+ const bTotalsRow =
141
+ pad("TOTAL", 30) +
142
+ BEHAVIOUR_MATURITIES.map((m) =>
143
+ pad(String(maturityTotals[m] || 0), 7),
144
+ ).join("") +
145
+ String(behaviourTotal);
146
+ lines.push(bTotalsRow);
147
+ lines.push("");
148
+
149
+ // Identify gaps (skills with < 2 questions per level)
150
+ const gaps = [];
151
+ for (const skillId of sortedSkillIds) {
152
+ const skillData = stats.skillStats[skillId];
153
+ for (const level of SKILL_LEVELS) {
154
+ if ((skillData[level] || 0) < 1) {
155
+ gaps.push(`${skillId}: missing ${level} questions`);
156
+ }
157
+ }
158
+ }
159
+
160
+ if (gaps.length > 0) {
161
+ lines.push("⚠️ GAPS:");
162
+ for (const gap of gaps.slice(0, 10)) {
163
+ lines.push(` - ${gap}`);
164
+ }
165
+ if (gaps.length > 10) {
166
+ lines.push(` ... and ${gaps.length - 10} more`);
167
+ }
168
+ }
169
+
170
+ return lines.join("\n");
171
+ }
172
+
173
+ /**
174
+ * Format table output for questions at a level/maturity
175
+ * @param {Object} view - Questions view
176
+ * @returns {string}
177
+ */
178
+ function formatTable(view) {
179
+ const lines = [];
180
+ const { filter, questions, stats } = view;
181
+
182
+ // Header
183
+ const levelOrMaturity = filter.level || filter.maturity || "ALL";
184
+ const sourceType = filter.level
185
+ ? "skills"
186
+ : filter.maturity
187
+ ? "behaviours"
188
+ : "sources";
189
+ lines.push(
190
+ `${levelOrMaturity.toUpperCase()} LEVEL QUESTIONS (${stats.totalQuestions} from ${Object.keys(stats.bySource).length} ${sourceType})`,
191
+ );
192
+ lines.push("═".repeat(80));
193
+ lines.push("");
194
+
195
+ // Table header
196
+ lines.push(pad("Source", 28) + " │ " + pad("Question", 45) + " │ Min");
197
+ lines.push("─".repeat(28) + "─┼─" + "─".repeat(45) + "─┼─" + "───");
198
+
199
+ // Group by source
200
+ const bySource = {};
201
+ for (const q of questions) {
202
+ if (!bySource[q.source]) {
203
+ bySource[q.source] = { name: q.sourceName, questions: [] };
204
+ }
205
+ bySource[q.source].questions.push(q);
206
+ }
207
+
208
+ for (const data of Object.values(bySource).sort((a, b) =>
209
+ a.name.localeCompare(b.name),
210
+ )) {
211
+ for (const q of data.questions) {
212
+ const source = truncate(data.name, 26);
213
+ const text = truncate(q.text, 43);
214
+ const mins = String(q.expectedDurationMinutes);
215
+ lines.push(pad(source, 28) + " │ " + pad(text, 45) + " │ " + mins);
216
+ }
217
+ }
218
+
219
+ lines.push("");
220
+ return lines.join("\n");
221
+ }
222
+
223
+ /**
224
+ * Format single source deep dive
225
+ * @param {Object} view - Questions view
226
+ * @returns {string}
227
+ */
228
+ function formatSingleSource(view) {
229
+ const lines = [];
230
+ const { questions, stats } = view;
231
+
232
+ if (questions.length === 0) {
233
+ return "No questions found.";
234
+ }
235
+
236
+ const sourceName = questions[0].sourceName;
237
+ const sourceType = questions[0].sourceType;
238
+
239
+ lines.push(`${sourceName} QUESTIONS (${stats.totalQuestions} total)`);
240
+ lines.push("═".repeat(60));
241
+ lines.push("");
242
+
243
+ // Group by level/maturity
244
+ const byLevel = {};
245
+ for (const q of questions) {
246
+ if (!byLevel[q.level]) {
247
+ byLevel[q.level] = [];
248
+ }
249
+ byLevel[q.level].push(q);
250
+ }
251
+
252
+ const orderedLevels =
253
+ sourceType === "skill" ? SKILL_LEVELS : BEHAVIOUR_MATURITIES;
254
+
255
+ for (const level of orderedLevels) {
256
+ if (!byLevel[level]) continue;
257
+
258
+ lines.push(level.toUpperCase());
259
+ for (const q of byLevel[level]) {
260
+ lines.push(` • [${q.id}] ${q.text}`);
261
+ lines.push(` Duration: ${q.expectedDurationMinutes} min`);
262
+ if (q.lookingFor.length > 0) {
263
+ lines.push(" Looking for:");
264
+ for (const item of q.lookingFor) {
265
+ lines.push(` - ${item}`);
266
+ }
267
+ }
268
+ if (q.followUps.length > 0) {
269
+ lines.push(" Follow-ups:");
270
+ for (const fu of q.followUps) {
271
+ lines.push(` → ${fu}`);
272
+ }
273
+ }
274
+ lines.push("");
275
+ }
276
+ }
277
+
278
+ return lines.join("\n");
279
+ }
280
+
281
+ /**
282
+ * Format questions as markdown for terminal
283
+ * @param {Object} view - Questions view from prepareQuestionsView
284
+ * @param {Object} options - Format options
285
+ * @param {boolean} options.stats - Stats only
286
+ * @param {Array} options.skills - Skills data for name resolution
287
+ * @returns {string}
288
+ */
289
+ export function questionsToMarkdown(view, options = {}) {
290
+ // Stats only mode
291
+ if (options.stats) {
292
+ return formatStats(view, options.skills || []);
293
+ }
294
+
295
+ // Single source deep dive
296
+ const uniqueSources = new Set(view.questions.map((q) => q.source));
297
+ if (uniqueSources.size === 1) {
298
+ return formatSingleSource(view);
299
+ }
300
+
301
+ // Table format (default)
302
+ return formatTable(view);
303
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Questions presentation helpers
3
+ *
4
+ * Shared utilities for formatting question data across output formats.
5
+ */
6
+
7
+ /**
8
+ * Skill levels in order
9
+ */
10
+ export const SKILL_LEVELS = [
11
+ "awareness",
12
+ "foundational",
13
+ "working",
14
+ "practitioner",
15
+ "expert",
16
+ ];
17
+
18
+ /**
19
+ * Behaviour maturities in order
20
+ */
21
+ export const BEHAVIOUR_MATURITIES = [
22
+ "emerging",
23
+ "developing",
24
+ "practicing",
25
+ "role_modeling",
26
+ "exemplifying",
27
+ ];
28
+
29
+ /**
30
+ * @typedef {Object} QuestionsFilter
31
+ * @property {string|null} level - Skill level filter
32
+ * @property {string|null} maturity - Behaviour maturity filter
33
+ * @property {string[]|null} skills - Skill IDs to include
34
+ * @property {string[]|null} behaviours - Behaviour IDs to include
35
+ * @property {string|null} capability - Capability filter for skills
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} FlattenedQuestion
40
+ * @property {string} source - Source skill/behaviour ID
41
+ * @property {string} sourceName - Source skill/behaviour name
42
+ * @property {string} sourceType - 'skill' or 'behaviour'
43
+ * @property {string} level - Skill level or behaviour maturity
44
+ * @property {string} id - Question ID
45
+ * @property {string} text - Question text
46
+ * @property {string[]} lookingFor - Expected answer indicators
47
+ * @property {number} expectedDurationMinutes - Time estimate
48
+ * @property {string[]} [followUps] - Follow-up questions
49
+ */
50
+
51
+ /**
52
+ * @typedef {Object} QuestionsView
53
+ * @property {QuestionsFilter} filter - Applied filters
54
+ * @property {FlattenedQuestion[]} questions - Flattened questions
55
+ * @property {Object} stats - Question statistics
56
+ */
57
+
58
+ /**
59
+ * Parse filter options from CLI args
60
+ * @param {Object} options - CLI options
61
+ * @returns {QuestionsFilter}
62
+ */
63
+ export function parseFilters(options) {
64
+ return {
65
+ level: options.level || null,
66
+ maturity: options.maturity || null,
67
+ skills: options.skill ? options.skill.split(",") : null,
68
+ behaviours: options.behaviour ? options.behaviour.split(",") : null,
69
+ capability: options.capability || null,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Get skill name by ID
75
+ * @param {string} skillId
76
+ * @param {Array} skills
77
+ * @returns {string}
78
+ */
79
+ function getSkillName(skillId, skills) {
80
+ const skill = skills.find((s) => s.id === skillId);
81
+ return skill ? skill.name : skillId;
82
+ }
83
+
84
+ /**
85
+ * Get behaviour name by ID
86
+ * @param {string} behaviourId
87
+ * @param {Array} behaviours
88
+ * @returns {string}
89
+ */
90
+ function getBehaviourName(behaviourId, behaviours) {
91
+ const behaviour = behaviours.find((b) => b.id === behaviourId);
92
+ return behaviour ? behaviour.name : behaviourId;
93
+ }
94
+
95
+ /**
96
+ * Get skill capability by ID
97
+ * @param {string} skillId
98
+ * @param {Array} skills
99
+ * @returns {string|null}
100
+ */
101
+ function getSkillCapability(skillId, skills) {
102
+ const skill = skills.find((s) => s.id === skillId);
103
+ return skill ? skill.capability : null;
104
+ }
105
+
106
+ /**
107
+ * Flatten all questions from question bank
108
+ * @param {Object} questionBank
109
+ * @param {Array} skills
110
+ * @param {Array} behaviours
111
+ * @param {QuestionsFilter} filter
112
+ * @returns {FlattenedQuestion[]}
113
+ */
114
+ export function flattenQuestions(questionBank, skills, behaviours, filter) {
115
+ const questions = [];
116
+
117
+ // Process skill questions
118
+ for (const [skillId, levels] of Object.entries(
119
+ questionBank.skillLevels || {},
120
+ )) {
121
+ const skillName = getSkillName(skillId, skills);
122
+ const capability = getSkillCapability(skillId, skills);
123
+
124
+ // Filter by skill IDs
125
+ if (filter.skills && !filter.skills.includes(skillId)) continue;
126
+
127
+ // Skip skills if filtering by specific behaviours
128
+ if (filter.behaviours) continue;
129
+
130
+ // Filter by capability
131
+ if (filter.capability && capability !== filter.capability) continue;
132
+
133
+ for (const [level, levelQuestions] of Object.entries(levels)) {
134
+ // Filter by level
135
+ if (filter.level && level !== filter.level) continue;
136
+
137
+ // Skip if filtering by maturity (behaviour-only filter)
138
+ if (filter.maturity) continue;
139
+
140
+ for (const q of levelQuestions) {
141
+ questions.push({
142
+ source: skillId,
143
+ sourceName: skillName,
144
+ sourceType: "skill",
145
+ level,
146
+ id: q.id,
147
+ text: q.text,
148
+ lookingFor: q.lookingFor || [],
149
+ expectedDurationMinutes: q.expectedDurationMinutes || 5,
150
+ followUps: q.followUps || [],
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ // Process behaviour questions
157
+ for (const [behaviourId, maturities] of Object.entries(
158
+ questionBank.behaviourMaturities || {},
159
+ )) {
160
+ const behaviourName = getBehaviourName(behaviourId, behaviours);
161
+
162
+ // Filter by behaviour IDs
163
+ if (filter.behaviours && !filter.behaviours.includes(behaviourId)) continue;
164
+
165
+ // Skip behaviours if filtering by capability (skill-only filter)
166
+ if (filter.capability) continue;
167
+
168
+ // Skip behaviours if filtering by specific skills
169
+ if (filter.skills) continue;
170
+
171
+ for (const [maturity, maturityQuestions] of Object.entries(maturities)) {
172
+ // Filter by maturity
173
+ if (filter.maturity && maturity !== filter.maturity) continue;
174
+
175
+ // Skip if filtering by level (skill-only filter)
176
+ if (filter.level) continue;
177
+
178
+ for (const q of maturityQuestions) {
179
+ questions.push({
180
+ source: behaviourId,
181
+ sourceName: behaviourName,
182
+ sourceType: "behaviour",
183
+ level: maturity,
184
+ id: q.id,
185
+ text: q.text,
186
+ lookingFor: q.lookingFor || [],
187
+ expectedDurationMinutes: q.expectedDurationMinutes || 5,
188
+ followUps: q.followUps || [],
189
+ });
190
+ }
191
+ }
192
+ }
193
+
194
+ return questions;
195
+ }
196
+
197
+ /**
198
+ * Calculate question statistics
199
+ * @param {FlattenedQuestion[]} questions
200
+ * @param {Object} questionBank
201
+ * @returns {Object}
202
+ */
203
+ export function calculateStats(questions, questionBank) {
204
+ const bySource = {};
205
+ const byLevel = {};
206
+
207
+ for (const q of questions) {
208
+ bySource[q.source] = (bySource[q.source] || 0) + 1;
209
+ byLevel[q.level] = (byLevel[q.level] || 0) + 1;
210
+ }
211
+
212
+ // Calculate full stats for skills and behaviours
213
+ const skillStats = {};
214
+ for (const [skillId, levels] of Object.entries(
215
+ questionBank.skillLevels || {},
216
+ )) {
217
+ skillStats[skillId] = {};
218
+ for (const level of SKILL_LEVELS) {
219
+ skillStats[skillId][level] = (levels[level] || []).length;
220
+ }
221
+ skillStats[skillId].total = Object.values(skillStats[skillId]).reduce(
222
+ (a, b) => a + b,
223
+ 0,
224
+ );
225
+ }
226
+
227
+ const behaviourStats = {};
228
+ for (const [behaviourId, maturities] of Object.entries(
229
+ questionBank.behaviourMaturities || {},
230
+ )) {
231
+ behaviourStats[behaviourId] = {};
232
+ for (const maturity of BEHAVIOUR_MATURITIES) {
233
+ behaviourStats[behaviourId][maturity] = (
234
+ maturities[maturity] || []
235
+ ).length;
236
+ }
237
+ behaviourStats[behaviourId].total = Object.values(
238
+ behaviourStats[behaviourId],
239
+ ).reduce((a, b) => a + b, 0);
240
+ }
241
+
242
+ return {
243
+ totalQuestions: questions.length,
244
+ bySource,
245
+ byLevel,
246
+ skillStats,
247
+ behaviourStats,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Prepare questions view for rendering
253
+ * @param {Object} params
254
+ * @param {Object} params.questionBank
255
+ * @param {Array} params.skills
256
+ * @param {Array} params.behaviours
257
+ * @param {QuestionsFilter} params.filter
258
+ * @returns {QuestionsView}
259
+ */
260
+ export function prepareQuestionsView({
261
+ questionBank,
262
+ skills,
263
+ behaviours,
264
+ filter,
265
+ }) {
266
+ const questions = flattenQuestions(questionBank, skills, behaviours, filter);
267
+ const stats = calculateStats(questions, questionBank);
268
+
269
+ return {
270
+ filter,
271
+ questions,
272
+ stats,
273
+ };
274
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Questions YAML Formatter
3
+ *
4
+ * Formats questions as YAML for export and bulk editing.
5
+ */
6
+
7
+ import { stringify } from "yaml";
8
+
9
+ /**
10
+ * Format questions as YAML
11
+ * @param {Object} view - Questions view from prepareQuestionsView
12
+ * @param {Object} options - Format options
13
+ * @returns {string}
14
+ */
15
+ export function questionsToYaml(view, _options = {}) {
16
+ const { filter, questions } = view;
17
+
18
+ // Build header comment
19
+ const filterParts = [];
20
+ if (filter.level) filterParts.push(`--level=${filter.level}`);
21
+ if (filter.maturity) filterParts.push(`--maturity=${filter.maturity}`);
22
+ if (filter.skills) filterParts.push(`--skill=${filter.skills.join(",")}`);
23
+ if (filter.behaviours)
24
+ filterParts.push(`--behaviour=${filter.behaviours.join(",")}`);
25
+ if (filter.capability) filterParts.push(`--capability=${filter.capability}`);
26
+
27
+ const filterStr =
28
+ filterParts.length > 0 ? filterParts.join(" ") : "(no filters)";
29
+ const header = `# Generated by: npx pathway questions ${filterStr}\n# Questions: ${questions.length}\n\n`;
30
+
31
+ // Group questions by source
32
+ const bySource = {};
33
+ for (const q of questions) {
34
+ if (!bySource[q.source]) {
35
+ bySource[q.source] = [];
36
+ }
37
+
38
+ // Build question object for YAML
39
+ const yamlQuestion = {
40
+ id: q.id,
41
+ text: q.text,
42
+ lookingFor: q.lookingFor,
43
+ expectedDurationMinutes: q.expectedDurationMinutes,
44
+ };
45
+
46
+ if (q.followUps.length > 0) {
47
+ yamlQuestion.followUps = q.followUps;
48
+ }
49
+
50
+ bySource[q.source].push({
51
+ level: q.level,
52
+ question: yamlQuestion,
53
+ });
54
+ }
55
+
56
+ // Build output structure grouped by source then level
57
+ const output = {};
58
+ for (const [sourceId, items] of Object.entries(bySource).sort()) {
59
+ output[sourceId] = {};
60
+ for (const item of items) {
61
+ if (!output[sourceId][item.level]) {
62
+ output[sourceId][item.level] = [];
63
+ }
64
+ output[sourceId][item.level].push(item.question);
65
+ }
66
+ }
67
+
68
+ // Stringify with nice formatting
69
+ const yamlContent = stringify(output, {
70
+ lineWidth: 80,
71
+ defaultKeyType: "PLAIN",
72
+ defaultStringType: "PLAIN",
73
+ });
74
+
75
+ return header + yamlContent;
76
+ }