@forwardimpact/map 0.11.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 (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +67 -0
  3. package/bin/fit-map.js +287 -0
  4. package/examples/behaviours/_index.yaml +8 -0
  5. package/examples/behaviours/outcome_ownership.yaml +43 -0
  6. package/examples/behaviours/polymathic_knowledge.yaml +41 -0
  7. package/examples/behaviours/precise_communication.yaml +39 -0
  8. package/examples/behaviours/relentless_curiosity.yaml +37 -0
  9. package/examples/behaviours/systems_thinking.yaml +40 -0
  10. package/examples/capabilities/_index.yaml +8 -0
  11. package/examples/capabilities/business.yaml +205 -0
  12. package/examples/capabilities/delivery.yaml +1001 -0
  13. package/examples/capabilities/people.yaml +68 -0
  14. package/examples/capabilities/reliability.yaml +349 -0
  15. package/examples/capabilities/scale.yaml +1672 -0
  16. package/examples/copilot-setup-steps.yaml +25 -0
  17. package/examples/devcontainer.yaml +21 -0
  18. package/examples/disciplines/_index.yaml +6 -0
  19. package/examples/disciplines/data_engineering.yaml +68 -0
  20. package/examples/disciplines/engineering_management.yaml +61 -0
  21. package/examples/disciplines/software_engineering.yaml +68 -0
  22. package/examples/drivers.yaml +202 -0
  23. package/examples/framework.yaml +73 -0
  24. package/examples/levels.yaml +115 -0
  25. package/examples/questions/behaviours/outcome_ownership.yaml +228 -0
  26. package/examples/questions/behaviours/polymathic_knowledge.yaml +275 -0
  27. package/examples/questions/behaviours/precise_communication.yaml +248 -0
  28. package/examples/questions/behaviours/relentless_curiosity.yaml +248 -0
  29. package/examples/questions/behaviours/systems_thinking.yaml +238 -0
  30. package/examples/questions/capabilities/business.yaml +107 -0
  31. package/examples/questions/capabilities/delivery.yaml +101 -0
  32. package/examples/questions/capabilities/people.yaml +106 -0
  33. package/examples/questions/capabilities/reliability.yaml +105 -0
  34. package/examples/questions/capabilities/scale.yaml +104 -0
  35. package/examples/questions/skills/architecture_design.yaml +115 -0
  36. package/examples/questions/skills/cloud_platforms.yaml +105 -0
  37. package/examples/questions/skills/code_quality.yaml +162 -0
  38. package/examples/questions/skills/data_modeling.yaml +107 -0
  39. package/examples/questions/skills/devops.yaml +111 -0
  40. package/examples/questions/skills/full_stack_development.yaml +118 -0
  41. package/examples/questions/skills/sre_practices.yaml +113 -0
  42. package/examples/questions/skills/stakeholder_management.yaml +116 -0
  43. package/examples/questions/skills/team_collaboration.yaml +106 -0
  44. package/examples/questions/skills/technical_writing.yaml +110 -0
  45. package/examples/self-assessments.yaml +64 -0
  46. package/examples/stages.yaml +191 -0
  47. package/examples/tracks/_index.yaml +5 -0
  48. package/examples/tracks/platform.yaml +47 -0
  49. package/examples/tracks/sre.yaml +46 -0
  50. package/examples/vscode-settings.yaml +21 -0
  51. package/package.json +49 -0
  52. package/schema/json/behaviour-questions.schema.json +95 -0
  53. package/schema/json/behaviour.schema.json +73 -0
  54. package/schema/json/capability-questions.schema.json +95 -0
  55. package/schema/json/capability.schema.json +229 -0
  56. package/schema/json/defs.schema.json +132 -0
  57. package/schema/json/discipline.schema.json +123 -0
  58. package/schema/json/drivers.schema.json +48 -0
  59. package/schema/json/framework.schema.json +68 -0
  60. package/schema/json/levels.schema.json +121 -0
  61. package/schema/json/self-assessments.schema.json +52 -0
  62. package/schema/json/skill-questions.schema.json +83 -0
  63. package/schema/json/stages.schema.json +88 -0
  64. package/schema/json/track.schema.json +95 -0
  65. package/schema/rdf/behaviour-questions.ttl +128 -0
  66. package/schema/rdf/behaviour.ttl +130 -0
  67. package/schema/rdf/capability.ttl +466 -0
  68. package/schema/rdf/defs.ttl +396 -0
  69. package/schema/rdf/discipline.ttl +313 -0
  70. package/schema/rdf/drivers.ttl +84 -0
  71. package/schema/rdf/framework.ttl +166 -0
  72. package/schema/rdf/levels.ttl +357 -0
  73. package/schema/rdf/self-assessments.ttl +147 -0
  74. package/schema/rdf/skill-questions.ttl +155 -0
  75. package/schema/rdf/stages.ttl +166 -0
  76. package/schema/rdf/track.ttl +225 -0
  77. package/src/index-generator.js +65 -0
  78. package/src/index.js +44 -0
  79. package/src/levels.js +553 -0
  80. package/src/loader.js +608 -0
  81. package/src/modifiers.js +23 -0
  82. package/src/schema-validation.js +438 -0
  83. package/src/validation.js +2136 -0
package/src/loader.js ADDED
@@ -0,0 +1,608 @@
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
+ instructions,
94
+ installScript,
95
+ implementationReference,
96
+ toolReferences,
97
+ } = skill;
98
+ allSkills.push({
99
+ id,
100
+ 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
107
+ ...(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
+ }
118
+
119
+ return allSkills;
120
+ }
121
+
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
+ }
182
+
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
+ }
230
+
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
+ }
260
+
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
+ }
282
+
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
+ ),
296
+ ]);
297
+
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;
368
+ }
369
+
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;
404
+ }
405
+
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);
495
+
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}`);
501
+ }
502
+
503
+ return {
504
+ valid: result.valid,
505
+ data,
506
+ errors: result.errors,
507
+ warnings: result.warnings,
508
+ };
509
+ }
510
+
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
+ };
575
+ }
576
+
577
+ /**
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
582
+ */
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;
608
+ }
@@ -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/libpathway.
6
+ */
7
+
8
+ import { Capability } from "./levels.js";
9
+
10
+ /**
11
+ * Valid skill capability names (derived from Capability enum)
12
+ * @type {Set<string>}
13
+ */
14
+ const VALID_CAPABILITIES = new Set(Object.values(Capability));
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
+ }