@forwardimpact/map 0.12.0 → 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.
- package/README.md +1 -1
- package/bin/fit-map.js +12 -12
- package/package.json +9 -6
- package/schema/json/discipline.schema.json +2 -6
- package/schema/rdf/discipline.ttl +6 -19
- package/src/index-generator.js +67 -38
- package/src/index.js +10 -25
- package/src/loader.js +407 -562
- package/src/schema-validation.js +327 -307
- package/examples/behaviours/_index.yaml +0 -8
- package/examples/behaviours/outcome_ownership.yaml +0 -43
- package/examples/behaviours/polymathic_knowledge.yaml +0 -41
- package/examples/behaviours/precise_communication.yaml +0 -39
- package/examples/behaviours/relentless_curiosity.yaml +0 -37
- package/examples/behaviours/systems_thinking.yaml +0 -40
- package/examples/capabilities/_index.yaml +0 -8
- package/examples/capabilities/business.yaml +0 -205
- package/examples/capabilities/delivery.yaml +0 -1001
- package/examples/capabilities/people.yaml +0 -68
- package/examples/capabilities/reliability.yaml +0 -349
- package/examples/capabilities/scale.yaml +0 -1672
- package/examples/copilot-setup-steps.yaml +0 -25
- package/examples/devcontainer.yaml +0 -21
- package/examples/disciplines/_index.yaml +0 -6
- package/examples/disciplines/data_engineering.yaml +0 -68
- package/examples/disciplines/engineering_management.yaml +0 -61
- package/examples/disciplines/software_engineering.yaml +0 -68
- package/examples/drivers.yaml +0 -202
- package/examples/framework.yaml +0 -73
- package/examples/levels.yaml +0 -115
- package/examples/questions/behaviours/outcome_ownership.yaml +0 -228
- package/examples/questions/behaviours/polymathic_knowledge.yaml +0 -275
- package/examples/questions/behaviours/precise_communication.yaml +0 -248
- package/examples/questions/behaviours/relentless_curiosity.yaml +0 -248
- package/examples/questions/behaviours/systems_thinking.yaml +0 -238
- package/examples/questions/capabilities/business.yaml +0 -107
- package/examples/questions/capabilities/delivery.yaml +0 -101
- package/examples/questions/capabilities/people.yaml +0 -106
- package/examples/questions/capabilities/reliability.yaml +0 -105
- package/examples/questions/capabilities/scale.yaml +0 -104
- package/examples/questions/skills/architecture_design.yaml +0 -115
- package/examples/questions/skills/cloud_platforms.yaml +0 -105
- package/examples/questions/skills/code_quality.yaml +0 -162
- package/examples/questions/skills/data_modeling.yaml +0 -107
- package/examples/questions/skills/devops.yaml +0 -111
- package/examples/questions/skills/full_stack_development.yaml +0 -118
- package/examples/questions/skills/sre_practices.yaml +0 -113
- package/examples/questions/skills/stakeholder_management.yaml +0 -116
- package/examples/questions/skills/team_collaboration.yaml +0 -106
- package/examples/questions/skills/technical_writing.yaml +0 -110
- package/examples/self-assessments.yaml +0 -64
- package/examples/stages.yaml +0 -191
- package/examples/tracks/_index.yaml +0 -5
- package/examples/tracks/platform.yaml +0 -47
- package/examples/tracks/sre.yaml +0 -46
- package/examples/vscode-settings.yaml +0 -21
package/src/schema-validation.js
CHANGED
|
@@ -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
|
|
42
|
-
* @param {Array
|
|
43
|
-
* @param {Array
|
|
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
|
|
53
|
-
* @param {string} message
|
|
54
|
-
* @param {string} [path]
|
|
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
|
|
66
|
-
* @param {string} message
|
|
67
|
-
* @param {string} [path]
|
|
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
|
|
127
|
-
* @param {string} filePath
|
|
128
|
-
* @returns {Array
|
|
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
|
-
*
|
|
150
|
-
* @returns {Promise<Ajv>}
|
|
94
|
+
* Schema validator class with injectable dependencies.
|
|
151
95
|
*/
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
181
|
+
return ajv;
|
|
182
|
+
}
|
|
211
183
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
295
|
-
|
|
284
|
+
if (!result.valid) {
|
|
285
|
+
allErrors.push(...result.errors);
|
|
286
|
+
}
|
|
296
287
|
}
|
|
297
|
-
}
|
|
298
288
|
|
|
299
|
-
|
|
300
|
-
|
|
289
|
+
for (const [dirName, schemaFile] of Object.entries(SCHEMA_MAPPINGS)) {
|
|
290
|
+
if (dirName.includes(".yaml")) continue;
|
|
301
291
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
417
|
-
* @
|
|
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
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
}
|