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