@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
@@ -12,22 +12,16 @@ import { fileURLToPath } from "url";
12
12
  import Ajv from "ajv";
13
13
  import addFormats from "ajv-formats";
14
14
 
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = dirname(__filename);
17
- const schemaDir = join(__dirname, "../schema/json");
18
-
19
15
  /**
20
16
  * Schema mappings for different file types
21
17
  * Maps directory/file patterns to schema files
22
18
  */
23
19
  const SCHEMA_MAPPINGS = {
24
- // Single files at root of data directory
25
20
  "drivers.yaml": "drivers.schema.json",
26
21
  "levels.yaml": "levels.schema.json",
27
22
  "stages.yaml": "stages.schema.json",
28
23
  "framework.yaml": "framework.schema.json",
29
24
  "self-assessments.yaml": "self-assessments.schema.json",
30
- // Directories - each file in directory uses the schema
31
25
  capabilities: "capability.schema.json",
32
26
  disciplines: "discipline.schema.json",
33
27
  tracks: "track.schema.json",
@@ -38,9 +32,9 @@ const SCHEMA_MAPPINGS = {
38
32
 
39
33
  /**
40
34
  * Create a validation result object
41
- * @param {boolean} valid - Whether validation passed
42
- * @param {Array<{type: string, message: string, path?: string}>} errors - Array of errors
43
- * @param {Array<{type: string, message: string, path?: string}>} warnings - Array of warnings
35
+ * @param {boolean} valid
36
+ * @param {Array} errors
37
+ * @param {Array} warnings
44
38
  * @returns {{valid: boolean, errors: Array, warnings: Array}}
45
39
  */
46
40
  function createValidationResult(valid, errors = [], warnings = []) {
@@ -49,9 +43,9 @@ function createValidationResult(valid, errors = [], warnings = []) {
49
43
 
50
44
  /**
51
45
  * Create a validation error
52
- * @param {string} type - Error type
53
- * @param {string} message - Error message
54
- * @param {string} [path] - Path to invalid data
46
+ * @param {string} type
47
+ * @param {string} message
48
+ * @param {string} [path]
55
49
  * @returns {{type: string, message: string, path?: string}}
56
50
  */
57
51
  function createError(type, message, path) {
@@ -62,9 +56,9 @@ function createError(type, message, path) {
62
56
 
63
57
  /**
64
58
  * Create a validation warning
65
- * @param {string} type - Warning type
66
- * @param {string} message - Warning message
67
- * @param {string} [path] - Path to concerning data
59
+ * @param {string} type
60
+ * @param {string} message
61
+ * @param {string} [path]
68
62
  * @returns {{type: string, message: string, path?: string}}
69
63
  */
70
64
  function createWarning(type, message, path) {
@@ -73,66 +67,17 @@ function createWarning(type, message, path) {
73
67
  return warning;
74
68
  }
75
69
 
76
- /**
77
- * Check if a path exists and is a directory
78
- * @param {string} path - Path to check
79
- * @returns {Promise<boolean>}
80
- */
81
- async function isDirectory(path) {
82
- try {
83
- const stats = await stat(path);
84
- return stats.isDirectory();
85
- } catch {
86
- return false;
87
- }
88
- }
89
-
90
- /**
91
- * Check if a file exists
92
- * @param {string} path - Path to check
93
- * @returns {Promise<boolean>}
94
- */
95
- async function fileExists(path) {
96
- try {
97
- await stat(path);
98
- return true;
99
- } catch {
100
- return false;
101
- }
102
- }
103
-
104
- /**
105
- * Load and parse a JSON schema
106
- * @param {string} schemaPath - Path to the schema file
107
- * @returns {Promise<Object>} Parsed schema
108
- */
109
- async function loadSchema(schemaPath) {
110
- const content = await readFile(schemaPath, "utf-8");
111
- return JSON.parse(content);
112
- }
113
-
114
- /**
115
- * Load and parse a YAML file
116
- * @param {string} filePath - Path to the YAML file
117
- * @returns {Promise<any>} Parsed YAML content
118
- */
119
- async function loadYamlFile(filePath) {
120
- const content = await readFile(filePath, "utf-8");
121
- return parseYaml(content);
122
- }
123
-
124
70
  /**
125
71
  * Format Ajv errors into readable messages
126
- * @param {import('ajv').ErrorObject[]} ajvErrors - Ajv error objects
127
- * @param {string} filePath - File being validated
128
- * @returns {Array<{type: string, message: string, path?: string}>}
72
+ * @param {import('ajv').ErrorObject[]} ajvErrors
73
+ * @param {string} filePath
74
+ * @returns {Array}
129
75
  */
130
76
  function formatAjvErrors(ajvErrors, filePath) {
131
77
  return ajvErrors.map((err) => {
132
78
  const path = err.instancePath ? `${filePath}${err.instancePath}` : filePath;
133
79
  let message = err.message || "Unknown error";
134
80
 
135
- // Add context for specific error types
136
81
  if (err.keyword === "additionalProperties") {
137
82
  message = `${message}: '${err.params.additionalProperty}'`;
138
83
  } else if (err.keyword === "enum") {
@@ -146,293 +91,368 @@ function formatAjvErrors(ajvErrors, filePath) {
146
91
  }
147
92
 
148
93
  /**
149
- * Create and configure an Ajv instance with all schemas loaded
150
- * @returns {Promise<Ajv>}
94
+ * Schema validator class with injectable dependencies.
151
95
  */
152
- async function createValidator() {
153
- const ajv = new Ajv({
154
- allErrors: true,
155
- strict: false,
156
- validateFormats: true,
157
- });
158
- addFormats(ajv);
159
-
160
- // Load all schema files
161
- const schemaFiles = await readdir(schemaDir);
162
- for (const file of schemaFiles.filter((f) => f.endsWith(".schema.json"))) {
163
- const schema = await loadSchema(join(schemaDir, file));
164
- ajv.addSchema(schema);
96
+ export class SchemaValidator {
97
+ #fs;
98
+ #schemaDir;
99
+ #ajvFactory;
100
+
101
+ /**
102
+ * @param {{ readFile: Function, readdir: Function, stat: Function }} fs
103
+ * @param {string} schemaDir - Path to JSON schema directory
104
+ * @param {{ Ajv: Function, addFormats: Function }} ajvFactory
105
+ */
106
+ constructor(fs, schemaDir, ajvFactory) {
107
+ if (!fs) throw new Error("fs is required");
108
+ if (!schemaDir) throw new Error("schemaDir is required");
109
+ if (!ajvFactory) throw new Error("ajvFactory is required");
110
+ this.#fs = fs;
111
+ this.#schemaDir = schemaDir;
112
+ this.#ajvFactory = ajvFactory;
165
113
  }
166
114
 
167
- return ajv;
168
- }
115
+ /**
116
+ * Check if a path exists and is a directory
117
+ * @param {string} path
118
+ * @returns {Promise<boolean>}
119
+ */
120
+ async #isDirectory(path) {
121
+ try {
122
+ const stats = await this.#fs.stat(path);
123
+ return stats.isDirectory();
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
169
128
 
170
- /**
171
- * Validate a single file against a schema
172
- * @param {Ajv} ajv - Configured Ajv instance
173
- * @param {string} filePath - Path to the YAML file
174
- * @param {string} schemaId - Schema $id to validate against
175
- * @returns {Promise<{valid: boolean, errors: Array}>}
176
- */
177
- async function validateFile(ajv, filePath, schemaId) {
178
- const data = await loadYamlFile(filePath);
179
- const validate = ajv.getSchema(schemaId);
180
-
181
- if (!validate) {
182
- return {
183
- valid: false,
184
- errors: [
185
- createError("SCHEMA_NOT_FOUND", `Schema not found: ${schemaId}`),
186
- ],
187
- };
129
+ /**
130
+ * Check if a file exists
131
+ * @param {string} path
132
+ * @returns {Promise<boolean>}
133
+ */
134
+ async #fileExists(path) {
135
+ try {
136
+ await this.#fs.stat(path);
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
188
141
  }
189
142
 
190
- const valid = validate(data);
191
- const errors = valid ? [] : formatAjvErrors(validate.errors || [], filePath);
143
+ /**
144
+ * Load and parse a JSON schema
145
+ * @param {string} schemaPath
146
+ * @returns {Promise<Object>}
147
+ */
148
+ async #loadSchema(schemaPath) {
149
+ const content = await this.#fs.readFile(schemaPath, "utf-8");
150
+ return JSON.parse(content);
151
+ }
192
152
 
193
- return { valid, errors };
194
- }
153
+ /**
154
+ * Load and parse a YAML file
155
+ * @param {string} filePath
156
+ * @returns {Promise<any>}
157
+ */
158
+ async #loadYamlFile(filePath) {
159
+ const content = await this.#fs.readFile(filePath, "utf-8");
160
+ return parseYaml(content);
161
+ }
195
162
 
196
- /**
197
- * Validate all files in a directory against a schema
198
- * @param {Ajv} ajv - Configured Ajv instance
199
- * @param {string} dirPath - Path to the directory
200
- * @param {string} schemaId - Schema $id to validate against
201
- * @returns {Promise<{valid: boolean, errors: Array}>}
202
- */
203
- async function validateDirectory(ajv, dirPath, schemaId) {
204
- const files = await readdir(dirPath);
205
- const yamlFiles = files.filter(
206
- (f) => f.endsWith(".yaml") && !f.startsWith("_"),
207
- );
163
+ /**
164
+ * Create and configure an Ajv instance with all schemas loaded
165
+ * @returns {Promise<Ajv>}
166
+ */
167
+ async #createValidator() {
168
+ const ajv = new this.#ajvFactory.Ajv({
169
+ allErrors: true,
170
+ strict: false,
171
+ validateFormats: true,
172
+ });
173
+ this.#ajvFactory.addFormats(ajv);
174
+
175
+ const schemaFiles = await this.#fs.readdir(this.#schemaDir);
176
+ for (const file of schemaFiles.filter((f) => f.endsWith(".schema.json"))) {
177
+ const schema = await this.#loadSchema(join(this.#schemaDir, file));
178
+ ajv.addSchema(schema);
179
+ }
208
180
 
209
- const allErrors = [];
210
- let allValid = true;
181
+ return ajv;
182
+ }
211
183
 
212
- for (const file of yamlFiles) {
213
- const filePath = join(dirPath, file);
214
- const result = await validateFile(ajv, filePath, schemaId);
215
- if (!result.valid) {
216
- allValid = false;
217
- allErrors.push(...result.errors);
184
+ /**
185
+ * Validate a single file against a schema
186
+ * @param {Ajv} ajv
187
+ * @param {string} filePath
188
+ * @param {string} schemaId
189
+ * @returns {Promise<{valid: boolean, errors: Array}>}
190
+ */
191
+ async #validateFile(ajv, filePath, schemaId) {
192
+ const data = await this.#loadYamlFile(filePath);
193
+ const validate = ajv.getSchema(schemaId);
194
+
195
+ if (!validate) {
196
+ return {
197
+ valid: false,
198
+ errors: [
199
+ createError("SCHEMA_NOT_FOUND", `Schema not found: ${schemaId}`),
200
+ ],
201
+ };
218
202
  }
203
+
204
+ const valid = validate(data);
205
+ const errors = valid
206
+ ? []
207
+ : formatAjvErrors(validate.errors || [], filePath);
208
+
209
+ return { valid, errors };
219
210
  }
220
211
 
221
- return { valid: allValid, errors: allErrors };
222
- }
212
+ /**
213
+ * Validate all files in a directory against a schema
214
+ * @param {Ajv} ajv
215
+ * @param {string} dirPath
216
+ * @param {string} schemaId
217
+ * @returns {Promise<{valid: boolean, errors: Array}>}
218
+ */
219
+ async #validateDirectory(ajv, dirPath, schemaId) {
220
+ const files = await this.#fs.readdir(dirPath);
221
+ const yamlFiles = files.filter(
222
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
223
+ );
223
224
 
224
- /**
225
- * Build the schema $id from the schema filename
226
- * @param {string} schemaFilename - Schema filename (e.g., "capability.schema.json")
227
- * @returns {string} Full schema $id URL
228
- */
229
- function getSchemaId(schemaFilename) {
230
- return `https://www.forwardimpact.team/schema/json/${schemaFilename}`;
231
- }
225
+ const allErrors = [];
226
+ let allValid = true;
232
227
 
233
- /**
234
- * Validate a data directory against JSON schemas
235
- * @param {string} dataDir - Path to the data directory
236
- * @returns {Promise<{valid: boolean, errors: Array, warnings: Array, stats: Object}>}
237
- */
238
- export async function validateDataDirectory(dataDir) {
239
- const ajv = await createValidator();
240
- const allErrors = [];
241
- const warnings = [];
242
- const stats = {
243
- filesValidated: 0,
244
- schemasUsed: new Set(),
245
- };
246
-
247
- // Validate single files at root level
248
- for (const [filename, schemaFile] of Object.entries(SCHEMA_MAPPINGS)) {
249
- // Skip directory mappings
250
- if (!filename.includes(".yaml")) continue;
251
-
252
- const filePath = join(dataDir, filename);
253
- if (!(await fileExists(filePath))) {
254
- // Some files are optional
255
- if (!["self-assessments.yaml"].includes(filename)) {
256
- warnings.push(
257
- createWarning("MISSING_FILE", `Optional file not found: ${filename}`),
258
- );
228
+ for (const file of yamlFiles) {
229
+ const filePath = join(dirPath, file);
230
+ const result = await this.#validateFile(ajv, filePath, schemaId);
231
+ if (!result.valid) {
232
+ allValid = false;
233
+ allErrors.push(...result.errors);
259
234
  }
260
- continue;
261
235
  }
262
236
 
263
- const schemaId = getSchemaId(schemaFile);
264
- const result = await validateFile(ajv, filePath, schemaId);
265
- stats.filesValidated++;
266
- stats.schemasUsed.add(schemaFile);
267
-
268
- if (!result.valid) {
269
- allErrors.push(...result.errors);
270
- }
237
+ return { valid: allValid, errors: allErrors };
271
238
  }
272
239
 
273
- // Validate directories
274
- for (const [dirName, schemaFile] of Object.entries(SCHEMA_MAPPINGS)) {
275
- // Skip single file mappings
276
- if (dirName.includes(".yaml")) continue;
240
+ /**
241
+ * Build the schema $id from the schema filename
242
+ * @param {string} schemaFilename
243
+ * @returns {string}
244
+ */
245
+ #getSchemaId(schemaFilename) {
246
+ return `https://www.forwardimpact.team/schema/json/${schemaFilename}`;
247
+ }
277
248
 
278
- const dirPath = join(dataDir, dirName);
279
- if (!(await isDirectory(dirPath))) {
280
- continue;
281
- }
249
+ /**
250
+ * Validate a data directory against JSON schemas
251
+ * @param {string} dataDir
252
+ * @returns {Promise<{valid: boolean, errors: Array, warnings: Array, stats: Object}>}
253
+ */
254
+ async validateDataDirectory(dataDir) {
255
+ const ajv = await this.#createValidator();
256
+ const allErrors = [];
257
+ const warnings = [];
258
+ const stats = {
259
+ filesValidated: 0,
260
+ schemasUsed: new Set(),
261
+ };
282
262
 
283
- const schemaId = getSchemaId(schemaFile);
284
- const result = await validateDirectory(ajv, dirPath, schemaId);
263
+ for (const [filename, schemaFile] of Object.entries(SCHEMA_MAPPINGS)) {
264
+ if (!filename.includes(".yaml")) continue;
265
+
266
+ const filePath = join(dataDir, filename);
267
+ if (!(await this.#fileExists(filePath))) {
268
+ if (!["self-assessments.yaml"].includes(filename)) {
269
+ warnings.push(
270
+ createWarning(
271
+ "MISSING_FILE",
272
+ `Optional file not found: ${filename}`,
273
+ ),
274
+ );
275
+ }
276
+ continue;
277
+ }
285
278
 
286
- // Count files
287
- const files = await readdir(dirPath);
288
- const yamlFiles = files.filter(
289
- (f) => f.endsWith(".yaml") && !f.startsWith("_"),
290
- );
291
- stats.filesValidated += yamlFiles.length;
292
- stats.schemasUsed.add(schemaFile);
279
+ const schemaId = this.#getSchemaId(schemaFile);
280
+ const result = await this.#validateFile(ajv, filePath, schemaId);
281
+ stats.filesValidated++;
282
+ stats.schemasUsed.add(schemaFile);
293
283
 
294
- if (!result.valid) {
295
- allErrors.push(...result.errors);
284
+ if (!result.valid) {
285
+ allErrors.push(...result.errors);
286
+ }
296
287
  }
297
- }
298
288
 
299
- return createValidationResult(allErrors.length === 0, allErrors, warnings);
300
- }
289
+ for (const [dirName, schemaFile] of Object.entries(SCHEMA_MAPPINGS)) {
290
+ if (dirName.includes(".yaml")) continue;
301
291
 
302
- /**
303
- * Validate referential integrity (skill/behaviour references exist)
304
- * This supplements schema validation with cross-file reference checks
305
- * @param {Object} data - Loaded data object
306
- * @param {Array} data.skills - Skills
307
- * @param {Array} data.behaviours - Behaviours
308
- * @param {Array} data.disciplines - Disciplines
309
- * @param {Array} data.drivers - Drivers
310
- * @param {Array} data.capabilities - Capabilities
311
- * @returns {{valid: boolean, errors: Array, warnings: Array}}
312
- */
313
- export function validateReferentialIntegrity(data) {
314
- const errors = [];
315
- const warnings = [];
316
-
317
- const skillIds = new Set((data.skills || []).map((s) => s.id));
318
- const behaviourIds = new Set((data.behaviours || []).map((b) => b.id));
319
- const capabilityIds = new Set((data.capabilities || []).map((c) => c.id));
320
-
321
- // Validate discipline skill references
322
- for (const discipline of data.disciplines || []) {
323
- const allSkillRefs = [
324
- ...(discipline.coreSkills || []),
325
- ...(discipline.supportingSkills || []),
326
- ...(discipline.broadSkills || []),
327
- ];
328
-
329
- for (const skillId of allSkillRefs) {
330
- if (!skillIds.has(skillId)) {
331
- errors.push(
332
- createError(
333
- "INVALID_REFERENCE",
334
- `Discipline '${discipline.id}' references unknown skill '${skillId}'`,
335
- `disciplines/${discipline.id}`,
336
- ),
337
- );
292
+ const dirPath = join(dataDir, dirName);
293
+ if (!(await this.#isDirectory(dirPath))) {
294
+ continue;
338
295
  }
339
- }
340
296
 
341
- // Validate behaviour modifier references
342
- for (const behaviourId of Object.keys(
343
- discipline.behaviourModifiers || {},
344
- )) {
345
- if (!behaviourIds.has(behaviourId)) {
346
- errors.push(
347
- createError(
348
- "INVALID_REFERENCE",
349
- `Discipline '${discipline.id}' references unknown behaviour '${behaviourId}'`,
350
- `disciplines/${discipline.id}`,
351
- ),
352
- );
297
+ const schemaId = this.#getSchemaId(schemaFile);
298
+ const result = await this.#validateDirectory(ajv, dirPath, schemaId);
299
+
300
+ const files = await this.#fs.readdir(dirPath);
301
+ const yamlFiles = files.filter(
302
+ (f) => f.endsWith(".yaml") && !f.startsWith("_"),
303
+ );
304
+ stats.filesValidated += yamlFiles.length;
305
+ stats.schemasUsed.add(schemaFile);
306
+
307
+ if (!result.valid) {
308
+ allErrors.push(...result.errors);
353
309
  }
354
310
  }
311
+
312
+ return createValidationResult(allErrors.length === 0, allErrors, warnings);
355
313
  }
356
314
 
357
- // Validate track skill modifier references (should reference capabilities, not skills)
358
- for (const track of data.tracks || []) {
359
- for (const capabilityId of Object.keys(track.skillModifiers || {})) {
360
- if (!capabilityIds.has(capabilityId)) {
361
- errors.push(
362
- createError(
363
- "INVALID_REFERENCE",
364
- `Track '${track.id}' references unknown capability '${capabilityId}'`,
365
- `tracks/${track.id}`,
366
- ),
367
- );
315
+ /**
316
+ * Validate referential integrity (skill/behaviour references exist)
317
+ * @param {Object} data
318
+ * @returns {{valid: boolean, errors: Array, warnings: Array}}
319
+ */
320
+ validateReferentialIntegrity(data) {
321
+ const errors = [];
322
+ const warnings = [];
323
+
324
+ const skillIds = new Set((data.skills || []).map((s) => s.id));
325
+ const behaviourIds = new Set((data.behaviours || []).map((b) => b.id));
326
+ const capabilityIds = new Set((data.capabilities || []).map((c) => c.id));
327
+
328
+ for (const discipline of data.disciplines || []) {
329
+ const allSkillRefs = [
330
+ ...(discipline.coreSkills || []),
331
+ ...(discipline.supportingSkills || []),
332
+ ...(discipline.broadSkills || []),
333
+ ];
334
+
335
+ for (const skillId of allSkillRefs) {
336
+ if (!skillIds.has(skillId)) {
337
+ errors.push(
338
+ createError(
339
+ "INVALID_REFERENCE",
340
+ `Discipline '${discipline.id}' references unknown skill '${skillId}'`,
341
+ `disciplines/${discipline.id}`,
342
+ ),
343
+ );
344
+ }
368
345
  }
369
- }
370
346
 
371
- // Validate behaviour modifier references
372
- for (const behaviourId of Object.keys(track.behaviourModifiers || {})) {
373
- if (!behaviourIds.has(behaviourId)) {
374
- errors.push(
375
- createError(
376
- "INVALID_REFERENCE",
377
- `Track '${track.id}' references unknown behaviour '${behaviourId}'`,
378
- `tracks/${track.id}`,
379
- ),
380
- );
347
+ for (const behaviourId of Object.keys(
348
+ discipline.behaviourModifiers || {},
349
+ )) {
350
+ if (!behaviourIds.has(behaviourId)) {
351
+ errors.push(
352
+ createError(
353
+ "INVALID_REFERENCE",
354
+ `Discipline '${discipline.id}' references unknown behaviour '${behaviourId}'`,
355
+ `disciplines/${discipline.id}`,
356
+ ),
357
+ );
358
+ }
381
359
  }
382
360
  }
383
- }
384
361
 
385
- // Validate driver skill/behaviour references
386
- for (const driver of data.drivers || []) {
387
- for (const skillId of driver.contributingSkills || []) {
388
- if (!skillIds.has(skillId)) {
389
- errors.push(
390
- createError(
391
- "INVALID_REFERENCE",
392
- `Driver '${driver.id}' references unknown skill '${skillId}'`,
393
- `drivers`,
394
- ),
395
- );
362
+ for (const track of data.tracks || []) {
363
+ for (const capabilityId of Object.keys(track.skillModifiers || {})) {
364
+ if (!capabilityIds.has(capabilityId)) {
365
+ errors.push(
366
+ createError(
367
+ "INVALID_REFERENCE",
368
+ `Track '${track.id}' references unknown capability '${capabilityId}'`,
369
+ `tracks/${track.id}`,
370
+ ),
371
+ );
372
+ }
373
+ }
374
+
375
+ for (const behaviourId of Object.keys(track.behaviourModifiers || {})) {
376
+ if (!behaviourIds.has(behaviourId)) {
377
+ errors.push(
378
+ createError(
379
+ "INVALID_REFERENCE",
380
+ `Track '${track.id}' references unknown behaviour '${behaviourId}'`,
381
+ `tracks/${track.id}`,
382
+ ),
383
+ );
384
+ }
396
385
  }
397
386
  }
398
387
 
399
- for (const behaviourId of driver.contributingBehaviours || []) {
400
- if (!behaviourIds.has(behaviourId)) {
401
- errors.push(
402
- createError(
403
- "INVALID_REFERENCE",
404
- `Driver '${driver.id}' references unknown behaviour '${behaviourId}'`,
405
- `drivers`,
406
- ),
407
- );
388
+ for (const driver of data.drivers || []) {
389
+ for (const skillId of driver.contributingSkills || []) {
390
+ if (!skillIds.has(skillId)) {
391
+ errors.push(
392
+ createError(
393
+ "INVALID_REFERENCE",
394
+ `Driver '${driver.id}' references unknown skill '${skillId}'`,
395
+ `drivers`,
396
+ ),
397
+ );
398
+ }
399
+ }
400
+
401
+ for (const behaviourId of driver.contributingBehaviours || []) {
402
+ if (!behaviourIds.has(behaviourId)) {
403
+ errors.push(
404
+ createError(
405
+ "INVALID_REFERENCE",
406
+ `Driver '${driver.id}' references unknown behaviour '${behaviourId}'`,
407
+ `drivers`,
408
+ ),
409
+ );
410
+ }
408
411
  }
409
412
  }
413
+
414
+ return createValidationResult(errors.length === 0, errors, warnings);
410
415
  }
411
416
 
412
- return createValidationResult(errors.length === 0, errors, warnings);
417
+ /**
418
+ * Run full validation: schema validation + referential integrity
419
+ * @param {string} dataDir
420
+ * @param {Object} [loadedData]
421
+ * @returns {Promise<{valid: boolean, errors: Array, warnings: Array}>}
422
+ */
423
+ async runFullValidation(dataDir, loadedData) {
424
+ const allErrors = [];
425
+ const allWarnings = [];
426
+
427
+ const schemaResult = await this.validateDataDirectory(dataDir);
428
+ allErrors.push(...schemaResult.errors);
429
+ allWarnings.push(...schemaResult.warnings);
430
+
431
+ if (loadedData) {
432
+ const refResult = this.validateReferentialIntegrity(loadedData);
433
+ allErrors.push(...refResult.errors);
434
+ allWarnings.push(...refResult.warnings);
435
+ }
436
+
437
+ return createValidationResult(
438
+ allErrors.length === 0,
439
+ allErrors,
440
+ allWarnings,
441
+ );
442
+ }
413
443
  }
414
444
 
415
445
  /**
416
- * Run full validation: schema validation + referential integrity
417
- * @param {string} dataDir - Path to the data directory
418
- * @param {Object} [loadedData] - Pre-loaded data (if available, skips schema validation stats gathering)
419
- * @returns {Promise<{valid: boolean, errors: Array, warnings: Array}>}
446
+ * Create a SchemaValidator with real dependencies
447
+ * @returns {SchemaValidator}
420
448
  */
421
- export async function runSchemaValidation(dataDir, loadedData) {
422
- const allErrors = [];
423
- const allWarnings = [];
424
-
425
- // Run schema validation
426
- const schemaResult = await validateDataDirectory(dataDir);
427
- allErrors.push(...schemaResult.errors);
428
- allWarnings.push(...schemaResult.warnings);
429
-
430
- // If we have loaded data, also check referential integrity
431
- if (loadedData) {
432
- const refResult = validateReferentialIntegrity(loadedData);
433
- allErrors.push(...refResult.errors);
434
- allWarnings.push(...refResult.warnings);
435
- }
436
-
437
- return createValidationResult(allErrors.length === 0, allErrors, allWarnings);
449
+ export function createSchemaValidator() {
450
+ const schemaDir = join(
451
+ dirname(fileURLToPath(import.meta.url)),
452
+ "../schema/json",
453
+ );
454
+ return new SchemaValidator({ readFile, readdir, stat }, schemaDir, {
455
+ Ajv,
456
+ addFormats,
457
+ });
438
458
  }