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