@forwardimpact/schema 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/bin/fit-schema.js +260 -0
  2. package/examples/behaviours/_index.yaml +8 -0
  3. package/examples/behaviours/outcome_ownership.yaml +43 -0
  4. package/examples/behaviours/polymathic_knowledge.yaml +41 -0
  5. package/examples/behaviours/precise_communication.yaml +39 -0
  6. package/examples/behaviours/relentless_curiosity.yaml +37 -0
  7. package/examples/behaviours/systems_thinking.yaml +40 -0
  8. package/examples/capabilities/_index.yaml +8 -0
  9. package/examples/capabilities/business.yaml +189 -0
  10. package/examples/capabilities/delivery.yaml +305 -0
  11. package/examples/capabilities/people.yaml +68 -0
  12. package/examples/capabilities/reliability.yaml +414 -0
  13. package/examples/capabilities/scale.yaml +378 -0
  14. package/examples/copilot-setup-steps.yaml +25 -0
  15. package/examples/devcontainer.yaml +21 -0
  16. package/examples/disciplines/_index.yaml +6 -0
  17. package/examples/disciplines/data_engineering.yaml +78 -0
  18. package/examples/disciplines/engineering_management.yaml +63 -0
  19. package/examples/disciplines/software_engineering.yaml +78 -0
  20. package/examples/drivers.yaml +202 -0
  21. package/examples/framework.yaml +69 -0
  22. package/examples/grades.yaml +115 -0
  23. package/examples/questions/behaviours/outcome_ownership.yaml +51 -0
  24. package/examples/questions/behaviours/polymathic_knowledge.yaml +47 -0
  25. package/examples/questions/behaviours/precise_communication.yaml +54 -0
  26. package/examples/questions/behaviours/relentless_curiosity.yaml +50 -0
  27. package/examples/questions/behaviours/systems_thinking.yaml +52 -0
  28. package/examples/questions/skills/architecture_design.yaml +53 -0
  29. package/examples/questions/skills/cloud_platforms.yaml +47 -0
  30. package/examples/questions/skills/code_quality.yaml +48 -0
  31. package/examples/questions/skills/data_modeling.yaml +45 -0
  32. package/examples/questions/skills/devops.yaml +46 -0
  33. package/examples/questions/skills/full_stack_development.yaml +47 -0
  34. package/examples/questions/skills/sre_practices.yaml +43 -0
  35. package/examples/questions/skills/stakeholder_management.yaml +48 -0
  36. package/examples/questions/skills/team_collaboration.yaml +42 -0
  37. package/examples/questions/skills/technical_writing.yaml +42 -0
  38. package/examples/self-assessments.yaml +64 -0
  39. package/examples/stages.yaml +139 -0
  40. package/examples/tracks/_index.yaml +5 -0
  41. package/examples/tracks/platform.yaml +49 -0
  42. package/examples/tracks/sre.yaml +48 -0
  43. package/examples/vscode-settings.yaml +21 -0
  44. package/lib/index-generator.js +65 -0
  45. package/lib/index.js +44 -0
  46. package/lib/levels.js +601 -0
  47. package/lib/loader.js +599 -0
  48. package/lib/modifiers.js +23 -0
  49. package/lib/schema-validation.js +438 -0
  50. package/lib/validation.js +2130 -0
  51. package/package.json +49 -0
  52. package/schema/json/behaviour-questions.schema.json +68 -0
  53. package/schema/json/behaviour.schema.json +73 -0
  54. package/schema/json/capability.schema.json +220 -0
  55. package/schema/json/defs.schema.json +132 -0
  56. package/schema/json/discipline.schema.json +132 -0
  57. package/schema/json/drivers.schema.json +48 -0
  58. package/schema/json/framework.schema.json +55 -0
  59. package/schema/json/grades.schema.json +121 -0
  60. package/schema/json/index.schema.json +18 -0
  61. package/schema/json/self-assessments.schema.json +52 -0
  62. package/schema/json/skill-questions.schema.json +68 -0
  63. package/schema/json/stages.schema.json +84 -0
  64. package/schema/json/track.schema.json +100 -0
  65. package/schema/rdf/pathway.ttl +2362 -0
@@ -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
+ }