@forwardimpact/map 0.11.0

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