@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.
- package/bin/fit-schema.js +260 -0
- package/examples/behaviours/_index.yaml +8 -0
- package/examples/behaviours/outcome_ownership.yaml +43 -0
- package/examples/behaviours/polymathic_knowledge.yaml +41 -0
- package/examples/behaviours/precise_communication.yaml +39 -0
- package/examples/behaviours/relentless_curiosity.yaml +37 -0
- package/examples/behaviours/systems_thinking.yaml +40 -0
- package/examples/capabilities/_index.yaml +8 -0
- package/examples/capabilities/business.yaml +189 -0
- package/examples/capabilities/delivery.yaml +305 -0
- package/examples/capabilities/people.yaml +68 -0
- package/examples/capabilities/reliability.yaml +414 -0
- package/examples/capabilities/scale.yaml +378 -0
- package/examples/copilot-setup-steps.yaml +25 -0
- package/examples/devcontainer.yaml +21 -0
- package/examples/disciplines/_index.yaml +6 -0
- package/examples/disciplines/data_engineering.yaml +78 -0
- package/examples/disciplines/engineering_management.yaml +63 -0
- package/examples/disciplines/software_engineering.yaml +78 -0
- package/examples/drivers.yaml +202 -0
- package/examples/framework.yaml +69 -0
- package/examples/grades.yaml +115 -0
- package/examples/questions/behaviours/outcome_ownership.yaml +51 -0
- package/examples/questions/behaviours/polymathic_knowledge.yaml +47 -0
- package/examples/questions/behaviours/precise_communication.yaml +54 -0
- package/examples/questions/behaviours/relentless_curiosity.yaml +50 -0
- package/examples/questions/behaviours/systems_thinking.yaml +52 -0
- package/examples/questions/skills/architecture_design.yaml +53 -0
- package/examples/questions/skills/cloud_platforms.yaml +47 -0
- package/examples/questions/skills/code_quality.yaml +48 -0
- package/examples/questions/skills/data_modeling.yaml +45 -0
- package/examples/questions/skills/devops.yaml +46 -0
- package/examples/questions/skills/full_stack_development.yaml +47 -0
- package/examples/questions/skills/sre_practices.yaml +43 -0
- package/examples/questions/skills/stakeholder_management.yaml +48 -0
- package/examples/questions/skills/team_collaboration.yaml +42 -0
- package/examples/questions/skills/technical_writing.yaml +42 -0
- package/examples/self-assessments.yaml +64 -0
- package/examples/stages.yaml +139 -0
- package/examples/tracks/_index.yaml +5 -0
- package/examples/tracks/platform.yaml +49 -0
- package/examples/tracks/sre.yaml +48 -0
- package/examples/vscode-settings.yaml +21 -0
- package/lib/index-generator.js +65 -0
- package/lib/index.js +44 -0
- package/lib/levels.js +601 -0
- package/lib/loader.js +599 -0
- package/lib/modifiers.js +23 -0
- package/lib/schema-validation.js +438 -0
- package/lib/validation.js +2130 -0
- package/package.json +49 -0
- package/schema/json/behaviour-questions.schema.json +68 -0
- package/schema/json/behaviour.schema.json +73 -0
- package/schema/json/capability.schema.json +220 -0
- package/schema/json/defs.schema.json +132 -0
- package/schema/json/discipline.schema.json +132 -0
- package/schema/json/drivers.schema.json +48 -0
- package/schema/json/framework.schema.json +55 -0
- package/schema/json/grades.schema.json +121 -0
- package/schema/json/index.schema.json +18 -0
- package/schema/json/self-assessments.schema.json +52 -0
- package/schema/json/skill-questions.schema.json +68 -0
- package/schema/json/stages.schema.json +84 -0
- package/schema/json/track.schema.json +100 -0
- package/schema/rdf/pathway.ttl +2362 -0
|
@@ -0,0 +1,2130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engineering Pathway Validation Functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides comprehensive data validation with referential integrity checks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Capability,
|
|
9
|
+
Stage,
|
|
10
|
+
getSkillLevelIndex,
|
|
11
|
+
getBehaviourMaturityIndex,
|
|
12
|
+
} from "./levels.js";
|
|
13
|
+
|
|
14
|
+
import { isCapability } from "./modifiers.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a validation result object
|
|
18
|
+
* @param {boolean} valid - Whether validation passed
|
|
19
|
+
* @param {Array} errors - Array of errors
|
|
20
|
+
* @param {Array} warnings - Array of warnings
|
|
21
|
+
* @returns {import('./levels.js').ValidationResult}
|
|
22
|
+
*/
|
|
23
|
+
function createValidationResult(valid, errors = [], warnings = []) {
|
|
24
|
+
return { valid, errors, warnings };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a validation error
|
|
29
|
+
* @param {string} type - Error type
|
|
30
|
+
* @param {string} message - Error message
|
|
31
|
+
* @param {string} [path] - Path to invalid data
|
|
32
|
+
* @param {*} [value] - Invalid value
|
|
33
|
+
* @returns {import('./levels.js').ValidationError}
|
|
34
|
+
*/
|
|
35
|
+
function createError(type, message, path, value) {
|
|
36
|
+
const error = { type, message };
|
|
37
|
+
if (path !== undefined) error.path = path;
|
|
38
|
+
if (value !== undefined) error.value = value;
|
|
39
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a validation warning
|
|
44
|
+
* @param {string} type - Warning type
|
|
45
|
+
* @param {string} message - Warning message
|
|
46
|
+
* @param {string} [path] - Path to concerning data
|
|
47
|
+
* @returns {import('./levels.js').ValidationWarning}
|
|
48
|
+
*/
|
|
49
|
+
function createWarning(type, message, path) {
|
|
50
|
+
const warning = { type, message };
|
|
51
|
+
if (path !== undefined) warning.path = path;
|
|
52
|
+
return warning;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate that a skill has required properties
|
|
57
|
+
* @param {import('./levels.js').Skill} skill - Skill to validate
|
|
58
|
+
* @param {number} index - Index in the skills array
|
|
59
|
+
* @param {string[]} [requiredStageIds] - Stage IDs that must be present in agent skills
|
|
60
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
61
|
+
*/
|
|
62
|
+
function validateSkill(skill, index, requiredStageIds = []) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const path = `skills[${index}]`;
|
|
66
|
+
|
|
67
|
+
if (!skill.id) {
|
|
68
|
+
errors.push(createError("MISSING_REQUIRED", "Skill missing id", path));
|
|
69
|
+
}
|
|
70
|
+
if (!skill.name) {
|
|
71
|
+
errors.push(
|
|
72
|
+
createError("MISSING_REQUIRED", "Skill missing name", `${path}.name`),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!skill.capability) {
|
|
76
|
+
errors.push(
|
|
77
|
+
createError(
|
|
78
|
+
"MISSING_REQUIRED",
|
|
79
|
+
"Skill missing capability",
|
|
80
|
+
`${path}.capability`,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
} else if (!Object.values(Capability).includes(skill.capability)) {
|
|
84
|
+
errors.push(
|
|
85
|
+
createError(
|
|
86
|
+
"INVALID_VALUE",
|
|
87
|
+
`Invalid skill capability: ${skill.capability}`,
|
|
88
|
+
`${path}.capability`,
|
|
89
|
+
skill.capability,
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!skill.description) {
|
|
94
|
+
warnings.push(
|
|
95
|
+
createWarning(
|
|
96
|
+
"MISSING_OPTIONAL",
|
|
97
|
+
"Skill missing description",
|
|
98
|
+
`${path}.description`,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (!skill.levelDescriptions) {
|
|
103
|
+
warnings.push(
|
|
104
|
+
createWarning(
|
|
105
|
+
"MISSING_OPTIONAL",
|
|
106
|
+
"Skill missing level descriptions",
|
|
107
|
+
`${path}.levelDescriptions`,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate agent section if present
|
|
113
|
+
if (skill.agent) {
|
|
114
|
+
const agentPath = `${path}.agent`;
|
|
115
|
+
if (!skill.agent.name) {
|
|
116
|
+
errors.push(
|
|
117
|
+
createError(
|
|
118
|
+
"MISSING_REQUIRED",
|
|
119
|
+
"Skill agent section missing name",
|
|
120
|
+
`${agentPath}.name`,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (!skill.agent.description) {
|
|
125
|
+
errors.push(
|
|
126
|
+
createError(
|
|
127
|
+
"MISSING_REQUIRED",
|
|
128
|
+
"Skill agent section missing description",
|
|
129
|
+
`${agentPath}.description`,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Validate stages (required for agent skills)
|
|
135
|
+
if (!skill.agent.stages) {
|
|
136
|
+
errors.push(
|
|
137
|
+
createError(
|
|
138
|
+
"MISSING_REQUIRED",
|
|
139
|
+
"Skill agent section missing stages",
|
|
140
|
+
`${agentPath}.stages`,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
} else if (typeof skill.agent.stages !== "object") {
|
|
144
|
+
errors.push(
|
|
145
|
+
createError(
|
|
146
|
+
"INVALID_VALUE",
|
|
147
|
+
"Skill agent stages must be an object",
|
|
148
|
+
`${agentPath}.stages`,
|
|
149
|
+
skill.agent.stages,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
} else {
|
|
153
|
+
// Validate each stage
|
|
154
|
+
const validStageIds = Object.values(Stage);
|
|
155
|
+
for (const [stageId, stageData] of Object.entries(skill.agent.stages)) {
|
|
156
|
+
if (!validStageIds.includes(stageId)) {
|
|
157
|
+
errors.push(
|
|
158
|
+
createError(
|
|
159
|
+
"INVALID_VALUE",
|
|
160
|
+
`Invalid stage ID: ${stageId}. Must be one of: ${validStageIds.join(", ")}`,
|
|
161
|
+
`${agentPath}.stages.${stageId}`,
|
|
162
|
+
stageId,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const stagePath = `${agentPath}.stages.${stageId}`;
|
|
168
|
+
// focus is required
|
|
169
|
+
if (!stageData.focus) {
|
|
170
|
+
errors.push(
|
|
171
|
+
createError(
|
|
172
|
+
"MISSING_REQUIRED",
|
|
173
|
+
`Stage ${stageId} missing focus`,
|
|
174
|
+
`${stagePath}.focus`,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
} else if (typeof stageData.focus !== "string") {
|
|
178
|
+
errors.push(
|
|
179
|
+
createError(
|
|
180
|
+
"INVALID_VALUE",
|
|
181
|
+
`Stage ${stageId} focus must be a string`,
|
|
182
|
+
`${stagePath}.focus`,
|
|
183
|
+
stageData.focus,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
// activities is required and must be an array
|
|
188
|
+
if (!stageData.activities) {
|
|
189
|
+
errors.push(
|
|
190
|
+
createError(
|
|
191
|
+
"MISSING_REQUIRED",
|
|
192
|
+
`Stage ${stageId} missing activities`,
|
|
193
|
+
`${stagePath}.activities`,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
} else if (!Array.isArray(stageData.activities)) {
|
|
197
|
+
errors.push(
|
|
198
|
+
createError(
|
|
199
|
+
"INVALID_VALUE",
|
|
200
|
+
`Stage ${stageId} activities must be an array`,
|
|
201
|
+
`${stagePath}.activities`,
|
|
202
|
+
stageData.activities,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
// ready is required and must be an array (these become checklist items)
|
|
207
|
+
if (!stageData.ready) {
|
|
208
|
+
errors.push(
|
|
209
|
+
createError(
|
|
210
|
+
"MISSING_REQUIRED",
|
|
211
|
+
`Stage ${stageId} missing ready criteria`,
|
|
212
|
+
`${stagePath}.ready`,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
} else if (!Array.isArray(stageData.ready)) {
|
|
216
|
+
errors.push(
|
|
217
|
+
createError(
|
|
218
|
+
"INVALID_VALUE",
|
|
219
|
+
`Stage ${stageId} ready must be an array`,
|
|
220
|
+
`${stagePath}.ready`,
|
|
221
|
+
stageData.ready,
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check that all required stages are present
|
|
228
|
+
if (requiredStageIds.length > 0) {
|
|
229
|
+
const presentStageIds = Object.keys(skill.agent.stages);
|
|
230
|
+
for (const requiredStageId of requiredStageIds) {
|
|
231
|
+
if (!presentStageIds.includes(requiredStageId)) {
|
|
232
|
+
errors.push(
|
|
233
|
+
createError(
|
|
234
|
+
"MISSING_REQUIRED",
|
|
235
|
+
`Skill agent missing required stage: ${requiredStageId}`,
|
|
236
|
+
`${agentPath}.stages.${requiredStageId}`,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Error if old 'reference' field is still present (moved to skill.implementationReference)
|
|
245
|
+
if (skill.agent.reference !== undefined) {
|
|
246
|
+
errors.push(
|
|
247
|
+
createError(
|
|
248
|
+
"INVALID_FIELD",
|
|
249
|
+
"Skill agent 'reference' field is not supported. Use skill.implementationReference instead.",
|
|
250
|
+
`${agentPath}.reference`,
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Error if old fields are still present
|
|
256
|
+
if (skill.agent.body !== undefined) {
|
|
257
|
+
errors.push(
|
|
258
|
+
createError(
|
|
259
|
+
"INVALID_FIELD",
|
|
260
|
+
"Skill agent 'body' field is not supported. Use stages instead.",
|
|
261
|
+
`${agentPath}.body`,
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (skill.agent.applicability !== undefined) {
|
|
266
|
+
errors.push(
|
|
267
|
+
createError(
|
|
268
|
+
"INVALID_FIELD",
|
|
269
|
+
"Skill agent 'applicability' field is not supported. Use stages instead.",
|
|
270
|
+
`${agentPath}.applicability`,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (skill.agent.guidance !== undefined) {
|
|
275
|
+
errors.push(
|
|
276
|
+
createError(
|
|
277
|
+
"INVALID_FIELD",
|
|
278
|
+
"Skill agent 'guidance' field is not supported. Use stages instead.",
|
|
279
|
+
`${agentPath}.guidance`,
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (skill.agent.verificationCriteria !== undefined) {
|
|
284
|
+
errors.push(
|
|
285
|
+
createError(
|
|
286
|
+
"INVALID_FIELD",
|
|
287
|
+
"Skill agent 'verificationCriteria' field is not supported. Use stages.{stage}.ready instead.",
|
|
288
|
+
`${agentPath}.verificationCriteria`,
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Validate implementationReference if present (optional string)
|
|
295
|
+
if (
|
|
296
|
+
skill.implementationReference !== undefined &&
|
|
297
|
+
typeof skill.implementationReference !== "string"
|
|
298
|
+
) {
|
|
299
|
+
errors.push(
|
|
300
|
+
createError(
|
|
301
|
+
"INVALID_VALUE",
|
|
302
|
+
"Skill implementationReference must be a string",
|
|
303
|
+
`${path}.implementationReference`,
|
|
304
|
+
skill.implementationReference,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Validate toolReferences array if present
|
|
310
|
+
if (skill.toolReferences !== undefined) {
|
|
311
|
+
if (!Array.isArray(skill.toolReferences)) {
|
|
312
|
+
errors.push(
|
|
313
|
+
createError(
|
|
314
|
+
"INVALID_VALUE",
|
|
315
|
+
"Skill toolReferences must be an array",
|
|
316
|
+
`${path}.toolReferences`,
|
|
317
|
+
skill.toolReferences,
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
skill.toolReferences.forEach((tool, i) => {
|
|
322
|
+
const toolPath = `${path}.toolReferences[${i}]`;
|
|
323
|
+
if (!tool.name) {
|
|
324
|
+
errors.push(
|
|
325
|
+
createError(
|
|
326
|
+
"MISSING_REQUIRED",
|
|
327
|
+
"Tool reference missing name",
|
|
328
|
+
`${toolPath}.name`,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (!tool.description) {
|
|
333
|
+
errors.push(
|
|
334
|
+
createError(
|
|
335
|
+
"MISSING_REQUIRED",
|
|
336
|
+
"Tool reference missing description",
|
|
337
|
+
`${toolPath}.description`,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (!tool.useWhen) {
|
|
342
|
+
errors.push(
|
|
343
|
+
createError(
|
|
344
|
+
"MISSING_REQUIRED",
|
|
345
|
+
"Tool reference missing useWhen",
|
|
346
|
+
`${toolPath}.useWhen`,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (tool.url !== undefined && typeof tool.url !== "string") {
|
|
351
|
+
errors.push(
|
|
352
|
+
createError(
|
|
353
|
+
"INVALID_VALUE",
|
|
354
|
+
"Tool reference url must be a string",
|
|
355
|
+
`${toolPath}.url`,
|
|
356
|
+
tool.url,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { errors, warnings };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Validate that a behaviour has required properties
|
|
369
|
+
* @param {import('./levels.js').Behaviour} behaviour - Behaviour to validate
|
|
370
|
+
* @param {number} index - Index in the behaviours array
|
|
371
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
372
|
+
*/
|
|
373
|
+
function validateBehaviour(behaviour, index) {
|
|
374
|
+
const errors = [];
|
|
375
|
+
const warnings = [];
|
|
376
|
+
const path = `behaviours[${index}]`;
|
|
377
|
+
|
|
378
|
+
// id is derived from filename by the loader
|
|
379
|
+
if (!behaviour.name) {
|
|
380
|
+
errors.push(
|
|
381
|
+
createError("MISSING_REQUIRED", "Behaviour missing name", `${path}.name`),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (!behaviour.description) {
|
|
385
|
+
warnings.push(
|
|
386
|
+
createWarning(
|
|
387
|
+
"MISSING_OPTIONAL",
|
|
388
|
+
"Behaviour missing description",
|
|
389
|
+
`${path}.description`,
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (!behaviour.maturityDescriptions) {
|
|
394
|
+
warnings.push(
|
|
395
|
+
createWarning(
|
|
396
|
+
"MISSING_OPTIONAL",
|
|
397
|
+
"Behaviour missing maturity descriptions",
|
|
398
|
+
`${path}.maturityDescriptions`,
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Validate agent section if present
|
|
404
|
+
if (behaviour.agent) {
|
|
405
|
+
const agentPath = `${path}.agent`;
|
|
406
|
+
|
|
407
|
+
// title is required for agent behaviours
|
|
408
|
+
if (!behaviour.agent.title) {
|
|
409
|
+
errors.push(
|
|
410
|
+
createError(
|
|
411
|
+
"MISSING_REQUIRED",
|
|
412
|
+
"Behaviour agent section missing title",
|
|
413
|
+
`${agentPath}.title`,
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
} else if (typeof behaviour.agent.title !== "string") {
|
|
417
|
+
errors.push(
|
|
418
|
+
createError(
|
|
419
|
+
"INVALID_VALUE",
|
|
420
|
+
"Behaviour agent title must be a string",
|
|
421
|
+
`${agentPath}.title`,
|
|
422
|
+
behaviour.agent.title,
|
|
423
|
+
),
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// workingStyle is required for agent behaviours
|
|
428
|
+
if (!behaviour.agent.workingStyle) {
|
|
429
|
+
errors.push(
|
|
430
|
+
createError(
|
|
431
|
+
"MISSING_REQUIRED",
|
|
432
|
+
"Behaviour agent section missing workingStyle",
|
|
433
|
+
`${agentPath}.workingStyle`,
|
|
434
|
+
),
|
|
435
|
+
);
|
|
436
|
+
} else if (typeof behaviour.agent.workingStyle !== "string") {
|
|
437
|
+
errors.push(
|
|
438
|
+
createError(
|
|
439
|
+
"INVALID_VALUE",
|
|
440
|
+
"Behaviour agent workingStyle must be a string",
|
|
441
|
+
`${agentPath}.workingStyle`,
|
|
442
|
+
behaviour.agent.workingStyle,
|
|
443
|
+
),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return { errors, warnings };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Validate that a driver has required properties and valid references
|
|
453
|
+
* @param {import('./levels.js').Driver} driver - Driver to validate
|
|
454
|
+
* @param {number} index - Index in the drivers array
|
|
455
|
+
* @param {Set<string>} skillIds - Set of valid skill IDs
|
|
456
|
+
* @param {Set<string>} behaviourIds - Set of valid behaviour IDs
|
|
457
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
458
|
+
*/
|
|
459
|
+
function validateDriver(driver, index, skillIds, behaviourIds) {
|
|
460
|
+
const errors = [];
|
|
461
|
+
const warnings = [];
|
|
462
|
+
const path = `drivers[${index}]`;
|
|
463
|
+
|
|
464
|
+
if (!driver.id) {
|
|
465
|
+
errors.push(createError("MISSING_REQUIRED", "Driver missing id", path));
|
|
466
|
+
}
|
|
467
|
+
if (!driver.name) {
|
|
468
|
+
errors.push(
|
|
469
|
+
createError("MISSING_REQUIRED", "Driver missing name", `${path}.name`),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (!driver.description) {
|
|
473
|
+
warnings.push(
|
|
474
|
+
createWarning(
|
|
475
|
+
"MISSING_OPTIONAL",
|
|
476
|
+
"Driver missing description",
|
|
477
|
+
`${path}.description`,
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Validate contributing skills
|
|
483
|
+
if (driver.contributingSkills) {
|
|
484
|
+
driver.contributingSkills.forEach((skillId, i) => {
|
|
485
|
+
if (!skillIds.has(skillId)) {
|
|
486
|
+
errors.push(
|
|
487
|
+
createError(
|
|
488
|
+
"INVALID_REFERENCE",
|
|
489
|
+
`Driver "${driver.id}" references non-existent skill: ${skillId}`,
|
|
490
|
+
`${path}.contributingSkills[${i}]`,
|
|
491
|
+
skillId,
|
|
492
|
+
),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Validate contributing behaviours
|
|
499
|
+
if (driver.contributingBehaviours) {
|
|
500
|
+
driver.contributingBehaviours.forEach((behaviourId, i) => {
|
|
501
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
502
|
+
errors.push(
|
|
503
|
+
createError(
|
|
504
|
+
"INVALID_REFERENCE",
|
|
505
|
+
`Driver "${driver.id}" references non-existent behaviour: ${behaviourId}`,
|
|
506
|
+
`${path}.contributingBehaviours[${i}]`,
|
|
507
|
+
behaviourId,
|
|
508
|
+
),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return { errors, warnings };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Validate that a discipline has required properties and valid references
|
|
519
|
+
* @param {import('./levels.js').Discipline} discipline - Discipline to validate
|
|
520
|
+
* @param {number} index - Index in the disciplines array
|
|
521
|
+
* @param {Set<string>} skillIds - Set of valid skill IDs
|
|
522
|
+
* @param {Set<string>} behaviourIds - Set of valid behaviour IDs
|
|
523
|
+
* @param {Set<string>} trackIds - Set of valid track IDs
|
|
524
|
+
* @param {Set<string>} gradeIds - Set of valid grade IDs
|
|
525
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
526
|
+
*/
|
|
527
|
+
function validateDiscipline(
|
|
528
|
+
discipline,
|
|
529
|
+
index,
|
|
530
|
+
skillIds,
|
|
531
|
+
behaviourIds,
|
|
532
|
+
trackIds,
|
|
533
|
+
gradeIds,
|
|
534
|
+
) {
|
|
535
|
+
const errors = [];
|
|
536
|
+
const warnings = [];
|
|
537
|
+
const path = `disciplines[${index}]`;
|
|
538
|
+
|
|
539
|
+
// id is derived from filename by the loader
|
|
540
|
+
if (!discipline.specialization) {
|
|
541
|
+
errors.push(
|
|
542
|
+
createError(
|
|
543
|
+
"MISSING_REQUIRED",
|
|
544
|
+
"Discipline missing specialization",
|
|
545
|
+
`${path}.specialization`,
|
|
546
|
+
),
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (!discipline.roleTitle) {
|
|
550
|
+
errors.push(
|
|
551
|
+
createError(
|
|
552
|
+
"MISSING_REQUIRED",
|
|
553
|
+
"Discipline missing roleTitle",
|
|
554
|
+
`${path}.roleTitle`,
|
|
555
|
+
),
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Validate validTracks (REQUIRED - must be an array)
|
|
560
|
+
// - null in array = allow trackless (generalist)
|
|
561
|
+
// - string values = specific track IDs
|
|
562
|
+
// - empty array = no valid combinations (discipline cannot be used)
|
|
563
|
+
if (!Array.isArray(discipline.validTracks)) {
|
|
564
|
+
errors.push(
|
|
565
|
+
createError(
|
|
566
|
+
"MISSING_REQUIRED",
|
|
567
|
+
`Discipline "${discipline.id}" missing required validTracks array`,
|
|
568
|
+
`${path}.validTracks`,
|
|
569
|
+
),
|
|
570
|
+
);
|
|
571
|
+
} else {
|
|
572
|
+
discipline.validTracks.forEach((trackId, i) => {
|
|
573
|
+
// null means "allow trackless" - skip validation
|
|
574
|
+
if (trackId === null) return;
|
|
575
|
+
if (!trackIds.has(trackId)) {
|
|
576
|
+
errors.push(
|
|
577
|
+
createError(
|
|
578
|
+
"INVALID_REFERENCE",
|
|
579
|
+
`Discipline "${discipline.id}" references non-existent track: ${trackId}`,
|
|
580
|
+
`${path}.validTracks[${i}]`,
|
|
581
|
+
trackId,
|
|
582
|
+
),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Validate minGrade if specified
|
|
589
|
+
if (discipline.minGrade) {
|
|
590
|
+
if (!gradeIds.has(discipline.minGrade)) {
|
|
591
|
+
errors.push(
|
|
592
|
+
createError(
|
|
593
|
+
"INVALID_REFERENCE",
|
|
594
|
+
`Discipline "${discipline.id}" references non-existent grade: ${discipline.minGrade}`,
|
|
595
|
+
`${path}.minGrade`,
|
|
596
|
+
discipline.minGrade,
|
|
597
|
+
),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Validate isManagement/isProfessional booleans (optional)
|
|
603
|
+
if (
|
|
604
|
+
discipline.isManagement !== undefined &&
|
|
605
|
+
typeof discipline.isManagement !== "boolean"
|
|
606
|
+
) {
|
|
607
|
+
errors.push(
|
|
608
|
+
createError(
|
|
609
|
+
"INVALID_VALUE",
|
|
610
|
+
`Discipline "${discipline.id}" has invalid isManagement value: ${discipline.isManagement} (must be boolean)`,
|
|
611
|
+
`${path}.isManagement`,
|
|
612
|
+
discipline.isManagement,
|
|
613
|
+
),
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (
|
|
617
|
+
discipline.isProfessional !== undefined &&
|
|
618
|
+
typeof discipline.isProfessional !== "boolean"
|
|
619
|
+
) {
|
|
620
|
+
errors.push(
|
|
621
|
+
createError(
|
|
622
|
+
"INVALID_VALUE",
|
|
623
|
+
`Discipline "${discipline.id}" has invalid isProfessional value: ${discipline.isProfessional} (must be boolean)`,
|
|
624
|
+
`${path}.isProfessional`,
|
|
625
|
+
discipline.isProfessional,
|
|
626
|
+
),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Validate core skills
|
|
631
|
+
if (!discipline.coreSkills || discipline.coreSkills.length === 0) {
|
|
632
|
+
errors.push(
|
|
633
|
+
createError(
|
|
634
|
+
"MISSING_REQUIRED",
|
|
635
|
+
"Discipline must have at least one core skill",
|
|
636
|
+
`${path}.coreSkills`,
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
} else {
|
|
640
|
+
discipline.coreSkills.forEach((skillId, i) => {
|
|
641
|
+
if (!skillIds.has(skillId)) {
|
|
642
|
+
errors.push(
|
|
643
|
+
createError(
|
|
644
|
+
"INVALID_REFERENCE",
|
|
645
|
+
`Discipline "${discipline.id}" references non-existent core skill: ${skillId}`,
|
|
646
|
+
`${path}.coreSkills[${i}]`,
|
|
647
|
+
skillId,
|
|
648
|
+
),
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Validate supporting skills
|
|
655
|
+
if (discipline.supportingSkills) {
|
|
656
|
+
discipline.supportingSkills.forEach((skillId, i) => {
|
|
657
|
+
if (!skillIds.has(skillId)) {
|
|
658
|
+
errors.push(
|
|
659
|
+
createError(
|
|
660
|
+
"INVALID_REFERENCE",
|
|
661
|
+
`Discipline "${discipline.id}" references non-existent supporting skill: ${skillId}`,
|
|
662
|
+
`${path}.supportingSkills[${i}]`,
|
|
663
|
+
skillId,
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Validate broad skills
|
|
671
|
+
if (discipline.broadSkills) {
|
|
672
|
+
discipline.broadSkills.forEach((skillId, i) => {
|
|
673
|
+
if (!skillIds.has(skillId)) {
|
|
674
|
+
errors.push(
|
|
675
|
+
createError(
|
|
676
|
+
"INVALID_REFERENCE",
|
|
677
|
+
`Discipline "${discipline.id}" references non-existent broad skill: ${skillId}`,
|
|
678
|
+
`${path}.broadSkills[${i}]`,
|
|
679
|
+
skillId,
|
|
680
|
+
),
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Validate behaviour modifiers
|
|
687
|
+
if (discipline.behaviourModifiers) {
|
|
688
|
+
Object.entries(discipline.behaviourModifiers).forEach(
|
|
689
|
+
([behaviourId, modifier]) => {
|
|
690
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
691
|
+
errors.push(
|
|
692
|
+
createError(
|
|
693
|
+
"INVALID_REFERENCE",
|
|
694
|
+
`Discipline "${discipline.id}" references non-existent behaviour: ${behaviourId}`,
|
|
695
|
+
`${path}.behaviourModifiers.${behaviourId}`,
|
|
696
|
+
behaviourId,
|
|
697
|
+
),
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
if (typeof modifier !== "number" || modifier < -1 || modifier > 1) {
|
|
701
|
+
errors.push(
|
|
702
|
+
createError(
|
|
703
|
+
"INVALID_VALUE",
|
|
704
|
+
`Discipline "${discipline.id}" has invalid behaviour modifier: ${modifier} (must be -1, 0, or 1)`,
|
|
705
|
+
`${path}.behaviourModifiers.${behaviourId}`,
|
|
706
|
+
modifier,
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Validate agent section if present
|
|
715
|
+
if (discipline.agent) {
|
|
716
|
+
const agentPath = `${path}.agent`;
|
|
717
|
+
|
|
718
|
+
// Required: identity
|
|
719
|
+
if (!discipline.agent.identity) {
|
|
720
|
+
errors.push(
|
|
721
|
+
createError(
|
|
722
|
+
"MISSING_REQUIRED",
|
|
723
|
+
"Discipline agent section missing identity",
|
|
724
|
+
`${agentPath}.identity`,
|
|
725
|
+
),
|
|
726
|
+
);
|
|
727
|
+
} else if (typeof discipline.agent.identity !== "string") {
|
|
728
|
+
errors.push(
|
|
729
|
+
createError(
|
|
730
|
+
"INVALID_VALUE",
|
|
731
|
+
"Discipline agent identity must be a string",
|
|
732
|
+
`${agentPath}.identity`,
|
|
733
|
+
discipline.agent.identity,
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Optional: priority (string)
|
|
739
|
+
if (
|
|
740
|
+
discipline.agent.priority !== undefined &&
|
|
741
|
+
typeof discipline.agent.priority !== "string"
|
|
742
|
+
) {
|
|
743
|
+
errors.push(
|
|
744
|
+
createError(
|
|
745
|
+
"INVALID_VALUE",
|
|
746
|
+
"Discipline agent priority must be a string",
|
|
747
|
+
`${agentPath}.priority`,
|
|
748
|
+
discipline.agent.priority,
|
|
749
|
+
),
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Optional: beforeMakingChanges (array of strings)
|
|
754
|
+
if (discipline.agent.beforeMakingChanges !== undefined) {
|
|
755
|
+
if (!Array.isArray(discipline.agent.beforeMakingChanges)) {
|
|
756
|
+
errors.push(
|
|
757
|
+
createError(
|
|
758
|
+
"INVALID_VALUE",
|
|
759
|
+
"Discipline agent beforeMakingChanges must be an array",
|
|
760
|
+
`${agentPath}.beforeMakingChanges`,
|
|
761
|
+
discipline.agent.beforeMakingChanges,
|
|
762
|
+
),
|
|
763
|
+
);
|
|
764
|
+
} else {
|
|
765
|
+
discipline.agent.beforeMakingChanges.forEach((item, i) => {
|
|
766
|
+
if (typeof item !== "string") {
|
|
767
|
+
errors.push(
|
|
768
|
+
createError(
|
|
769
|
+
"INVALID_VALUE",
|
|
770
|
+
"Discipline agent beforeMakingChanges items must be strings",
|
|
771
|
+
`${agentPath}.beforeMakingChanges[${i}]`,
|
|
772
|
+
item,
|
|
773
|
+
),
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Optional: delegation (string)
|
|
781
|
+
if (
|
|
782
|
+
discipline.agent.delegation !== undefined &&
|
|
783
|
+
typeof discipline.agent.delegation !== "string"
|
|
784
|
+
) {
|
|
785
|
+
errors.push(
|
|
786
|
+
createError(
|
|
787
|
+
"INVALID_VALUE",
|
|
788
|
+
"Discipline agent delegation must be a string",
|
|
789
|
+
`${agentPath}.delegation`,
|
|
790
|
+
discipline.agent.delegation,
|
|
791
|
+
),
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Optional: constraints (array of strings)
|
|
796
|
+
if (discipline.agent.constraints !== undefined) {
|
|
797
|
+
if (!Array.isArray(discipline.agent.constraints)) {
|
|
798
|
+
errors.push(
|
|
799
|
+
createError(
|
|
800
|
+
"INVALID_VALUE",
|
|
801
|
+
"Discipline agent constraints must be an array",
|
|
802
|
+
`${agentPath}.constraints`,
|
|
803
|
+
discipline.agent.constraints,
|
|
804
|
+
),
|
|
805
|
+
);
|
|
806
|
+
} else {
|
|
807
|
+
discipline.agent.constraints.forEach((item, i) => {
|
|
808
|
+
if (typeof item !== "string") {
|
|
809
|
+
errors.push(
|
|
810
|
+
createError(
|
|
811
|
+
"INVALID_VALUE",
|
|
812
|
+
"Discipline agent constraints items must be strings",
|
|
813
|
+
`${agentPath}.constraints[${i}]`,
|
|
814
|
+
item,
|
|
815
|
+
),
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Error if old 'coreInstructions' field is still present
|
|
823
|
+
if (discipline.agent.coreInstructions !== undefined) {
|
|
824
|
+
errors.push(
|
|
825
|
+
createError(
|
|
826
|
+
"INVALID_FIELD",
|
|
827
|
+
"Discipline agent 'coreInstructions' field is not supported. Use identity, priority, beforeMakingChanges, and delegation instead.",
|
|
828
|
+
`${agentPath}.coreInstructions`,
|
|
829
|
+
),
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return { errors, warnings };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Get all skill IDs referenced by any discipline
|
|
839
|
+
* @param {import('./levels.js').Discipline[]} disciplines - Array of disciplines
|
|
840
|
+
* @returns {Set<string>} Set of all referenced skill IDs
|
|
841
|
+
*/
|
|
842
|
+
function getAllDisciplineSkillIds(disciplines) {
|
|
843
|
+
const skillIds = new Set();
|
|
844
|
+
for (const discipline of disciplines) {
|
|
845
|
+
(discipline.coreSkills || []).forEach((id) => skillIds.add(id));
|
|
846
|
+
(discipline.supportingSkills || []).forEach((id) => skillIds.add(id));
|
|
847
|
+
(discipline.broadSkills || []).forEach((id) => skillIds.add(id));
|
|
848
|
+
}
|
|
849
|
+
return skillIds;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Validate that a track has required properties and valid references
|
|
854
|
+
* @param {import('./levels.js').Track} track - Track to validate
|
|
855
|
+
* @param {number} index - Index in the tracks array
|
|
856
|
+
* @param {Set<string>} disciplineSkillIds - Set of skill IDs used in any discipline
|
|
857
|
+
* @param {Set<string>} behaviourIds - Set of valid behaviour IDs
|
|
858
|
+
* @param {Set<string>} gradeIds - Set of valid grade IDs
|
|
859
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
860
|
+
*/
|
|
861
|
+
function validateTrack(
|
|
862
|
+
track,
|
|
863
|
+
index,
|
|
864
|
+
disciplineSkillIds,
|
|
865
|
+
behaviourIds,
|
|
866
|
+
gradeIds,
|
|
867
|
+
) {
|
|
868
|
+
const errors = [];
|
|
869
|
+
const warnings = [];
|
|
870
|
+
const path = `tracks[${index}]`;
|
|
871
|
+
|
|
872
|
+
// id is derived from filename by the loader
|
|
873
|
+
if (!track.name) {
|
|
874
|
+
errors.push(
|
|
875
|
+
createError("MISSING_REQUIRED", "Track missing name", `${path}.name`),
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Validate skill modifiers - must be capabilities only (not individual skill IDs)
|
|
880
|
+
if (track.skillModifiers) {
|
|
881
|
+
Object.entries(track.skillModifiers).forEach(([key, modifier]) => {
|
|
882
|
+
// Key must be a capability - individual skill IDs are not allowed
|
|
883
|
+
if (!isCapability(key)) {
|
|
884
|
+
errors.push(
|
|
885
|
+
createError(
|
|
886
|
+
"INVALID_SKILL_MODIFIER_KEY",
|
|
887
|
+
`Track "${track.id}" has invalid skillModifier key "${key}". Only capability names are allowed: delivery, data, ai, scale, reliability, people, process, business, documentation`,
|
|
888
|
+
`${path}.skillModifiers.${key}`,
|
|
889
|
+
key,
|
|
890
|
+
),
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
if (typeof modifier !== "number" || !Number.isInteger(modifier)) {
|
|
894
|
+
errors.push(
|
|
895
|
+
createError(
|
|
896
|
+
"INVALID_VALUE",
|
|
897
|
+
`Track "${track.id}" has invalid skill modifier: ${modifier} (must be an integer)`,
|
|
898
|
+
`${path}.skillModifiers.${key}`,
|
|
899
|
+
modifier,
|
|
900
|
+
),
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Validate behaviour modifiers
|
|
907
|
+
if (track.behaviourModifiers) {
|
|
908
|
+
Object.entries(track.behaviourModifiers).forEach(
|
|
909
|
+
([behaviourId, modifier]) => {
|
|
910
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
911
|
+
errors.push(
|
|
912
|
+
createError(
|
|
913
|
+
"INVALID_REFERENCE",
|
|
914
|
+
`Track "${track.id}" references non-existent behaviour: ${behaviourId}`,
|
|
915
|
+
`${path}.behaviourModifiers.${behaviourId}`,
|
|
916
|
+
behaviourId,
|
|
917
|
+
),
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
if (typeof modifier !== "number" || !Number.isInteger(modifier)) {
|
|
921
|
+
errors.push(
|
|
922
|
+
createError(
|
|
923
|
+
"INVALID_VALUE",
|
|
924
|
+
`Track "${track.id}" has invalid behaviour modifier: ${modifier} (must be an integer)`,
|
|
925
|
+
`${path}.behaviourModifiers.${behaviourId}`,
|
|
926
|
+
modifier,
|
|
927
|
+
),
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Validate minGrade if specified
|
|
935
|
+
if (track.minGrade) {
|
|
936
|
+
if (!gradeIds.has(track.minGrade)) {
|
|
937
|
+
errors.push(
|
|
938
|
+
createError(
|
|
939
|
+
"INVALID_REFERENCE",
|
|
940
|
+
`Track "${track.id}" references non-existent grade: ${track.minGrade}`,
|
|
941
|
+
`${path}.minGrade`,
|
|
942
|
+
track.minGrade,
|
|
943
|
+
),
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Validate assessment weights if specified
|
|
949
|
+
if (track.assessmentWeights) {
|
|
950
|
+
const { skillWeight, behaviourWeight } = track.assessmentWeights;
|
|
951
|
+
if (typeof skillWeight !== "number" || skillWeight < 0 || skillWeight > 1) {
|
|
952
|
+
errors.push(
|
|
953
|
+
createError(
|
|
954
|
+
"INVALID_VALUE",
|
|
955
|
+
`Track "${track.id}" has invalid assessmentWeights.skillWeight: ${skillWeight}`,
|
|
956
|
+
`${path}.assessmentWeights.skillWeight`,
|
|
957
|
+
skillWeight,
|
|
958
|
+
),
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
if (
|
|
962
|
+
typeof behaviourWeight !== "number" ||
|
|
963
|
+
behaviourWeight < 0 ||
|
|
964
|
+
behaviourWeight > 1
|
|
965
|
+
) {
|
|
966
|
+
errors.push(
|
|
967
|
+
createError(
|
|
968
|
+
"INVALID_VALUE",
|
|
969
|
+
`Track "${track.id}" has invalid assessmentWeights.behaviourWeight: ${behaviourWeight}`,
|
|
970
|
+
`${path}.assessmentWeights.behaviourWeight`,
|
|
971
|
+
behaviourWeight,
|
|
972
|
+
),
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
if (
|
|
976
|
+
typeof skillWeight === "number" &&
|
|
977
|
+
typeof behaviourWeight === "number"
|
|
978
|
+
) {
|
|
979
|
+
const sum = skillWeight + behaviourWeight;
|
|
980
|
+
if (Math.abs(sum - 1.0) > 0.001) {
|
|
981
|
+
errors.push(
|
|
982
|
+
createError(
|
|
983
|
+
"INVALID_VALUE",
|
|
984
|
+
`Track "${track.id}" assessmentWeights must sum to 1.0 (got ${sum})`,
|
|
985
|
+
`${path}.assessmentWeights`,
|
|
986
|
+
{ skillWeight, behaviourWeight },
|
|
987
|
+
),
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Validate agent section if present
|
|
994
|
+
if (track.agent) {
|
|
995
|
+
const agentPath = `${path}.agent`;
|
|
996
|
+
|
|
997
|
+
// Optional: identity (string) - if provided, overrides discipline identity
|
|
998
|
+
if (
|
|
999
|
+
track.agent.identity !== undefined &&
|
|
1000
|
+
typeof track.agent.identity !== "string"
|
|
1001
|
+
) {
|
|
1002
|
+
errors.push(
|
|
1003
|
+
createError(
|
|
1004
|
+
"INVALID_VALUE",
|
|
1005
|
+
"Track agent identity must be a string",
|
|
1006
|
+
`${agentPath}.identity`,
|
|
1007
|
+
track.agent.identity,
|
|
1008
|
+
),
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Optional: priority (string)
|
|
1013
|
+
if (
|
|
1014
|
+
track.agent.priority !== undefined &&
|
|
1015
|
+
typeof track.agent.priority !== "string"
|
|
1016
|
+
) {
|
|
1017
|
+
errors.push(
|
|
1018
|
+
createError(
|
|
1019
|
+
"INVALID_VALUE",
|
|
1020
|
+
"Track agent priority must be a string",
|
|
1021
|
+
`${agentPath}.priority`,
|
|
1022
|
+
track.agent.priority,
|
|
1023
|
+
),
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Optional: beforeMakingChanges (array of strings)
|
|
1028
|
+
if (track.agent.beforeMakingChanges !== undefined) {
|
|
1029
|
+
if (!Array.isArray(track.agent.beforeMakingChanges)) {
|
|
1030
|
+
errors.push(
|
|
1031
|
+
createError(
|
|
1032
|
+
"INVALID_VALUE",
|
|
1033
|
+
"Track agent beforeMakingChanges must be an array",
|
|
1034
|
+
`${agentPath}.beforeMakingChanges`,
|
|
1035
|
+
track.agent.beforeMakingChanges,
|
|
1036
|
+
),
|
|
1037
|
+
);
|
|
1038
|
+
} else {
|
|
1039
|
+
track.agent.beforeMakingChanges.forEach((item, i) => {
|
|
1040
|
+
if (typeof item !== "string") {
|
|
1041
|
+
errors.push(
|
|
1042
|
+
createError(
|
|
1043
|
+
"INVALID_VALUE",
|
|
1044
|
+
"Track agent beforeMakingChanges items must be strings",
|
|
1045
|
+
`${agentPath}.beforeMakingChanges[${i}]`,
|
|
1046
|
+
item,
|
|
1047
|
+
),
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Optional: constraints (array of strings)
|
|
1055
|
+
if (track.agent.constraints !== undefined) {
|
|
1056
|
+
if (!Array.isArray(track.agent.constraints)) {
|
|
1057
|
+
errors.push(
|
|
1058
|
+
createError(
|
|
1059
|
+
"INVALID_VALUE",
|
|
1060
|
+
"Track agent constraints must be an array",
|
|
1061
|
+
`${agentPath}.constraints`,
|
|
1062
|
+
track.agent.constraints,
|
|
1063
|
+
),
|
|
1064
|
+
);
|
|
1065
|
+
} else {
|
|
1066
|
+
track.agent.constraints.forEach((item, i) => {
|
|
1067
|
+
if (typeof item !== "string") {
|
|
1068
|
+
errors.push(
|
|
1069
|
+
createError(
|
|
1070
|
+
"INVALID_VALUE",
|
|
1071
|
+
"Track agent constraints items must be strings",
|
|
1072
|
+
`${agentPath}.constraints[${i}]`,
|
|
1073
|
+
item,
|
|
1074
|
+
),
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Error if old 'coreInstructions' field is still present
|
|
1082
|
+
if (track.agent.coreInstructions !== undefined) {
|
|
1083
|
+
errors.push(
|
|
1084
|
+
createError(
|
|
1085
|
+
"INVALID_FIELD",
|
|
1086
|
+
"Track agent 'coreInstructions' field is not supported. Use identity, priority, beforeMakingChanges, and constraints instead.",
|
|
1087
|
+
`${agentPath}.coreInstructions`,
|
|
1088
|
+
),
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return { errors, warnings };
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Validate that a grade has required properties and valid values
|
|
1098
|
+
* @param {import('./levels.js').Grade} grade - Grade to validate
|
|
1099
|
+
* @param {number} index - Index in the grades array
|
|
1100
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
1101
|
+
*/
|
|
1102
|
+
function validateGrade(grade, index) {
|
|
1103
|
+
const errors = [];
|
|
1104
|
+
const warnings = [];
|
|
1105
|
+
const path = `grades[${index}]`;
|
|
1106
|
+
|
|
1107
|
+
if (!grade.id) {
|
|
1108
|
+
errors.push(createError("MISSING_REQUIRED", "Grade missing id", path));
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (!grade.professionalTitle) {
|
|
1112
|
+
errors.push(
|
|
1113
|
+
createError(
|
|
1114
|
+
"MISSING_REQUIRED",
|
|
1115
|
+
"Grade missing professionalTitle",
|
|
1116
|
+
`${path}.professionalTitle`,
|
|
1117
|
+
),
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
if (!grade.managementTitle) {
|
|
1121
|
+
errors.push(
|
|
1122
|
+
createError(
|
|
1123
|
+
"MISSING_REQUIRED",
|
|
1124
|
+
"Grade missing managementTitle",
|
|
1125
|
+
`${path}.managementTitle`,
|
|
1126
|
+
),
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (typeof grade.ordinalRank !== "number") {
|
|
1131
|
+
errors.push(
|
|
1132
|
+
createError(
|
|
1133
|
+
"MISSING_REQUIRED",
|
|
1134
|
+
"Grade missing numeric ordinalRank",
|
|
1135
|
+
`${path}.ordinalRank`,
|
|
1136
|
+
),
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Validate base skill levels
|
|
1141
|
+
if (!grade.baseSkillLevels) {
|
|
1142
|
+
errors.push(
|
|
1143
|
+
createError(
|
|
1144
|
+
"MISSING_REQUIRED",
|
|
1145
|
+
"Grade missing baseSkillLevels",
|
|
1146
|
+
`${path}.baseSkillLevels`,
|
|
1147
|
+
),
|
|
1148
|
+
);
|
|
1149
|
+
} else {
|
|
1150
|
+
["primary", "secondary", "broad"].forEach((type) => {
|
|
1151
|
+
const level = grade.baseSkillLevels[type];
|
|
1152
|
+
if (!level) {
|
|
1153
|
+
errors.push(
|
|
1154
|
+
createError(
|
|
1155
|
+
"MISSING_REQUIRED",
|
|
1156
|
+
`Grade missing baseSkillLevels.${type}`,
|
|
1157
|
+
`${path}.baseSkillLevels.${type}`,
|
|
1158
|
+
),
|
|
1159
|
+
);
|
|
1160
|
+
} else if (getSkillLevelIndex(level) === -1) {
|
|
1161
|
+
errors.push(
|
|
1162
|
+
createError(
|
|
1163
|
+
"INVALID_VALUE",
|
|
1164
|
+
`Grade "${grade.id}" has invalid baseSkillLevels.${type}: ${level}`,
|
|
1165
|
+
`${path}.baseSkillLevels.${type}`,
|
|
1166
|
+
level,
|
|
1167
|
+
),
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Validate base behaviour maturity
|
|
1174
|
+
if (!grade.baseBehaviourMaturity) {
|
|
1175
|
+
errors.push(
|
|
1176
|
+
createError(
|
|
1177
|
+
"MISSING_REQUIRED",
|
|
1178
|
+
"Grade missing baseBehaviourMaturity",
|
|
1179
|
+
`${path}.baseBehaviourMaturity`,
|
|
1180
|
+
),
|
|
1181
|
+
);
|
|
1182
|
+
} else if (getBehaviourMaturityIndex(grade.baseBehaviourMaturity) === -1) {
|
|
1183
|
+
errors.push(
|
|
1184
|
+
createError(
|
|
1185
|
+
"INVALID_VALUE",
|
|
1186
|
+
`Grade "${grade.id}" has invalid baseBehaviourMaturity: ${grade.baseBehaviourMaturity}`,
|
|
1187
|
+
`${path}.baseBehaviourMaturity`,
|
|
1188
|
+
grade.baseBehaviourMaturity,
|
|
1189
|
+
),
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Validate expectations
|
|
1194
|
+
if (!grade.expectations) {
|
|
1195
|
+
warnings.push(
|
|
1196
|
+
createWarning(
|
|
1197
|
+
"MISSING_OPTIONAL",
|
|
1198
|
+
"Grade missing expectations",
|
|
1199
|
+
`${path}.expectations`,
|
|
1200
|
+
),
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Validate yearsExperience if present (should be a string like "0-2" or "20+")
|
|
1205
|
+
if (
|
|
1206
|
+
grade.yearsExperience !== undefined &&
|
|
1207
|
+
typeof grade.yearsExperience !== "string"
|
|
1208
|
+
) {
|
|
1209
|
+
warnings.push(
|
|
1210
|
+
createWarning(
|
|
1211
|
+
"INVALID_VALUE",
|
|
1212
|
+
"Grade yearsExperience should be a string",
|
|
1213
|
+
`${path}.yearsExperience`,
|
|
1214
|
+
),
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
return { errors, warnings };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Validate that a capability has required properties
|
|
1223
|
+
* @param {Object} capability - Capability to validate
|
|
1224
|
+
* @param {number} index - Index in the capabilities array
|
|
1225
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
1226
|
+
*/
|
|
1227
|
+
function validateCapability(capability, index) {
|
|
1228
|
+
const errors = [];
|
|
1229
|
+
const warnings = [];
|
|
1230
|
+
const path = `capabilities[${index}]`;
|
|
1231
|
+
|
|
1232
|
+
// id is derived from filename by the loader
|
|
1233
|
+
if (!capability.name) {
|
|
1234
|
+
errors.push(
|
|
1235
|
+
createError(
|
|
1236
|
+
"MISSING_REQUIRED",
|
|
1237
|
+
"Capability missing name",
|
|
1238
|
+
`${path}.name`,
|
|
1239
|
+
),
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
if (!capability.emojiIcon) {
|
|
1243
|
+
warnings.push(
|
|
1244
|
+
createWarning(
|
|
1245
|
+
"MISSING_OPTIONAL",
|
|
1246
|
+
"Capability missing emojiIcon",
|
|
1247
|
+
`${path}.emojiIcon`,
|
|
1248
|
+
),
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Validate professionalResponsibilities and managementResponsibilities
|
|
1253
|
+
const expectedLevels = [
|
|
1254
|
+
"awareness",
|
|
1255
|
+
"foundational",
|
|
1256
|
+
"working",
|
|
1257
|
+
"practitioner",
|
|
1258
|
+
"expert",
|
|
1259
|
+
];
|
|
1260
|
+
|
|
1261
|
+
if (!capability.professionalResponsibilities) {
|
|
1262
|
+
warnings.push(
|
|
1263
|
+
createWarning(
|
|
1264
|
+
"MISSING_OPTIONAL",
|
|
1265
|
+
"Capability missing professionalResponsibilities",
|
|
1266
|
+
`${path}.professionalResponsibilities`,
|
|
1267
|
+
),
|
|
1268
|
+
);
|
|
1269
|
+
} else {
|
|
1270
|
+
for (const level of expectedLevels) {
|
|
1271
|
+
if (!capability.professionalResponsibilities[level]) {
|
|
1272
|
+
warnings.push(
|
|
1273
|
+
createWarning(
|
|
1274
|
+
"MISSING_OPTIONAL",
|
|
1275
|
+
`Capability missing ${level} professional responsibility`,
|
|
1276
|
+
`${path}.professionalResponsibilities.${level}`,
|
|
1277
|
+
),
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (!capability.managementResponsibilities) {
|
|
1284
|
+
warnings.push(
|
|
1285
|
+
createWarning(
|
|
1286
|
+
"MISSING_OPTIONAL",
|
|
1287
|
+
"Capability missing managementResponsibilities",
|
|
1288
|
+
`${path}.managementResponsibilities`,
|
|
1289
|
+
),
|
|
1290
|
+
);
|
|
1291
|
+
} else {
|
|
1292
|
+
for (const level of expectedLevels) {
|
|
1293
|
+
if (!capability.managementResponsibilities[level]) {
|
|
1294
|
+
warnings.push(
|
|
1295
|
+
createWarning(
|
|
1296
|
+
"MISSING_OPTIONAL",
|
|
1297
|
+
`Capability missing ${level} management responsibility`,
|
|
1298
|
+
`${path}.managementResponsibilities.${level}`,
|
|
1299
|
+
),
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return { errors, warnings };
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Validate a stage object
|
|
1310
|
+
* @param {Object} stage - Stage to validate
|
|
1311
|
+
* @param {number} index - Index in the stages array
|
|
1312
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
1313
|
+
*/
|
|
1314
|
+
function validateStage(stage, index) {
|
|
1315
|
+
const errors = [];
|
|
1316
|
+
const warnings = [];
|
|
1317
|
+
const path = `stages[${index}]`;
|
|
1318
|
+
|
|
1319
|
+
if (!stage.id) {
|
|
1320
|
+
errors.push(createError("MISSING_REQUIRED", "Stage missing id", path));
|
|
1321
|
+
} else if (!Object.values(Stage).includes(stage.id)) {
|
|
1322
|
+
errors.push(
|
|
1323
|
+
createError(
|
|
1324
|
+
"INVALID_VALUE",
|
|
1325
|
+
`Invalid stage id: ${stage.id}`,
|
|
1326
|
+
`${path}.id`,
|
|
1327
|
+
stage.id,
|
|
1328
|
+
),
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (!stage.name) {
|
|
1333
|
+
errors.push(
|
|
1334
|
+
createError("MISSING_REQUIRED", "Stage missing name", `${path}.name`),
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (!stage.description) {
|
|
1339
|
+
warnings.push(
|
|
1340
|
+
createWarning(
|
|
1341
|
+
"MISSING_OPTIONAL",
|
|
1342
|
+
"Stage missing description",
|
|
1343
|
+
`${path}.description`,
|
|
1344
|
+
),
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (!stage.handoffs || !Array.isArray(stage.handoffs)) {
|
|
1349
|
+
warnings.push(
|
|
1350
|
+
createWarning(
|
|
1351
|
+
"MISSING_OPTIONAL",
|
|
1352
|
+
"Stage missing handoffs array",
|
|
1353
|
+
`${path}.handoffs`,
|
|
1354
|
+
),
|
|
1355
|
+
);
|
|
1356
|
+
} else {
|
|
1357
|
+
stage.handoffs.forEach((handoff, hIndex) => {
|
|
1358
|
+
if (!handoff.targetStage) {
|
|
1359
|
+
errors.push(
|
|
1360
|
+
createError(
|
|
1361
|
+
"MISSING_REQUIRED",
|
|
1362
|
+
"Handoff missing targetStage",
|
|
1363
|
+
`${path}.handoffs[${hIndex}].targetStage`,
|
|
1364
|
+
),
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
if (!handoff.label) {
|
|
1368
|
+
errors.push(
|
|
1369
|
+
createError(
|
|
1370
|
+
"MISSING_REQUIRED",
|
|
1371
|
+
"Handoff missing label",
|
|
1372
|
+
`${path}.handoffs[${hIndex}].label`,
|
|
1373
|
+
),
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
if (!handoff.prompt) {
|
|
1377
|
+
errors.push(
|
|
1378
|
+
createError(
|
|
1379
|
+
"MISSING_REQUIRED",
|
|
1380
|
+
"Handoff missing prompt",
|
|
1381
|
+
`${path}.handoffs[${hIndex}].prompt`,
|
|
1382
|
+
),
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
return { errors, warnings };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Validate a self-assessment object
|
|
1393
|
+
* @param {import('./levels.js').SelfAssessment} selfAssessment - Self-assessment to validate
|
|
1394
|
+
* @param {import('./levels.js').Skill[]} skills - Array of valid skills
|
|
1395
|
+
* @param {import('./levels.js').Behaviour[]} behaviours - Array of valid behaviours
|
|
1396
|
+
* @returns {import('./levels.js').ValidationResult}
|
|
1397
|
+
*/
|
|
1398
|
+
export function validateSelfAssessment(selfAssessment, skills, behaviours) {
|
|
1399
|
+
const errors = [];
|
|
1400
|
+
const warnings = [];
|
|
1401
|
+
const skillIds = new Set(skills.map((s) => s.id));
|
|
1402
|
+
const behaviourIds = new Set(behaviours.map((b) => b.id));
|
|
1403
|
+
|
|
1404
|
+
if (!selfAssessment) {
|
|
1405
|
+
return createValidationResult(false, [
|
|
1406
|
+
createError("MISSING_REQUIRED", "Self-assessment is required"),
|
|
1407
|
+
]);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Validate skill assessments
|
|
1411
|
+
if (
|
|
1412
|
+
!selfAssessment.skillLevels ||
|
|
1413
|
+
Object.keys(selfAssessment.skillLevels).length === 0
|
|
1414
|
+
) {
|
|
1415
|
+
warnings.push(
|
|
1416
|
+
createWarning(
|
|
1417
|
+
"MISSING_OPTIONAL",
|
|
1418
|
+
"Self-assessment has no skill assessments",
|
|
1419
|
+
),
|
|
1420
|
+
);
|
|
1421
|
+
} else {
|
|
1422
|
+
Object.entries(selfAssessment.skillLevels).forEach(([skillId, level]) => {
|
|
1423
|
+
if (!skillIds.has(skillId)) {
|
|
1424
|
+
errors.push(
|
|
1425
|
+
createError(
|
|
1426
|
+
"INVALID_REFERENCE",
|
|
1427
|
+
`Self-assessment references non-existent skill: ${skillId}`,
|
|
1428
|
+
`selfAssessment.skillLevels.${skillId}`,
|
|
1429
|
+
skillId,
|
|
1430
|
+
),
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
if (getSkillLevelIndex(level) === -1) {
|
|
1434
|
+
errors.push(
|
|
1435
|
+
createError(
|
|
1436
|
+
"INVALID_VALUE",
|
|
1437
|
+
`Self-assessment has invalid skill level for ${skillId}: ${level}`,
|
|
1438
|
+
`selfAssessment.skillLevels.${skillId}`,
|
|
1439
|
+
level,
|
|
1440
|
+
),
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Validate behaviour assessments
|
|
1447
|
+
if (
|
|
1448
|
+
!selfAssessment.behaviourMaturities ||
|
|
1449
|
+
Object.keys(selfAssessment.behaviourMaturities).length === 0
|
|
1450
|
+
) {
|
|
1451
|
+
warnings.push(
|
|
1452
|
+
createWarning(
|
|
1453
|
+
"MISSING_OPTIONAL",
|
|
1454
|
+
"Self-assessment has no behaviour assessments",
|
|
1455
|
+
),
|
|
1456
|
+
);
|
|
1457
|
+
} else {
|
|
1458
|
+
Object.entries(selfAssessment.behaviourMaturities).forEach(
|
|
1459
|
+
([behaviourId, maturity]) => {
|
|
1460
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
1461
|
+
errors.push(
|
|
1462
|
+
createError(
|
|
1463
|
+
"INVALID_REFERENCE",
|
|
1464
|
+
`Self-assessment references non-existent behaviour: ${behaviourId}`,
|
|
1465
|
+
`selfAssessment.behaviourMaturities.${behaviourId}`,
|
|
1466
|
+
behaviourId,
|
|
1467
|
+
),
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
if (getBehaviourMaturityIndex(maturity) === -1) {
|
|
1471
|
+
errors.push(
|
|
1472
|
+
createError(
|
|
1473
|
+
"INVALID_VALUE",
|
|
1474
|
+
`Self-assessment has invalid behaviour maturity for ${behaviourId}: ${maturity}`,
|
|
1475
|
+
`selfAssessment.behaviourMaturities.${behaviourId}`,
|
|
1476
|
+
maturity,
|
|
1477
|
+
),
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return createValidationResult(errors.length === 0, errors, warnings);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Validate all data with referential integrity checks
|
|
1489
|
+
* @param {Object} data - All data to validate
|
|
1490
|
+
* @param {import('./levels.js').Driver[]} data.drivers - Drivers
|
|
1491
|
+
* @param {import('./levels.js').Behaviour[]} data.behaviours - Behaviours
|
|
1492
|
+
* @param {import('./levels.js').Skill[]} data.skills - Skills
|
|
1493
|
+
* @param {import('./levels.js').Discipline[]} data.disciplines - Disciplines
|
|
1494
|
+
* @param {import('./levels.js').Track[]} data.tracks - Tracks
|
|
1495
|
+
* @param {import('./levels.js').Grade[]} data.grades - Grades
|
|
1496
|
+
* @param {Object[]} data.capabilities - Capabilities
|
|
1497
|
+
* @param {Object[]} [data.stages] - Stages
|
|
1498
|
+
* @returns {import('./levels.js').ValidationResult}
|
|
1499
|
+
*/
|
|
1500
|
+
export function validateAllData({
|
|
1501
|
+
drivers,
|
|
1502
|
+
behaviours,
|
|
1503
|
+
skills,
|
|
1504
|
+
disciplines,
|
|
1505
|
+
tracks,
|
|
1506
|
+
grades,
|
|
1507
|
+
capabilities,
|
|
1508
|
+
stages,
|
|
1509
|
+
}) {
|
|
1510
|
+
const allErrors = [];
|
|
1511
|
+
const allWarnings = [];
|
|
1512
|
+
|
|
1513
|
+
// Build ID sets for reference validation
|
|
1514
|
+
const skillIds = new Set((skills || []).map((s) => s.id));
|
|
1515
|
+
const behaviourIds = new Set((behaviours || []).map((b) => b.id));
|
|
1516
|
+
const capabilityIds = new Set((capabilities || []).map((c) => c.id));
|
|
1517
|
+
|
|
1518
|
+
// Extract stage IDs for agent skill validation
|
|
1519
|
+
const requiredStageIds = (stages || []).map((s) => s.id);
|
|
1520
|
+
|
|
1521
|
+
// Validate skills
|
|
1522
|
+
if (!skills || skills.length === 0) {
|
|
1523
|
+
allErrors.push(
|
|
1524
|
+
createError("MISSING_REQUIRED", "At least one skill is required"),
|
|
1525
|
+
);
|
|
1526
|
+
} else {
|
|
1527
|
+
skills.forEach((skill, index) => {
|
|
1528
|
+
const { errors, warnings } = validateSkill(
|
|
1529
|
+
skill,
|
|
1530
|
+
index,
|
|
1531
|
+
requiredStageIds,
|
|
1532
|
+
);
|
|
1533
|
+
allErrors.push(...errors);
|
|
1534
|
+
allWarnings.push(...warnings);
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
// Check for duplicate IDs
|
|
1538
|
+
const seenIds = new Set();
|
|
1539
|
+
skills.forEach((skill, index) => {
|
|
1540
|
+
if (skill.id) {
|
|
1541
|
+
if (seenIds.has(skill.id)) {
|
|
1542
|
+
allErrors.push(
|
|
1543
|
+
createError(
|
|
1544
|
+
"DUPLICATE_ID",
|
|
1545
|
+
`Duplicate skill ID: ${skill.id}`,
|
|
1546
|
+
`skills[${index}]`,
|
|
1547
|
+
skill.id,
|
|
1548
|
+
),
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
seenIds.add(skill.id);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// Validate behaviours
|
|
1557
|
+
if (!behaviours || behaviours.length === 0) {
|
|
1558
|
+
allErrors.push(
|
|
1559
|
+
createError("MISSING_REQUIRED", "At least one behaviour is required"),
|
|
1560
|
+
);
|
|
1561
|
+
} else {
|
|
1562
|
+
behaviours.forEach((behaviour, index) => {
|
|
1563
|
+
const { errors, warnings } = validateBehaviour(behaviour, index);
|
|
1564
|
+
allErrors.push(...errors);
|
|
1565
|
+
allWarnings.push(...warnings);
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
// Check for duplicate IDs
|
|
1569
|
+
const seenIds = new Set();
|
|
1570
|
+
behaviours.forEach((behaviour, index) => {
|
|
1571
|
+
if (behaviour.id) {
|
|
1572
|
+
if (seenIds.has(behaviour.id)) {
|
|
1573
|
+
allErrors.push(
|
|
1574
|
+
createError(
|
|
1575
|
+
"DUPLICATE_ID",
|
|
1576
|
+
`Duplicate behaviour ID: ${behaviour.id}`,
|
|
1577
|
+
`behaviours[${index}]`,
|
|
1578
|
+
behaviour.id,
|
|
1579
|
+
),
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
seenIds.add(behaviour.id);
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Get track IDs for discipline validation
|
|
1588
|
+
const trackIdSet = new Set((tracks || []).map((t) => t.id));
|
|
1589
|
+
|
|
1590
|
+
// Get grade IDs for discipline and track validation
|
|
1591
|
+
const gradeIdSet = new Set((grades || []).map((g) => g.id));
|
|
1592
|
+
|
|
1593
|
+
// Validate disciplines
|
|
1594
|
+
if (!disciplines || disciplines.length === 0) {
|
|
1595
|
+
allErrors.push(
|
|
1596
|
+
createError("MISSING_REQUIRED", "At least one discipline is required"),
|
|
1597
|
+
);
|
|
1598
|
+
} else {
|
|
1599
|
+
disciplines.forEach((discipline, index) => {
|
|
1600
|
+
const { errors, warnings } = validateDiscipline(
|
|
1601
|
+
discipline,
|
|
1602
|
+
index,
|
|
1603
|
+
skillIds,
|
|
1604
|
+
behaviourIds,
|
|
1605
|
+
trackIdSet,
|
|
1606
|
+
gradeIdSet,
|
|
1607
|
+
);
|
|
1608
|
+
allErrors.push(...errors);
|
|
1609
|
+
allWarnings.push(...warnings);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// Check for duplicate IDs
|
|
1613
|
+
const seenIds = new Set();
|
|
1614
|
+
disciplines.forEach((discipline, index) => {
|
|
1615
|
+
if (discipline.id) {
|
|
1616
|
+
if (seenIds.has(discipline.id)) {
|
|
1617
|
+
allErrors.push(
|
|
1618
|
+
createError(
|
|
1619
|
+
"DUPLICATE_ID",
|
|
1620
|
+
`Duplicate discipline ID: ${discipline.id}`,
|
|
1621
|
+
`disciplines[${index}]`,
|
|
1622
|
+
discipline.id,
|
|
1623
|
+
),
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
seenIds.add(discipline.id);
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Get all skill IDs from disciplines for track validation
|
|
1632
|
+
const disciplineSkillIds = getAllDisciplineSkillIds(disciplines || []);
|
|
1633
|
+
|
|
1634
|
+
// Validate tracks
|
|
1635
|
+
if (!tracks || tracks.length === 0) {
|
|
1636
|
+
allErrors.push(
|
|
1637
|
+
createError("MISSING_REQUIRED", "At least one track is required"),
|
|
1638
|
+
);
|
|
1639
|
+
} else {
|
|
1640
|
+
tracks.forEach((track, index) => {
|
|
1641
|
+
const { errors, warnings } = validateTrack(
|
|
1642
|
+
track,
|
|
1643
|
+
index,
|
|
1644
|
+
disciplineSkillIds,
|
|
1645
|
+
behaviourIds,
|
|
1646
|
+
gradeIdSet,
|
|
1647
|
+
);
|
|
1648
|
+
allErrors.push(...errors);
|
|
1649
|
+
allWarnings.push(...warnings);
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
// Check for duplicate IDs
|
|
1653
|
+
const seenIds = new Set();
|
|
1654
|
+
tracks.forEach((track, index) => {
|
|
1655
|
+
if (track.id) {
|
|
1656
|
+
if (seenIds.has(track.id)) {
|
|
1657
|
+
allErrors.push(
|
|
1658
|
+
createError(
|
|
1659
|
+
"DUPLICATE_ID",
|
|
1660
|
+
`Duplicate track ID: ${track.id}`,
|
|
1661
|
+
`tracks[${index}]`,
|
|
1662
|
+
track.id,
|
|
1663
|
+
),
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
seenIds.add(track.id);
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Validate grades
|
|
1672
|
+
if (!grades || grades.length === 0) {
|
|
1673
|
+
allErrors.push(
|
|
1674
|
+
createError("MISSING_REQUIRED", "At least one grade is required"),
|
|
1675
|
+
);
|
|
1676
|
+
} else {
|
|
1677
|
+
grades.forEach((grade, index) => {
|
|
1678
|
+
const { errors, warnings } = validateGrade(grade, index);
|
|
1679
|
+
allErrors.push(...errors);
|
|
1680
|
+
allWarnings.push(...warnings);
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
// Check for duplicate IDs
|
|
1684
|
+
const seenIds = new Set();
|
|
1685
|
+
grades.forEach((grade, index) => {
|
|
1686
|
+
if (grade.id) {
|
|
1687
|
+
if (seenIds.has(grade.id)) {
|
|
1688
|
+
allErrors.push(
|
|
1689
|
+
createError(
|
|
1690
|
+
"DUPLICATE_ID",
|
|
1691
|
+
`Duplicate grade ID: ${grade.id}`,
|
|
1692
|
+
`grades[${index}]`,
|
|
1693
|
+
grade.id,
|
|
1694
|
+
),
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
seenIds.add(grade.id);
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// Validate capabilities (required)
|
|
1703
|
+
if (!capabilities || capabilities.length === 0) {
|
|
1704
|
+
allErrors.push(
|
|
1705
|
+
createError("MISSING_REQUIRED", "At least one capability is required"),
|
|
1706
|
+
);
|
|
1707
|
+
} else {
|
|
1708
|
+
capabilities.forEach((capability, index) => {
|
|
1709
|
+
const { errors, warnings } = validateCapability(capability, index);
|
|
1710
|
+
allErrors.push(...errors);
|
|
1711
|
+
allWarnings.push(...warnings);
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
// Check for duplicate IDs
|
|
1715
|
+
const seenIds = new Set();
|
|
1716
|
+
capabilities.forEach((capability, index) => {
|
|
1717
|
+
if (capability.id) {
|
|
1718
|
+
if (seenIds.has(capability.id)) {
|
|
1719
|
+
allErrors.push(
|
|
1720
|
+
createError(
|
|
1721
|
+
"DUPLICATE_ID",
|
|
1722
|
+
`Duplicate capability ID: ${capability.id}`,
|
|
1723
|
+
`capabilities[${index}]`,
|
|
1724
|
+
capability.id,
|
|
1725
|
+
),
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
seenIds.add(capability.id);
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
// Validate skill capability references against loaded capabilities
|
|
1733
|
+
if (skills && skills.length > 0) {
|
|
1734
|
+
skills.forEach((skill, index) => {
|
|
1735
|
+
if (skill.capability && !capabilityIds.has(skill.capability)) {
|
|
1736
|
+
allErrors.push(
|
|
1737
|
+
createError(
|
|
1738
|
+
"INVALID_REFERENCE",
|
|
1739
|
+
`Skill '${skill.id}' references unknown capability '${skill.capability}'`,
|
|
1740
|
+
`skills[${index}].capability`,
|
|
1741
|
+
skill.capability,
|
|
1742
|
+
),
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// Validate stages (optional but validate if present)
|
|
1750
|
+
if (stages && stages.length > 0) {
|
|
1751
|
+
stages.forEach((stage, index) => {
|
|
1752
|
+
const { errors, warnings } = validateStage(stage, index);
|
|
1753
|
+
allErrors.push(...errors);
|
|
1754
|
+
allWarnings.push(...warnings);
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
// Check for duplicate IDs
|
|
1758
|
+
const seenIds = new Set();
|
|
1759
|
+
stages.forEach((stage, index) => {
|
|
1760
|
+
if (stage.id) {
|
|
1761
|
+
if (seenIds.has(stage.id)) {
|
|
1762
|
+
allErrors.push(
|
|
1763
|
+
createError(
|
|
1764
|
+
"DUPLICATE_ID",
|
|
1765
|
+
`Duplicate stage ID: ${stage.id}`,
|
|
1766
|
+
`stages[${index}]`,
|
|
1767
|
+
stage.id,
|
|
1768
|
+
),
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
seenIds.add(stage.id);
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
// Validate handoff targets reference valid stages
|
|
1776
|
+
const stageIds = new Set(stages.map((s) => s.id));
|
|
1777
|
+
stages.forEach((stage, sIndex) => {
|
|
1778
|
+
if (stage.handoffs) {
|
|
1779
|
+
stage.handoffs.forEach((handoff, hIndex) => {
|
|
1780
|
+
if (handoff.target && !stageIds.has(handoff.target)) {
|
|
1781
|
+
allErrors.push(
|
|
1782
|
+
createError(
|
|
1783
|
+
"INVALID_REFERENCE",
|
|
1784
|
+
`Stage '${stage.id}' handoff references unknown stage '${handoff.target}'`,
|
|
1785
|
+
`stages[${sIndex}].handoffs[${hIndex}].target`,
|
|
1786
|
+
handoff.target,
|
|
1787
|
+
),
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// Validate drivers (required)
|
|
1796
|
+
if (!drivers || drivers.length === 0) {
|
|
1797
|
+
allErrors.push(
|
|
1798
|
+
createError("MISSING_REQUIRED", "At least one driver is required"),
|
|
1799
|
+
);
|
|
1800
|
+
} else {
|
|
1801
|
+
drivers.forEach((driver, index) => {
|
|
1802
|
+
const { errors, warnings } = validateDriver(
|
|
1803
|
+
driver,
|
|
1804
|
+
index,
|
|
1805
|
+
skillIds,
|
|
1806
|
+
behaviourIds,
|
|
1807
|
+
);
|
|
1808
|
+
allErrors.push(...errors);
|
|
1809
|
+
allWarnings.push(...warnings);
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// Check for duplicate IDs
|
|
1813
|
+
const seenIds = new Set();
|
|
1814
|
+
drivers.forEach((driver, index) => {
|
|
1815
|
+
if (driver.id) {
|
|
1816
|
+
if (seenIds.has(driver.id)) {
|
|
1817
|
+
allErrors.push(
|
|
1818
|
+
createError(
|
|
1819
|
+
"DUPLICATE_ID",
|
|
1820
|
+
`Duplicate driver ID: ${driver.id}`,
|
|
1821
|
+
`drivers[${index}]`,
|
|
1822
|
+
driver.id,
|
|
1823
|
+
),
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
seenIds.add(driver.id);
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
return createValidationResult(allErrors.length === 0, allErrors, allWarnings);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Validate question bank structure
|
|
1836
|
+
* @param {import('./levels.js').QuestionBank} questionBank - Question bank to validate
|
|
1837
|
+
* @param {import('./levels.js').Skill[]} skills - Valid skills
|
|
1838
|
+
* @param {import('./levels.js').Behaviour[]} behaviours - Valid behaviours
|
|
1839
|
+
* @returns {import('./levels.js').ValidationResult}
|
|
1840
|
+
*/
|
|
1841
|
+
export function validateQuestionBank(questionBank, skills, behaviours) {
|
|
1842
|
+
const errors = [];
|
|
1843
|
+
const warnings = [];
|
|
1844
|
+
const skillIds = new Set(skills.map((s) => s.id));
|
|
1845
|
+
const behaviourIds = new Set(behaviours.map((b) => b.id));
|
|
1846
|
+
|
|
1847
|
+
if (!questionBank) {
|
|
1848
|
+
return createValidationResult(false, [
|
|
1849
|
+
createError("MISSING_REQUIRED", "Question bank is required"),
|
|
1850
|
+
]);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Validate skill questions
|
|
1854
|
+
if (questionBank.skillLevels) {
|
|
1855
|
+
Object.entries(questionBank.skillLevels).forEach(
|
|
1856
|
+
([skillId, levelQuestions]) => {
|
|
1857
|
+
if (!skillIds.has(skillId)) {
|
|
1858
|
+
errors.push(
|
|
1859
|
+
createError(
|
|
1860
|
+
"INVALID_REFERENCE",
|
|
1861
|
+
`Question bank references non-existent skill: ${skillId}`,
|
|
1862
|
+
`questionBank.skillLevels.${skillId}`,
|
|
1863
|
+
skillId,
|
|
1864
|
+
),
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
Object.entries(levelQuestions || {}).forEach(([level, questions]) => {
|
|
1868
|
+
if (getSkillLevelIndex(level) === -1) {
|
|
1869
|
+
errors.push(
|
|
1870
|
+
createError(
|
|
1871
|
+
"INVALID_VALUE",
|
|
1872
|
+
`Question bank has invalid skill level: ${level}`,
|
|
1873
|
+
`questionBank.skillLevels.${skillId}.${level}`,
|
|
1874
|
+
level,
|
|
1875
|
+
),
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
1879
|
+
warnings.push(
|
|
1880
|
+
createWarning(
|
|
1881
|
+
"EMPTY_QUESTIONS",
|
|
1882
|
+
`No questions for skill ${skillId} at level ${level}`,
|
|
1883
|
+
`questionBank.skillLevels.${skillId}.${level}`,
|
|
1884
|
+
),
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
},
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// Validate behaviour questions
|
|
1893
|
+
if (questionBank.behaviourMaturities) {
|
|
1894
|
+
Object.entries(questionBank.behaviourMaturities).forEach(
|
|
1895
|
+
([behaviourId, maturityQuestions]) => {
|
|
1896
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
1897
|
+
errors.push(
|
|
1898
|
+
createError(
|
|
1899
|
+
"INVALID_REFERENCE",
|
|
1900
|
+
`Question bank references non-existent behaviour: ${behaviourId}`,
|
|
1901
|
+
`questionBank.behaviourMaturities.${behaviourId}`,
|
|
1902
|
+
behaviourId,
|
|
1903
|
+
),
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
Object.entries(maturityQuestions || {}).forEach(
|
|
1907
|
+
([maturity, questions]) => {
|
|
1908
|
+
if (getBehaviourMaturityIndex(maturity) === -1) {
|
|
1909
|
+
errors.push(
|
|
1910
|
+
createError(
|
|
1911
|
+
"INVALID_VALUE",
|
|
1912
|
+
`Question bank has invalid behaviour maturity: ${maturity}`,
|
|
1913
|
+
`questionBank.behaviourMaturities.${behaviourId}.${maturity}`,
|
|
1914
|
+
maturity,
|
|
1915
|
+
),
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
1919
|
+
warnings.push(
|
|
1920
|
+
createWarning(
|
|
1921
|
+
"EMPTY_QUESTIONS",
|
|
1922
|
+
`No questions for behaviour ${behaviourId} at maturity ${maturity}`,
|
|
1923
|
+
`questionBank.behaviourMaturities.${behaviourId}.${maturity}`,
|
|
1924
|
+
),
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
},
|
|
1928
|
+
);
|
|
1929
|
+
},
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
return createValidationResult(errors.length === 0, errors, warnings);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* Validate agent-specific data comprehensively
|
|
1938
|
+
* This validates cross-references between human and agent definitions
|
|
1939
|
+
* @param {Object} params - Validation parameters
|
|
1940
|
+
* @param {Object} params.humanData - Human data (disciplines, tracks, skills, behaviours, stages)
|
|
1941
|
+
* @param {Object} params.agentData - Agent-specific data (disciplines, tracks, behaviours with agent sections)
|
|
1942
|
+
* @returns {import('./levels.js').ValidationResult}
|
|
1943
|
+
*/
|
|
1944
|
+
export function validateAgentData({ humanData, agentData }) {
|
|
1945
|
+
const errors = [];
|
|
1946
|
+
const warnings = [];
|
|
1947
|
+
|
|
1948
|
+
const humanDisciplineIds = new Set(
|
|
1949
|
+
(humanData.disciplines || []).map((d) => d.id),
|
|
1950
|
+
);
|
|
1951
|
+
const humanTrackIds = new Set((humanData.tracks || []).map((t) => t.id));
|
|
1952
|
+
const humanBehaviourIds = new Set(
|
|
1953
|
+
(humanData.behaviours || []).map((b) => b.id),
|
|
1954
|
+
);
|
|
1955
|
+
const stageIds = new Set((humanData.stages || []).map((s) => s.id));
|
|
1956
|
+
|
|
1957
|
+
// Validate agent disciplines reference human disciplines
|
|
1958
|
+
for (const agentDiscipline of agentData.disciplines || []) {
|
|
1959
|
+
if (!humanDisciplineIds.has(agentDiscipline.id)) {
|
|
1960
|
+
errors.push(
|
|
1961
|
+
createError(
|
|
1962
|
+
"ORPHANED_AGENT",
|
|
1963
|
+
`Agent discipline '${agentDiscipline.id}' has no human definition`,
|
|
1964
|
+
`agentData.disciplines`,
|
|
1965
|
+
agentDiscipline.id,
|
|
1966
|
+
),
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Validate required identity exists (spread from agent section by loader)
|
|
1971
|
+
if (!agentDiscipline.identity) {
|
|
1972
|
+
errors.push(
|
|
1973
|
+
createError(
|
|
1974
|
+
"MISSING_REQUIRED",
|
|
1975
|
+
`Agent discipline '${agentDiscipline.id}' missing identity`,
|
|
1976
|
+
`agentData.disciplines.${agentDiscipline.id}.identity`,
|
|
1977
|
+
),
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// Validate agent tracks reference human tracks
|
|
1983
|
+
for (const agentTrack of agentData.tracks || []) {
|
|
1984
|
+
if (!humanTrackIds.has(agentTrack.id)) {
|
|
1985
|
+
errors.push(
|
|
1986
|
+
createError(
|
|
1987
|
+
"ORPHANED_AGENT",
|
|
1988
|
+
`Agent track '${agentTrack.id}' has no human definition`,
|
|
1989
|
+
`agentData.tracks`,
|
|
1990
|
+
agentTrack.id,
|
|
1991
|
+
),
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// Validate agent behaviours reference human behaviours
|
|
1997
|
+
for (const agentBehaviour of agentData.behaviours || []) {
|
|
1998
|
+
if (!humanBehaviourIds.has(agentBehaviour.id)) {
|
|
1999
|
+
errors.push(
|
|
2000
|
+
createError(
|
|
2001
|
+
"ORPHANED_AGENT",
|
|
2002
|
+
`Agent behaviour '${agentBehaviour.id}' has no human definition`,
|
|
2003
|
+
`agentData.behaviours`,
|
|
2004
|
+
agentBehaviour.id,
|
|
2005
|
+
),
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// Validate required agent fields (spread from agent section by loader)
|
|
2010
|
+
if (!agentBehaviour.title) {
|
|
2011
|
+
errors.push(
|
|
2012
|
+
createError(
|
|
2013
|
+
"MISSING_REQUIRED",
|
|
2014
|
+
`Agent behaviour '${agentBehaviour.id}' missing title`,
|
|
2015
|
+
`agentData.behaviours.${agentBehaviour.id}.title`,
|
|
2016
|
+
),
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
if (!agentBehaviour.workingStyle) {
|
|
2020
|
+
errors.push(
|
|
2021
|
+
createError(
|
|
2022
|
+
"MISSING_REQUIRED",
|
|
2023
|
+
`Agent behaviour '${agentBehaviour.id}' missing workingStyle`,
|
|
2024
|
+
`agentData.behaviours.${agentBehaviour.id}.workingStyle`,
|
|
2025
|
+
),
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// Validate skills with agent sections have complete stage coverage
|
|
2031
|
+
const skillsWithAgent = (humanData.skills || []).filter((s) => s.agent);
|
|
2032
|
+
const requiredStages = ["plan", "code", "review"];
|
|
2033
|
+
|
|
2034
|
+
for (const skill of skillsWithAgent) {
|
|
2035
|
+
const stages = skill.agent.stages || {};
|
|
2036
|
+
const missingStages = requiredStages.filter((stage) => !stages[stage]);
|
|
2037
|
+
|
|
2038
|
+
if (missingStages.length > 0) {
|
|
2039
|
+
warnings.push(
|
|
2040
|
+
createWarning(
|
|
2041
|
+
"INCOMPLETE_STAGES",
|
|
2042
|
+
`Skill '${skill.id}' agent section missing stages: ${missingStages.join(", ")}`,
|
|
2043
|
+
`skills.${skill.id}.agent.stages`,
|
|
2044
|
+
),
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// Validate each stage has required fields
|
|
2049
|
+
for (const [stageId, stageData] of Object.entries(stages)) {
|
|
2050
|
+
if (!stageData.focus) {
|
|
2051
|
+
errors.push(
|
|
2052
|
+
createError(
|
|
2053
|
+
"MISSING_REQUIRED",
|
|
2054
|
+
`Skill '${skill.id}' agent stage '${stageId}' missing focus`,
|
|
2055
|
+
`skills.${skill.id}.agent.stages.${stageId}.focus`,
|
|
2056
|
+
),
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
if (
|
|
2060
|
+
!stageData.activities ||
|
|
2061
|
+
!Array.isArray(stageData.activities) ||
|
|
2062
|
+
stageData.activities.length === 0
|
|
2063
|
+
) {
|
|
2064
|
+
errors.push(
|
|
2065
|
+
createError(
|
|
2066
|
+
"MISSING_REQUIRED",
|
|
2067
|
+
`Skill '${skill.id}' agent stage '${stageId}' missing or empty activities`,
|
|
2068
|
+
`skills.${skill.id}.agent.stages.${stageId}.activities`,
|
|
2069
|
+
),
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
if (
|
|
2073
|
+
!stageData.ready ||
|
|
2074
|
+
!Array.isArray(stageData.ready) ||
|
|
2075
|
+
stageData.ready.length === 0
|
|
2076
|
+
) {
|
|
2077
|
+
errors.push(
|
|
2078
|
+
createError(
|
|
2079
|
+
"MISSING_REQUIRED",
|
|
2080
|
+
`Skill '${skill.id}' agent stage '${stageId}' missing or empty ready criteria`,
|
|
2081
|
+
`skills.${skill.id}.agent.stages.${stageId}.ready`,
|
|
2082
|
+
),
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Validate stage handoff targets exist
|
|
2089
|
+
for (const stage of humanData.stages || []) {
|
|
2090
|
+
if (stage.handoffs) {
|
|
2091
|
+
for (const handoff of stage.handoffs) {
|
|
2092
|
+
const targetId = handoff.targetStage || handoff.target;
|
|
2093
|
+
if (targetId && !stageIds.has(targetId)) {
|
|
2094
|
+
errors.push(
|
|
2095
|
+
createError(
|
|
2096
|
+
"INVALID_REFERENCE",
|
|
2097
|
+
`Stage '${stage.id}' handoff references unknown stage '${targetId}'`,
|
|
2098
|
+
`stages.${stage.id}.handoffs`,
|
|
2099
|
+
targetId,
|
|
2100
|
+
),
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// Summary statistics as warnings (informational)
|
|
2108
|
+
const stats = {
|
|
2109
|
+
agentDisciplines: (agentData.disciplines || []).length,
|
|
2110
|
+
agentTracks: (agentData.tracks || []).length,
|
|
2111
|
+
agentBehaviours: (agentData.behaviours || []).length,
|
|
2112
|
+
skillsWithAgent: skillsWithAgent.length,
|
|
2113
|
+
skillsWithCompleteStages: skillsWithAgent.filter((s) => {
|
|
2114
|
+
const stages = s.agent.stages || {};
|
|
2115
|
+
return requiredStages.every((stage) => stages[stage]);
|
|
2116
|
+
}).length,
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
if (stats.skillsWithCompleteStages < stats.skillsWithAgent) {
|
|
2120
|
+
warnings.push(
|
|
2121
|
+
createWarning(
|
|
2122
|
+
"INCOMPLETE_COVERAGE",
|
|
2123
|
+
`${stats.skillsWithCompleteStages}/${stats.skillsWithAgent} skills have complete stage coverage (plan, code, review)`,
|
|
2124
|
+
"agentData",
|
|
2125
|
+
),
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
return createValidationResult(errors.length === 0, errors, warnings);
|
|
2130
|
+
}
|