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