@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,564 @@
1
+ /**
2
+ * Engineering Pathway Data Loader
3
+ *
4
+ * Utility for loading and parsing YAML data files.
5
+ * Uses directory structure: disciplines/, tracks/, skills/
6
+ */
7
+
8
+ import { readFile, readdir, stat } from "fs/promises";
9
+ import { parse as parseYaml } from "yaml";
10
+ import { join, basename } from "path";
11
+ import { validateAllData, validateQuestionBank } from "./validation.js";
12
+
13
+ /**
14
+ * Check if a file exists
15
+ * @param {string} path - Path to check
16
+ * @returns {Promise<boolean>} True if file exists
17
+ */
18
+ async function fileExists(path) {
19
+ try {
20
+ await stat(path);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Load a YAML file and parse it
29
+ * @param {string} filePath - Path to the YAML file
30
+ * @returns {Promise<any>} Parsed YAML content
31
+ */
32
+ export async function loadYamlFile(filePath) {
33
+ const content = await readFile(filePath, "utf-8");
34
+ return parseYaml(content);
35
+ }
36
+
37
+ /**
38
+ * Load framework configuration from a data directory
39
+ * @param {string} dataDir - Path to the data directory
40
+ * @returns {Promise<Object>} Framework configuration
41
+ */
42
+ export async function loadFrameworkConfig(dataDir) {
43
+ return loadYamlFile(join(dataDir, "framework.yaml"));
44
+ }
45
+
46
+ /**
47
+ * Load all question files from a directory
48
+ * @param {string} dir - Directory path
49
+ * @returns {Promise<Object>} Map of id to question levels
50
+ */
51
+ async function loadQuestionsFromDir(dir) {
52
+ const files = await readdir(dir);
53
+ const yamlFiles = files.filter((f) => f.endsWith(".yaml"));
54
+
55
+ const entries = await Promise.all(
56
+ yamlFiles.map(async (file) => {
57
+ const id = basename(file, ".yaml");
58
+ const content = await loadYamlFile(join(dir, file));
59
+ return [id, content];
60
+ }),
61
+ );
62
+
63
+ return Object.fromEntries(entries);
64
+ }
65
+
66
+ /**
67
+ * Load skills from capability files
68
+ * Skills are embedded in capability YAML files under the 'skills' array.
69
+ * This function extracts all skills and adds the capability ID back to each.
70
+ * @param {string} capabilitiesDir - Path to capabilities directory
71
+ * @returns {Promise<Array>} Array of skill objects in flat format
72
+ */
73
+ async function loadSkillsFromCapabilities(capabilitiesDir) {
74
+ const files = await readdir(capabilitiesDir);
75
+ const yamlFiles = files.filter(
76
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
77
+ );
78
+
79
+ const allSkills = [];
80
+
81
+ for (const file of yamlFiles) {
82
+ const capabilityId = basename(file, ".yaml"); // Derive ID from filename
83
+ const capability = await loadYamlFile(join(capabilitiesDir, file));
84
+
85
+ if (capability.skills && Array.isArray(capability.skills)) {
86
+ for (const skill of capability.skills) {
87
+ const { id, name, isHumanOnly, human, agent } = skill;
88
+ allSkills.push({
89
+ id,
90
+ name,
91
+ capability: capabilityId, // Add capability from parent
92
+ description: human.description,
93
+ levelDescriptions: human.levelDescriptions,
94
+ // Include isHumanOnly flag for agent filtering (defaults to false)
95
+ ...(isHumanOnly && { isHumanOnly }),
96
+ // Preserve agent section for agent generation
97
+ ...(agent && { agent }),
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ return allSkills;
104
+ }
105
+
106
+ /**
107
+ * Load disciplines from directory (individual files: disciplines/{id}.yaml)
108
+ * @param {string} disciplinesDir - Path to disciplines directory
109
+ * @returns {Promise<Array>} Array of discipline objects
110
+ */
111
+ async function loadDisciplinesFromDir(disciplinesDir) {
112
+ const files = await readdir(disciplinesDir);
113
+ const yamlFiles = files.filter(
114
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
115
+ );
116
+
117
+ const disciplines = await Promise.all(
118
+ yamlFiles.map(async (file) => {
119
+ const id = basename(file, ".yaml"); // Derive ID from filename
120
+ const content = await loadYamlFile(join(disciplinesDir, file));
121
+ const {
122
+ specialization,
123
+ roleTitle,
124
+ // Shared content - now at root level
125
+ description,
126
+ // Structural properties (derivation inputs) - at top level
127
+ coreSkills,
128
+ supportingSkills,
129
+ broadSkills,
130
+ behaviourModifiers,
131
+ // Presentation sections
132
+ human,
133
+ agent,
134
+ } = content;
135
+ return {
136
+ id,
137
+ specialization,
138
+ roleTitle,
139
+ // Shared content at top level
140
+ description,
141
+ // Structural properties at top level
142
+ coreSkills,
143
+ supportingSkills,
144
+ broadSkills,
145
+ behaviourModifiers,
146
+ // Human presentation content (role summaries only)
147
+ ...human,
148
+ // Preserve agent section for agent generation
149
+ ...(agent && { agent }),
150
+ };
151
+ }),
152
+ );
153
+
154
+ return disciplines;
155
+ }
156
+
157
+ /**
158
+ * Load tracks from directory (individual files: tracks/{id}.yaml)
159
+ * @param {string} tracksDir - Path to tracks directory
160
+ * @returns {Promise<Array>} Array of track objects
161
+ */
162
+ async function loadTracksFromDir(tracksDir) {
163
+ const files = await readdir(tracksDir);
164
+ const yamlFiles = files.filter(
165
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
166
+ );
167
+
168
+ const tracks = await Promise.all(
169
+ yamlFiles.map(async (file) => {
170
+ const id = basename(file, ".yaml"); // Derive ID from filename
171
+ const content = await loadYamlFile(join(tracksDir, file));
172
+ const {
173
+ name,
174
+ // Shared content - now at root level
175
+ description,
176
+ roleContext,
177
+ // Structural properties (derivation inputs) - at top level
178
+ isProfessional,
179
+ isManagement,
180
+ skillModifiers,
181
+ behaviourModifiers,
182
+ assessmentWeights,
183
+ validDisciplines,
184
+ // Agent section (no human section anymore for tracks)
185
+ agent,
186
+ } = content;
187
+ return {
188
+ id,
189
+ name,
190
+ // Shared content at top level
191
+ description,
192
+ roleContext,
193
+ // Structural properties at top level
194
+ isProfessional,
195
+ isManagement,
196
+ skillModifiers,
197
+ behaviourModifiers,
198
+ assessmentWeights,
199
+ validDisciplines,
200
+ // Preserve agent section for agent generation
201
+ ...(agent && { agent }),
202
+ };
203
+ }),
204
+ );
205
+
206
+ return tracks;
207
+ }
208
+
209
+ /**
210
+ * Load behaviours from directory (individual files: behaviours/{id}.yaml)
211
+ * @param {string} behavioursDir - Path to behaviours directory
212
+ * @returns {Promise<Array>} Array of behaviour objects
213
+ */
214
+ async function loadBehavioursFromDir(behavioursDir) {
215
+ const files = await readdir(behavioursDir);
216
+ const yamlFiles = files.filter(
217
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
218
+ );
219
+
220
+ const behaviours = await Promise.all(
221
+ yamlFiles.map(async (file) => {
222
+ const id = basename(file, ".yaml"); // Derive ID from filename
223
+ const content = await loadYamlFile(join(behavioursDir, file));
224
+ // Flatten human properties to top level (behaviours use human: section in YAML)
225
+ const { name, human, agent } = content;
226
+ return {
227
+ id,
228
+ name,
229
+ ...human,
230
+ // Preserve agent section for agent generation
231
+ ...(agent && { agent }),
232
+ };
233
+ }),
234
+ );
235
+
236
+ return behaviours;
237
+ }
238
+
239
+ /**
240
+ * Load capabilities from directory
241
+ * @param {string} capabilitiesDir - Path to capabilities directory
242
+ * @returns {Promise<Array>} Array of capability objects
243
+ */
244
+ async function loadCapabilitiesFromDir(capabilitiesDir) {
245
+ const files = await readdir(capabilitiesDir);
246
+ const yamlFiles = files.filter(
247
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
248
+ );
249
+
250
+ const capabilities = await Promise.all(
251
+ yamlFiles.map(async (file) => {
252
+ const id = basename(file, ".yaml"); // Derive ID from filename
253
+ const content = await loadYamlFile(join(capabilitiesDir, file));
254
+ return { id, ...content }; // Add derived ID
255
+ }),
256
+ );
257
+
258
+ return capabilities;
259
+ }
260
+
261
+ /**
262
+ * Load questions from folder structure
263
+ * @param {string} questionsDir - Path to questions directory
264
+ * @returns {Promise<import('./levels.js').QuestionBank>}
265
+ */
266
+ export async function loadQuestionFolder(questionsDir) {
267
+ const [skillLevels, behaviourMaturities] = await Promise.all([
268
+ loadQuestionsFromDir(join(questionsDir, "skills")),
269
+ loadQuestionsFromDir(join(questionsDir, "behaviours")),
270
+ ]);
271
+
272
+ return { skillLevels, behaviourMaturities };
273
+ }
274
+
275
+ /**
276
+ * Load all data from a directory
277
+ * @param {string} dataDir - Path to the data directory
278
+ * @param {Object} [options] - Loading options
279
+ * @param {boolean} [options.validate=true] - Whether to validate data after loading
280
+ * @param {boolean} [options.throwOnError=true] - Whether to throw on validation errors
281
+ * @returns {Promise<Object>} All loaded data
282
+ */
283
+ export async function loadAllData(dataDir, options = {}) {
284
+ const { validate = true, throwOnError = true } = options;
285
+
286
+ // Load capabilities first (skills are embedded in capabilities)
287
+ const capabilities = await loadCapabilitiesFromDir(
288
+ join(dataDir, "capabilities"),
289
+ );
290
+
291
+ // Extract skills from capabilities
292
+ const skills = await loadSkillsFromCapabilities(
293
+ join(dataDir, "capabilities"),
294
+ );
295
+
296
+ // Load remaining data files in parallel
297
+ const [
298
+ drivers,
299
+ behaviours,
300
+ disciplines,
301
+ tracks,
302
+ grades,
303
+ stages,
304
+ questions,
305
+ framework,
306
+ ] = await Promise.all([
307
+ loadYamlFile(join(dataDir, "drivers.yaml")),
308
+ loadBehavioursFromDir(join(dataDir, "behaviours")),
309
+ loadDisciplinesFromDir(join(dataDir, "disciplines")),
310
+ loadTracksFromDir(join(dataDir, "tracks")),
311
+ loadYamlFile(join(dataDir, "grades.yaml")),
312
+ loadYamlFile(join(dataDir, "stages.yaml")),
313
+ loadQuestionFolder(join(dataDir, "questions")),
314
+ loadYamlFile(join(dataDir, "framework.yaml")),
315
+ ]);
316
+
317
+ const data = {
318
+ drivers,
319
+ behaviours,
320
+ skills,
321
+ disciplines,
322
+ tracks,
323
+ grades,
324
+ capabilities,
325
+ stages,
326
+ questions,
327
+ framework,
328
+ };
329
+
330
+ // Validate if requested
331
+ if (validate) {
332
+ const result = validateAllData(data);
333
+
334
+ if (!result.valid && throwOnError) {
335
+ const errorMessages = result.errors
336
+ .map((e) => `${e.type}: ${e.message}`)
337
+ .join("\n");
338
+ throw new Error(`Data validation failed:\n${errorMessages}`);
339
+ }
340
+
341
+ data.validation = result;
342
+ }
343
+
344
+ return data;
345
+ }
346
+
347
+ /**
348
+ * Load question bank from a folder
349
+ * @param {string} questionsDir - Path to the questions folder
350
+ * @param {import('./levels.js').Skill[]} [skills] - Skills for validation
351
+ * @param {import('./levels.js').Behaviour[]} [behaviours] - Behaviours for validation
352
+ * @param {Object} [options] - Loading options
353
+ * @param {boolean} [options.validate=true] - Whether to validate
354
+ * @param {boolean} [options.throwOnError=true] - Whether to throw on errors
355
+ * @returns {Promise<import('./levels.js').QuestionBank>} Loaded question bank
356
+ */
357
+ export async function loadQuestionBankFromFolder(
358
+ questionsDir,
359
+ skills,
360
+ behaviours,
361
+ options = {},
362
+ ) {
363
+ const { validate = true, throwOnError = true } = options;
364
+
365
+ const questionBank = await loadQuestionFolder(questionsDir);
366
+
367
+ if (validate && skills && behaviours) {
368
+ const result = validateQuestionBank(questionBank, skills, behaviours);
369
+
370
+ if (!result.valid && throwOnError) {
371
+ const errorMessages = result.errors
372
+ .map((e) => `${e.type}: ${e.message}`)
373
+ .join("\n");
374
+ throw new Error(`Question bank validation failed:\n${errorMessages}`);
375
+ }
376
+
377
+ questionBank.validation = result;
378
+ }
379
+
380
+ return questionBank;
381
+ }
382
+
383
+ /**
384
+ * Load self-assessments from a file
385
+ * @param {string} filePath - Path to the self-assessments YAML file
386
+ * @returns {Promise<import('./levels.js').SelfAssessment[]>} Array of self-assessments
387
+ */
388
+ export async function loadSelfAssessments(filePath) {
389
+ return loadYamlFile(filePath);
390
+ }
391
+
392
+ /**
393
+ * Create a data loader for a specific directory
394
+ * @param {string} dataDir - Path to the data directory
395
+ * @returns {Object} Data loader with bound methods
396
+ */
397
+ export function createDataLoader(dataDir) {
398
+ return {
399
+ /**
400
+ * Load all core data
401
+ * @param {Object} [options] - Loading options
402
+ * @returns {Promise<Object>} All data
403
+ */
404
+ loadAll: (options) => loadAllData(dataDir, options),
405
+
406
+ /**
407
+ * Load question bank
408
+ * @param {import('./levels.js').Skill[]} skills - Skills for validation
409
+ * @param {import('./levels.js').Behaviour[]} behaviours - Behaviours for validation
410
+ * @param {Object} [options] - Loading options
411
+ * @returns {Promise<import('./levels.js').QuestionBank>} Question bank
412
+ */
413
+ loadQuestions: (skills, behaviours, options) =>
414
+ loadQuestionBankFromFolder(
415
+ join(dataDir, "questions"),
416
+ skills,
417
+ behaviours,
418
+ options,
419
+ ),
420
+
421
+ /**
422
+ * Load self-assessments
423
+ * @returns {Promise<import('./levels.js').SelfAssessment[]>} Self-assessments
424
+ */
425
+ loadSelfAssessments: () =>
426
+ loadSelfAssessments(join(dataDir, "self-assessments.yaml")),
427
+
428
+ /**
429
+ * Load a specific file
430
+ * @param {string} filename - File name to load
431
+ * @returns {Promise<any>} Parsed content
432
+ */
433
+ loadFile: (filename) => loadYamlFile(join(dataDir, filename)),
434
+ };
435
+ }
436
+
437
+ /**
438
+ * Load example data from the examples directory
439
+ * @param {string} rootDir - Root directory of the project
440
+ * @param {Object} [options] - Loading options
441
+ * @returns {Promise<Object>} Example data
442
+ */
443
+ export async function loadExampleData(rootDir, options = {}) {
444
+ const examplesDir = join(rootDir, "examples");
445
+ return loadAllData(examplesDir, options);
446
+ }
447
+
448
+ /**
449
+ * Validate data and optionally throw on errors
450
+ *
451
+ * This is a synchronous validation function for when you already have
452
+ * the data loaded and just need to validate it.
453
+ *
454
+ * @param {Object} data - All competency data
455
+ * @param {import('./levels.js').Driver[]} data.drivers - Drivers
456
+ * @param {import('./levels.js').Behaviour[]} data.behaviours - Behaviours
457
+ * @param {import('./levels.js').Skill[]} data.skills - Skills
458
+ * @param {import('./levels.js').Discipline[]} data.disciplines - Disciplines
459
+ * @param {import('./levels.js').Track[]} data.tracks - Tracks
460
+ * @param {import('./levels.js').Grade[]} data.grades - Grades
461
+ * @param {Object} [options] - Options
462
+ * @param {boolean} [options.throwOnError=true] - Whether to throw on validation errors
463
+ * @returns {{valid: boolean, data: Object, errors: Array, warnings: Array}}
464
+ */
465
+ export function loadAndValidate(data, options = {}) {
466
+ const { throwOnError = true } = options;
467
+
468
+ const result = validateAllData(data);
469
+
470
+ if (!result.valid && throwOnError) {
471
+ const errorMessages = result.errors
472
+ .map((e) => `${e.type}: ${e.message}`)
473
+ .join("\n");
474
+ throw new Error(`Data validation failed:\n${errorMessages}`);
475
+ }
476
+
477
+ return {
478
+ valid: result.valid,
479
+ data,
480
+ errors: result.errors,
481
+ warnings: result.warnings,
482
+ };
483
+ }
484
+
485
+ /**
486
+ * Load agent-specific data for agent profile generation
487
+ * Uses co-located files: each entity file contains both human and agent sections
488
+ * @param {string} dataDir - Path to the data directory
489
+ * @returns {Promise<Object>} Agent data including disciplines, tracks, behaviours, vscodeSettings
490
+ */
491
+ export async function loadAgentData(dataDir) {
492
+ const disciplinesDir = join(dataDir, "disciplines");
493
+ const tracksDir = join(dataDir, "tracks");
494
+ const behavioursDir = join(dataDir, "behaviours");
495
+
496
+ // 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
+ ]);
506
+
507
+ // Extract agent sections from co-located files
508
+ const disciplines = disciplineFiles
509
+ .filter((d) => d.agent)
510
+ .map((d) => ({
511
+ id: d.id,
512
+ ...d.agent,
513
+ }));
514
+
515
+ const tracks = trackFiles
516
+ .filter((t) => t.agent)
517
+ .map((t) => ({
518
+ id: t.id,
519
+ ...t.agent,
520
+ }));
521
+
522
+ const behaviours = behaviourFiles
523
+ .filter((b) => b.agent)
524
+ .map((b) => ({
525
+ id: b.id,
526
+ ...b.agent,
527
+ }));
528
+
529
+ return {
530
+ disciplines,
531
+ tracks,
532
+ behaviours,
533
+ vscodeSettings,
534
+ };
535
+ }
536
+
537
+ /**
538
+ * Load skills with agent sections from capability files
539
+ * Skills are embedded in capability YAML files under the 'skills' array.
540
+ * @param {string} dataDir - Path to the data directory
541
+ * @returns {Promise<Array>} Skills with agent sections preserved
542
+ */
543
+ export async function loadSkillsWithAgentData(dataDir) {
544
+ const capabilitiesDir = join(dataDir, "capabilities");
545
+
546
+ const files = await readdir(capabilitiesDir);
547
+ const yamlFiles = files.filter(
548
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
549
+ );
550
+
551
+ const allSkills = [];
552
+
553
+ for (const file of yamlFiles) {
554
+ const capability = await loadYamlFile(join(capabilitiesDir, file));
555
+
556
+ if (capability.skills && Array.isArray(capability.skills)) {
557
+ for (const skill of capability.skills) {
558
+ allSkills.push(skill);
559
+ }
560
+ }
561
+ }
562
+
563
+ return allSkills;
564
+ }