@forwardimpact/pathway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/app/commands/agent.js +430 -0
- package/app/commands/behaviour.js +61 -0
- package/app/commands/command-factory.js +211 -0
- package/app/commands/discipline.js +58 -0
- package/app/commands/driver.js +94 -0
- package/app/commands/grade.js +60 -0
- package/app/commands/index.js +20 -0
- package/app/commands/init.js +67 -0
- package/app/commands/interview.js +68 -0
- package/app/commands/job.js +157 -0
- package/app/commands/progress.js +77 -0
- package/app/commands/questions.js +179 -0
- package/app/commands/serve.js +143 -0
- package/app/commands/site.js +121 -0
- package/app/commands/skill.js +76 -0
- package/app/commands/stage.js +129 -0
- package/app/commands/track.js +70 -0
- package/app/components/action-buttons.js +66 -0
- package/app/components/behaviour-profile.js +53 -0
- package/app/components/builder.js +341 -0
- package/app/components/card.js +98 -0
- package/app/components/checklist.js +145 -0
- package/app/components/comparison-radar.js +237 -0
- package/app/components/detail.js +230 -0
- package/app/components/error-page.js +72 -0
- package/app/components/grid.js +109 -0
- package/app/components/list.js +120 -0
- package/app/components/modifier-table.js +142 -0
- package/app/components/nav.js +64 -0
- package/app/components/progression-table.js +320 -0
- package/app/components/radar-chart.js +102 -0
- package/app/components/skill-matrix.js +97 -0
- package/app/css/base.css +56 -0
- package/app/css/bundles/app.css +40 -0
- package/app/css/bundles/handout.css +43 -0
- package/app/css/bundles/slides.css +40 -0
- package/app/css/components/badges.css +215 -0
- package/app/css/components/buttons.css +101 -0
- package/app/css/components/forms.css +105 -0
- package/app/css/components/layout.css +209 -0
- package/app/css/components/nav.css +166 -0
- package/app/css/components/progress.css +166 -0
- package/app/css/components/states.css +82 -0
- package/app/css/components/surfaces.css +243 -0
- package/app/css/components/tables.css +362 -0
- package/app/css/components/typography.css +122 -0
- package/app/css/components/utilities.css +41 -0
- package/app/css/pages/agent-builder.css +391 -0
- package/app/css/pages/assessment-results.css +453 -0
- package/app/css/pages/detail.css +59 -0
- package/app/css/pages/interview-builder.css +148 -0
- package/app/css/pages/job-builder.css +134 -0
- package/app/css/pages/landing.css +92 -0
- package/app/css/pages/lifecycle.css +118 -0
- package/app/css/pages/progress-builder.css +274 -0
- package/app/css/pages/self-assessment.css +502 -0
- package/app/css/reset.css +50 -0
- package/app/css/tokens.css +153 -0
- package/app/css/views/handout.css +30 -0
- package/app/css/views/print.css +608 -0
- package/app/css/views/slide-animations.css +113 -0
- package/app/css/views/slide-base.css +330 -0
- package/app/css/views/slide-sections.css +597 -0
- package/app/css/views/slide-tables.css +275 -0
- package/app/formatters/agent/dom.js +540 -0
- package/app/formatters/agent/profile.js +133 -0
- package/app/formatters/agent/skill.js +58 -0
- package/app/formatters/behaviour/dom.js +91 -0
- package/app/formatters/behaviour/markdown.js +54 -0
- package/app/formatters/behaviour/shared.js +64 -0
- package/app/formatters/discipline/dom.js +187 -0
- package/app/formatters/discipline/markdown.js +87 -0
- package/app/formatters/discipline/shared.js +131 -0
- package/app/formatters/driver/dom.js +103 -0
- package/app/formatters/driver/shared.js +92 -0
- package/app/formatters/grade/dom.js +208 -0
- package/app/formatters/grade/markdown.js +94 -0
- package/app/formatters/grade/shared.js +86 -0
- package/app/formatters/index.js +50 -0
- package/app/formatters/interview/dom.js +97 -0
- package/app/formatters/interview/markdown.js +66 -0
- package/app/formatters/interview/shared.js +332 -0
- package/app/formatters/job/description.js +176 -0
- package/app/formatters/job/dom.js +411 -0
- package/app/formatters/job/markdown.js +102 -0
- package/app/formatters/progress/dom.js +135 -0
- package/app/formatters/progress/markdown.js +86 -0
- package/app/formatters/progress/shared.js +339 -0
- package/app/formatters/questions/json.js +43 -0
- package/app/formatters/questions/markdown.js +303 -0
- package/app/formatters/questions/shared.js +274 -0
- package/app/formatters/questions/yaml.js +76 -0
- package/app/formatters/shared.js +71 -0
- package/app/formatters/skill/dom.js +168 -0
- package/app/formatters/skill/markdown.js +109 -0
- package/app/formatters/skill/shared.js +125 -0
- package/app/formatters/stage/dom.js +135 -0
- package/app/formatters/stage/index.js +12 -0
- package/app/formatters/stage/shared.js +111 -0
- package/app/formatters/track/dom.js +128 -0
- package/app/formatters/track/markdown.js +105 -0
- package/app/formatters/track/shared.js +181 -0
- package/app/handout-main.js +421 -0
- package/app/handout.html +21 -0
- package/app/index.html +59 -0
- package/app/lib/card-mappers.js +173 -0
- package/app/lib/cli-output.js +270 -0
- package/app/lib/error-boundary.js +70 -0
- package/app/lib/errors.js +49 -0
- package/app/lib/form-controls.js +47 -0
- package/app/lib/job-cache.js +86 -0
- package/app/lib/markdown.js +114 -0
- package/app/lib/radar.js +866 -0
- package/app/lib/reactive.js +77 -0
- package/app/lib/render.js +212 -0
- package/app/lib/router-core.js +160 -0
- package/app/lib/router-pages.js +16 -0
- package/app/lib/router-slides.js +202 -0
- package/app/lib/state.js +148 -0
- package/app/lib/utils.js +14 -0
- package/app/lib/yaml-loader.js +327 -0
- package/app/main.js +213 -0
- package/app/model/agent.js +702 -0
- package/app/model/checklist.js +137 -0
- package/app/model/derivation.js +699 -0
- package/app/model/index-generator.js +71 -0
- package/app/model/interview.js +539 -0
- package/app/model/job.js +222 -0
- package/app/model/levels.js +591 -0
- package/app/model/loader.js +564 -0
- package/app/model/matching.js +858 -0
- package/app/model/modifiers.js +158 -0
- package/app/model/profile.js +266 -0
- package/app/model/progression.js +507 -0
- package/app/model/validation.js +1385 -0
- package/app/pages/agent-builder.js +823 -0
- package/app/pages/assessment-results.js +507 -0
- package/app/pages/behaviour.js +70 -0
- package/app/pages/discipline.js +71 -0
- package/app/pages/driver.js +106 -0
- package/app/pages/grade.js +117 -0
- package/app/pages/interview-builder.js +50 -0
- package/app/pages/interview.js +304 -0
- package/app/pages/job-builder.js +50 -0
- package/app/pages/job.js +58 -0
- package/app/pages/landing.js +305 -0
- package/app/pages/progress-builder.js +58 -0
- package/app/pages/progress.js +495 -0
- package/app/pages/self-assessment.js +729 -0
- package/app/pages/skill.js +113 -0
- package/app/pages/stage.js +231 -0
- package/app/pages/track.js +69 -0
- package/app/slide-main.js +360 -0
- package/app/slides/behaviour.js +38 -0
- package/app/slides/chapter.js +82 -0
- package/app/slides/discipline.js +40 -0
- package/app/slides/driver.js +39 -0
- package/app/slides/grade.js +32 -0
- package/app/slides/index.js +198 -0
- package/app/slides/interview.js +58 -0
- package/app/slides/job.js +55 -0
- package/app/slides/overview.js +126 -0
- package/app/slides/progress.js +83 -0
- package/app/slides/skill.js +40 -0
- package/app/slides/track.js +39 -0
- package/app/slides.html +56 -0
- package/app/types.js +147 -0
- package/bin/pathway.js +489 -0
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
- package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
- package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
- package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
- package/examples/agents/.vscode/settings.json +8 -0
- package/examples/behaviours/_index.yaml +8 -0
- package/examples/behaviours/outcome_ownership.yaml +44 -0
- package/examples/behaviours/polymathic_knowledge.yaml +42 -0
- package/examples/behaviours/precise_communication.yaml +40 -0
- package/examples/behaviours/relentless_curiosity.yaml +38 -0
- package/examples/behaviours/systems_thinking.yaml +41 -0
- package/examples/capabilities/_index.yaml +8 -0
- package/examples/capabilities/business.yaml +251 -0
- package/examples/capabilities/delivery.yaml +352 -0
- package/examples/capabilities/people.yaml +100 -0
- package/examples/capabilities/reliability.yaml +318 -0
- package/examples/capabilities/scale.yaml +394 -0
- package/examples/disciplines/_index.yaml +5 -0
- package/examples/disciplines/data_engineering.yaml +76 -0
- package/examples/disciplines/software_engineering.yaml +76 -0
- package/examples/drivers.yaml +205 -0
- package/examples/framework.yaml +58 -0
- package/examples/grades.yaml +118 -0
- package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
- package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
- package/examples/questions/behaviours/precise_communication.yaml +55 -0
- package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
- package/examples/questions/behaviours/systems_thinking.yaml +53 -0
- package/examples/questions/skills/architecture_design.yaml +54 -0
- package/examples/questions/skills/cloud_platforms.yaml +48 -0
- package/examples/questions/skills/code_quality.yaml +49 -0
- package/examples/questions/skills/data_modeling.yaml +46 -0
- package/examples/questions/skills/devops.yaml +47 -0
- package/examples/questions/skills/full_stack_development.yaml +48 -0
- package/examples/questions/skills/sre_practices.yaml +44 -0
- package/examples/questions/skills/stakeholder_management.yaml +49 -0
- package/examples/questions/skills/team_collaboration.yaml +43 -0
- package/examples/questions/skills/technical_writing.yaml +43 -0
- package/examples/self-assessments.yaml +66 -0
- package/examples/stages.yaml +76 -0
- package/examples/tracks/_index.yaml +6 -0
- package/examples/tracks/manager.yaml +53 -0
- package/examples/tracks/platform.yaml +54 -0
- package/examples/tracks/sre.yaml +58 -0
- package/examples/vscode-settings.yaml +22 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1385 @@
|
|
|
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
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
60
|
+
*/
|
|
61
|
+
function validateSkill(skill, index) {
|
|
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.levelDescriptions) {
|
|
102
|
+
warnings.push(
|
|
103
|
+
createWarning(
|
|
104
|
+
"MISSING_OPTIONAL",
|
|
105
|
+
"Skill missing level descriptions",
|
|
106
|
+
`${path}.levelDescriptions`,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { errors, warnings };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate that a behaviour has required properties
|
|
116
|
+
* @param {import('./levels.js').Behaviour} behaviour - Behaviour to validate
|
|
117
|
+
* @param {number} index - Index in the behaviours array
|
|
118
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
119
|
+
*/
|
|
120
|
+
function validateBehaviour(behaviour, index) {
|
|
121
|
+
const errors = [];
|
|
122
|
+
const warnings = [];
|
|
123
|
+
const path = `behaviours[${index}]`;
|
|
124
|
+
|
|
125
|
+
// id is derived from filename by the loader
|
|
126
|
+
if (!behaviour.name) {
|
|
127
|
+
errors.push(
|
|
128
|
+
createError("MISSING_REQUIRED", "Behaviour missing name", `${path}.name`),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (!behaviour.description) {
|
|
132
|
+
warnings.push(
|
|
133
|
+
createWarning(
|
|
134
|
+
"MISSING_OPTIONAL",
|
|
135
|
+
"Behaviour missing description",
|
|
136
|
+
`${path}.description`,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (!behaviour.maturityDescriptions) {
|
|
141
|
+
warnings.push(
|
|
142
|
+
createWarning(
|
|
143
|
+
"MISSING_OPTIONAL",
|
|
144
|
+
"Behaviour missing maturity descriptions",
|
|
145
|
+
`${path}.maturityDescriptions`,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { errors, warnings };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate that a driver has required properties and valid references
|
|
155
|
+
* @param {import('./levels.js').Driver} driver - Driver to validate
|
|
156
|
+
* @param {number} index - Index in the drivers array
|
|
157
|
+
* @param {Set<string>} skillIds - Set of valid skill IDs
|
|
158
|
+
* @param {Set<string>} behaviourIds - Set of valid behaviour IDs
|
|
159
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
160
|
+
*/
|
|
161
|
+
function validateDriver(driver, index, skillIds, behaviourIds) {
|
|
162
|
+
const errors = [];
|
|
163
|
+
const warnings = [];
|
|
164
|
+
const path = `drivers[${index}]`;
|
|
165
|
+
|
|
166
|
+
if (!driver.id) {
|
|
167
|
+
errors.push(createError("MISSING_REQUIRED", "Driver missing id", path));
|
|
168
|
+
}
|
|
169
|
+
if (!driver.name) {
|
|
170
|
+
errors.push(
|
|
171
|
+
createError("MISSING_REQUIRED", "Driver missing name", `${path}.name`),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (!driver.description) {
|
|
175
|
+
warnings.push(
|
|
176
|
+
createWarning(
|
|
177
|
+
"MISSING_OPTIONAL",
|
|
178
|
+
"Driver missing description",
|
|
179
|
+
`${path}.description`,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Validate contributing skills
|
|
185
|
+
if (driver.contributingSkills) {
|
|
186
|
+
driver.contributingSkills.forEach((skillId, i) => {
|
|
187
|
+
if (!skillIds.has(skillId)) {
|
|
188
|
+
errors.push(
|
|
189
|
+
createError(
|
|
190
|
+
"INVALID_REFERENCE",
|
|
191
|
+
`Driver "${driver.id}" references non-existent skill: ${skillId}`,
|
|
192
|
+
`${path}.contributingSkills[${i}]`,
|
|
193
|
+
skillId,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Validate contributing behaviours
|
|
201
|
+
if (driver.contributingBehaviours) {
|
|
202
|
+
driver.contributingBehaviours.forEach((behaviourId, i) => {
|
|
203
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
204
|
+
errors.push(
|
|
205
|
+
createError(
|
|
206
|
+
"INVALID_REFERENCE",
|
|
207
|
+
`Driver "${driver.id}" references non-existent behaviour: ${behaviourId}`,
|
|
208
|
+
`${path}.contributingBehaviours[${i}]`,
|
|
209
|
+
behaviourId,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { errors, warnings };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validate that a discipline has required properties and valid references
|
|
221
|
+
* @param {import('./levels.js').Discipline} discipline - Discipline to validate
|
|
222
|
+
* @param {number} index - Index in the disciplines array
|
|
223
|
+
* @param {Set<string>} skillIds - Set of valid skill IDs
|
|
224
|
+
* @param {Set<string>} behaviourIds - Set of valid behaviour IDs
|
|
225
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
226
|
+
*/
|
|
227
|
+
function validateDiscipline(discipline, index, skillIds, behaviourIds) {
|
|
228
|
+
const errors = [];
|
|
229
|
+
const warnings = [];
|
|
230
|
+
const path = `disciplines[${index}]`;
|
|
231
|
+
|
|
232
|
+
// id is derived from filename by the loader
|
|
233
|
+
if (!discipline.specialization) {
|
|
234
|
+
errors.push(
|
|
235
|
+
createError(
|
|
236
|
+
"MISSING_REQUIRED",
|
|
237
|
+
"Discipline missing specialization",
|
|
238
|
+
`${path}.specialization`,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (!discipline.roleTitle) {
|
|
243
|
+
errors.push(
|
|
244
|
+
createError(
|
|
245
|
+
"MISSING_REQUIRED",
|
|
246
|
+
"Discipline missing roleTitle",
|
|
247
|
+
`${path}.roleTitle`,
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Validate core skills
|
|
253
|
+
if (!discipline.coreSkills || discipline.coreSkills.length === 0) {
|
|
254
|
+
errors.push(
|
|
255
|
+
createError(
|
|
256
|
+
"MISSING_REQUIRED",
|
|
257
|
+
"Discipline must have at least one core skill",
|
|
258
|
+
`${path}.coreSkills`,
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
} else {
|
|
262
|
+
discipline.coreSkills.forEach((skillId, i) => {
|
|
263
|
+
if (!skillIds.has(skillId)) {
|
|
264
|
+
errors.push(
|
|
265
|
+
createError(
|
|
266
|
+
"INVALID_REFERENCE",
|
|
267
|
+
`Discipline "${discipline.id}" references non-existent core skill: ${skillId}`,
|
|
268
|
+
`${path}.coreSkills[${i}]`,
|
|
269
|
+
skillId,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Validate supporting skills
|
|
277
|
+
if (discipline.supportingSkills) {
|
|
278
|
+
discipline.supportingSkills.forEach((skillId, i) => {
|
|
279
|
+
if (!skillIds.has(skillId)) {
|
|
280
|
+
errors.push(
|
|
281
|
+
createError(
|
|
282
|
+
"INVALID_REFERENCE",
|
|
283
|
+
`Discipline "${discipline.id}" references non-existent supporting skill: ${skillId}`,
|
|
284
|
+
`${path}.supportingSkills[${i}]`,
|
|
285
|
+
skillId,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Validate broad skills
|
|
293
|
+
if (discipline.broadSkills) {
|
|
294
|
+
discipline.broadSkills.forEach((skillId, i) => {
|
|
295
|
+
if (!skillIds.has(skillId)) {
|
|
296
|
+
errors.push(
|
|
297
|
+
createError(
|
|
298
|
+
"INVALID_REFERENCE",
|
|
299
|
+
`Discipline "${discipline.id}" references non-existent broad skill: ${skillId}`,
|
|
300
|
+
`${path}.broadSkills[${i}]`,
|
|
301
|
+
skillId,
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Validate behaviour modifiers
|
|
309
|
+
if (discipline.behaviourModifiers) {
|
|
310
|
+
Object.entries(discipline.behaviourModifiers).forEach(
|
|
311
|
+
([behaviourId, modifier]) => {
|
|
312
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
313
|
+
errors.push(
|
|
314
|
+
createError(
|
|
315
|
+
"INVALID_REFERENCE",
|
|
316
|
+
`Discipline "${discipline.id}" references non-existent behaviour: ${behaviourId}`,
|
|
317
|
+
`${path}.behaviourModifiers.${behaviourId}`,
|
|
318
|
+
behaviourId,
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (typeof modifier !== "number" || modifier < -1 || modifier > 1) {
|
|
323
|
+
errors.push(
|
|
324
|
+
createError(
|
|
325
|
+
"INVALID_VALUE",
|
|
326
|
+
`Discipline "${discipline.id}" has invalid behaviour modifier: ${modifier} (must be -1, 0, or 1)`,
|
|
327
|
+
`${path}.behaviourModifiers.${behaviourId}`,
|
|
328
|
+
modifier,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { errors, warnings };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get all skill IDs referenced by any discipline
|
|
341
|
+
* @param {import('./levels.js').Discipline[]} disciplines - Array of disciplines
|
|
342
|
+
* @returns {Set<string>} Set of all referenced skill IDs
|
|
343
|
+
*/
|
|
344
|
+
function getAllDisciplineSkillIds(disciplines) {
|
|
345
|
+
const skillIds = new Set();
|
|
346
|
+
for (const discipline of disciplines) {
|
|
347
|
+
(discipline.coreSkills || []).forEach((id) => skillIds.add(id));
|
|
348
|
+
(discipline.supportingSkills || []).forEach((id) => skillIds.add(id));
|
|
349
|
+
(discipline.broadSkills || []).forEach((id) => skillIds.add(id));
|
|
350
|
+
}
|
|
351
|
+
return skillIds;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Validate that a track has required properties and valid references
|
|
356
|
+
* @param {import('./levels.js').Track} track - Track to validate
|
|
357
|
+
* @param {number} index - Index in the tracks array
|
|
358
|
+
* @param {Set<string>} disciplineSkillIds - Set of skill IDs used in any discipline
|
|
359
|
+
* @param {Set<string>} behaviourIds - Set of valid behaviour IDs
|
|
360
|
+
* @param {Set<string>} disciplineIds - Set of valid discipline IDs
|
|
361
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
362
|
+
*/
|
|
363
|
+
function validateTrack(
|
|
364
|
+
track,
|
|
365
|
+
index,
|
|
366
|
+
disciplineSkillIds,
|
|
367
|
+
behaviourIds,
|
|
368
|
+
disciplineIds,
|
|
369
|
+
gradeIds,
|
|
370
|
+
) {
|
|
371
|
+
const errors = [];
|
|
372
|
+
const warnings = [];
|
|
373
|
+
const path = `tracks[${index}]`;
|
|
374
|
+
|
|
375
|
+
// id is derived from filename by the loader
|
|
376
|
+
if (!track.name) {
|
|
377
|
+
errors.push(
|
|
378
|
+
createError("MISSING_REQUIRED", "Track missing name", `${path}.name`),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Validate isProfessional/isManagement booleans (optional, default to isProfessional: true)
|
|
383
|
+
if (
|
|
384
|
+
track.isProfessional !== undefined &&
|
|
385
|
+
typeof track.isProfessional !== "boolean"
|
|
386
|
+
) {
|
|
387
|
+
errors.push(
|
|
388
|
+
createError(
|
|
389
|
+
"INVALID_VALUE",
|
|
390
|
+
`Track "${track.id}" has invalid isProfessional value: ${track.isProfessional} (must be boolean)`,
|
|
391
|
+
`${path}.isProfessional`,
|
|
392
|
+
track.isProfessional,
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (
|
|
397
|
+
track.isManagement !== undefined &&
|
|
398
|
+
typeof track.isManagement !== "boolean"
|
|
399
|
+
) {
|
|
400
|
+
errors.push(
|
|
401
|
+
createError(
|
|
402
|
+
"INVALID_VALUE",
|
|
403
|
+
`Track "${track.id}" has invalid isManagement value: ${track.isManagement} (must be boolean)`,
|
|
404
|
+
`${path}.isManagement`,
|
|
405
|
+
track.isManagement,
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Validate skill modifiers - must be capabilities only (not individual skill IDs)
|
|
411
|
+
if (track.skillModifiers) {
|
|
412
|
+
Object.entries(track.skillModifiers).forEach(([key, modifier]) => {
|
|
413
|
+
// Key must be a capability - individual skill IDs are not allowed
|
|
414
|
+
if (!isCapability(key)) {
|
|
415
|
+
errors.push(
|
|
416
|
+
createError(
|
|
417
|
+
"INVALID_SKILL_MODIFIER_KEY",
|
|
418
|
+
`Track "${track.id}" has invalid skillModifier key "${key}". Only capability names are allowed: delivery, data, ai, scale, reliability, people, process, business, documentation`,
|
|
419
|
+
`${path}.skillModifiers.${key}`,
|
|
420
|
+
key,
|
|
421
|
+
),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if (typeof modifier !== "number" || !Number.isInteger(modifier)) {
|
|
425
|
+
errors.push(
|
|
426
|
+
createError(
|
|
427
|
+
"INVALID_VALUE",
|
|
428
|
+
`Track "${track.id}" has invalid skill modifier: ${modifier} (must be an integer)`,
|
|
429
|
+
`${path}.skillModifiers.${key}`,
|
|
430
|
+
modifier,
|
|
431
|
+
),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Validate behaviour modifiers
|
|
438
|
+
if (track.behaviourModifiers) {
|
|
439
|
+
Object.entries(track.behaviourModifiers).forEach(
|
|
440
|
+
([behaviourId, modifier]) => {
|
|
441
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
442
|
+
errors.push(
|
|
443
|
+
createError(
|
|
444
|
+
"INVALID_REFERENCE",
|
|
445
|
+
`Track "${track.id}" references non-existent behaviour: ${behaviourId}`,
|
|
446
|
+
`${path}.behaviourModifiers.${behaviourId}`,
|
|
447
|
+
behaviourId,
|
|
448
|
+
),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if (typeof modifier !== "number" || !Number.isInteger(modifier)) {
|
|
452
|
+
errors.push(
|
|
453
|
+
createError(
|
|
454
|
+
"INVALID_VALUE",
|
|
455
|
+
`Track "${track.id}" has invalid behaviour modifier: ${modifier} (must be an integer)`,
|
|
456
|
+
`${path}.behaviourModifiers.${behaviourId}`,
|
|
457
|
+
modifier,
|
|
458
|
+
),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Validate validDisciplines if specified
|
|
466
|
+
if (track.validDisciplines) {
|
|
467
|
+
track.validDisciplines.forEach((disciplineId, i) => {
|
|
468
|
+
if (!disciplineIds.has(disciplineId)) {
|
|
469
|
+
errors.push(
|
|
470
|
+
createError(
|
|
471
|
+
"INVALID_REFERENCE",
|
|
472
|
+
`Track "${track.id}" references non-existent discipline: ${disciplineId}`,
|
|
473
|
+
`${path}.validDisciplines[${i}]`,
|
|
474
|
+
disciplineId,
|
|
475
|
+
),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Validate minGrade if specified
|
|
482
|
+
if (track.minGrade) {
|
|
483
|
+
if (!gradeIds.has(track.minGrade)) {
|
|
484
|
+
errors.push(
|
|
485
|
+
createError(
|
|
486
|
+
"INVALID_REFERENCE",
|
|
487
|
+
`Track "${track.id}" references non-existent grade: ${track.minGrade}`,
|
|
488
|
+
`${path}.minGrade`,
|
|
489
|
+
track.minGrade,
|
|
490
|
+
),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Validate assessment weights if specified
|
|
496
|
+
if (track.assessmentWeights) {
|
|
497
|
+
const { skillWeight, behaviourWeight } = track.assessmentWeights;
|
|
498
|
+
if (typeof skillWeight !== "number" || skillWeight < 0 || skillWeight > 1) {
|
|
499
|
+
errors.push(
|
|
500
|
+
createError(
|
|
501
|
+
"INVALID_VALUE",
|
|
502
|
+
`Track "${track.id}" has invalid assessmentWeights.skillWeight: ${skillWeight}`,
|
|
503
|
+
`${path}.assessmentWeights.skillWeight`,
|
|
504
|
+
skillWeight,
|
|
505
|
+
),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (
|
|
509
|
+
typeof behaviourWeight !== "number" ||
|
|
510
|
+
behaviourWeight < 0 ||
|
|
511
|
+
behaviourWeight > 1
|
|
512
|
+
) {
|
|
513
|
+
errors.push(
|
|
514
|
+
createError(
|
|
515
|
+
"INVALID_VALUE",
|
|
516
|
+
`Track "${track.id}" has invalid assessmentWeights.behaviourWeight: ${behaviourWeight}`,
|
|
517
|
+
`${path}.assessmentWeights.behaviourWeight`,
|
|
518
|
+
behaviourWeight,
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (
|
|
523
|
+
typeof skillWeight === "number" &&
|
|
524
|
+
typeof behaviourWeight === "number"
|
|
525
|
+
) {
|
|
526
|
+
const sum = skillWeight + behaviourWeight;
|
|
527
|
+
if (Math.abs(sum - 1.0) > 0.001) {
|
|
528
|
+
errors.push(
|
|
529
|
+
createError(
|
|
530
|
+
"INVALID_VALUE",
|
|
531
|
+
`Track "${track.id}" assessmentWeights must sum to 1.0 (got ${sum})`,
|
|
532
|
+
`${path}.assessmentWeights`,
|
|
533
|
+
{ skillWeight, behaviourWeight },
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return { errors, warnings };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Validate that a grade has required properties and valid values
|
|
545
|
+
* @param {import('./levels.js').Grade} grade - Grade to validate
|
|
546
|
+
* @param {number} index - Index in the grades array
|
|
547
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
548
|
+
*/
|
|
549
|
+
function validateGrade(grade, index) {
|
|
550
|
+
const errors = [];
|
|
551
|
+
const warnings = [];
|
|
552
|
+
const path = `grades[${index}]`;
|
|
553
|
+
|
|
554
|
+
if (!grade.id) {
|
|
555
|
+
errors.push(createError("MISSING_REQUIRED", "Grade missing id", path));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!grade.professionalTitle) {
|
|
559
|
+
errors.push(
|
|
560
|
+
createError(
|
|
561
|
+
"MISSING_REQUIRED",
|
|
562
|
+
"Grade missing professionalTitle",
|
|
563
|
+
`${path}.professionalTitle`,
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (!grade.managementTitle) {
|
|
568
|
+
errors.push(
|
|
569
|
+
createError(
|
|
570
|
+
"MISSING_REQUIRED",
|
|
571
|
+
"Grade missing managementTitle",
|
|
572
|
+
`${path}.managementTitle`,
|
|
573
|
+
),
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (typeof grade.ordinalRank !== "number") {
|
|
578
|
+
errors.push(
|
|
579
|
+
createError(
|
|
580
|
+
"MISSING_REQUIRED",
|
|
581
|
+
"Grade missing numeric ordinalRank",
|
|
582
|
+
`${path}.ordinalRank`,
|
|
583
|
+
),
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Validate base skill levels
|
|
588
|
+
if (!grade.baseSkillLevels) {
|
|
589
|
+
errors.push(
|
|
590
|
+
createError(
|
|
591
|
+
"MISSING_REQUIRED",
|
|
592
|
+
"Grade missing baseSkillLevels",
|
|
593
|
+
`${path}.baseSkillLevels`,
|
|
594
|
+
),
|
|
595
|
+
);
|
|
596
|
+
} else {
|
|
597
|
+
["primary", "secondary", "broad"].forEach((type) => {
|
|
598
|
+
const level = grade.baseSkillLevels[type];
|
|
599
|
+
if (!level) {
|
|
600
|
+
errors.push(
|
|
601
|
+
createError(
|
|
602
|
+
"MISSING_REQUIRED",
|
|
603
|
+
`Grade missing baseSkillLevels.${type}`,
|
|
604
|
+
`${path}.baseSkillLevels.${type}`,
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
} else if (getSkillLevelIndex(level) === -1) {
|
|
608
|
+
errors.push(
|
|
609
|
+
createError(
|
|
610
|
+
"INVALID_VALUE",
|
|
611
|
+
`Grade "${grade.id}" has invalid baseSkillLevels.${type}: ${level}`,
|
|
612
|
+
`${path}.baseSkillLevels.${type}`,
|
|
613
|
+
level,
|
|
614
|
+
),
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Validate base behaviour maturity
|
|
621
|
+
if (!grade.baseBehaviourMaturity) {
|
|
622
|
+
errors.push(
|
|
623
|
+
createError(
|
|
624
|
+
"MISSING_REQUIRED",
|
|
625
|
+
"Grade missing baseBehaviourMaturity",
|
|
626
|
+
`${path}.baseBehaviourMaturity`,
|
|
627
|
+
),
|
|
628
|
+
);
|
|
629
|
+
} else if (getBehaviourMaturityIndex(grade.baseBehaviourMaturity) === -1) {
|
|
630
|
+
errors.push(
|
|
631
|
+
createError(
|
|
632
|
+
"INVALID_VALUE",
|
|
633
|
+
`Grade "${grade.id}" has invalid baseBehaviourMaturity: ${grade.baseBehaviourMaturity}`,
|
|
634
|
+
`${path}.baseBehaviourMaturity`,
|
|
635
|
+
grade.baseBehaviourMaturity,
|
|
636
|
+
),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Validate expectations
|
|
641
|
+
if (!grade.expectations) {
|
|
642
|
+
warnings.push(
|
|
643
|
+
createWarning(
|
|
644
|
+
"MISSING_OPTIONAL",
|
|
645
|
+
"Grade missing expectations",
|
|
646
|
+
`${path}.expectations`,
|
|
647
|
+
),
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Validate yearsExperience if present (should be a string like "0-2" or "20+")
|
|
652
|
+
if (
|
|
653
|
+
grade.yearsExperience !== undefined &&
|
|
654
|
+
typeof grade.yearsExperience !== "string"
|
|
655
|
+
) {
|
|
656
|
+
warnings.push(
|
|
657
|
+
createWarning(
|
|
658
|
+
"INVALID_VALUE",
|
|
659
|
+
"Grade yearsExperience should be a string",
|
|
660
|
+
`${path}.yearsExperience`,
|
|
661
|
+
),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return { errors, warnings };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Validate that a capability has required properties
|
|
670
|
+
* @param {Object} capability - Capability to validate
|
|
671
|
+
* @param {number} index - Index in the capabilities array
|
|
672
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
673
|
+
*/
|
|
674
|
+
function validateCapability(capability, index) {
|
|
675
|
+
const errors = [];
|
|
676
|
+
const warnings = [];
|
|
677
|
+
const path = `capabilities[${index}]`;
|
|
678
|
+
|
|
679
|
+
// id is derived from filename by the loader
|
|
680
|
+
if (!capability.name) {
|
|
681
|
+
errors.push(
|
|
682
|
+
createError(
|
|
683
|
+
"MISSING_REQUIRED",
|
|
684
|
+
"Capability missing name",
|
|
685
|
+
`${path}.name`,
|
|
686
|
+
),
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
if (!capability.emoji) {
|
|
690
|
+
warnings.push(
|
|
691
|
+
createWarning(
|
|
692
|
+
"MISSING_OPTIONAL",
|
|
693
|
+
"Capability missing emoji",
|
|
694
|
+
`${path}.emoji`,
|
|
695
|
+
),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Validate professionalResponsibilities and managementResponsibilities
|
|
700
|
+
const expectedLevels = [
|
|
701
|
+
"awareness",
|
|
702
|
+
"foundational",
|
|
703
|
+
"working",
|
|
704
|
+
"practitioner",
|
|
705
|
+
"expert",
|
|
706
|
+
];
|
|
707
|
+
|
|
708
|
+
if (!capability.professionalResponsibilities) {
|
|
709
|
+
warnings.push(
|
|
710
|
+
createWarning(
|
|
711
|
+
"MISSING_OPTIONAL",
|
|
712
|
+
"Capability missing professionalResponsibilities",
|
|
713
|
+
`${path}.professionalResponsibilities`,
|
|
714
|
+
),
|
|
715
|
+
);
|
|
716
|
+
} else {
|
|
717
|
+
for (const level of expectedLevels) {
|
|
718
|
+
if (!capability.professionalResponsibilities[level]) {
|
|
719
|
+
warnings.push(
|
|
720
|
+
createWarning(
|
|
721
|
+
"MISSING_OPTIONAL",
|
|
722
|
+
`Capability missing ${level} professional responsibility`,
|
|
723
|
+
`${path}.professionalResponsibilities.${level}`,
|
|
724
|
+
),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!capability.managementResponsibilities) {
|
|
731
|
+
warnings.push(
|
|
732
|
+
createWarning(
|
|
733
|
+
"MISSING_OPTIONAL",
|
|
734
|
+
"Capability missing managementResponsibilities",
|
|
735
|
+
`${path}.managementResponsibilities`,
|
|
736
|
+
),
|
|
737
|
+
);
|
|
738
|
+
} else {
|
|
739
|
+
for (const level of expectedLevels) {
|
|
740
|
+
if (!capability.managementResponsibilities[level]) {
|
|
741
|
+
warnings.push(
|
|
742
|
+
createWarning(
|
|
743
|
+
"MISSING_OPTIONAL",
|
|
744
|
+
`Capability missing ${level} management responsibility`,
|
|
745
|
+
`${path}.managementResponsibilities.${level}`,
|
|
746
|
+
),
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return { errors, warnings };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Validate a stage object
|
|
757
|
+
* @param {Object} stage - Stage to validate
|
|
758
|
+
* @param {number} index - Index in the stages array
|
|
759
|
+
* @returns {{errors: Array, warnings: Array}}
|
|
760
|
+
*/
|
|
761
|
+
function validateStage(stage, index) {
|
|
762
|
+
const errors = [];
|
|
763
|
+
const warnings = [];
|
|
764
|
+
const path = `stages[${index}]`;
|
|
765
|
+
|
|
766
|
+
if (!stage.id) {
|
|
767
|
+
errors.push(createError("MISSING_REQUIRED", "Stage missing id", path));
|
|
768
|
+
} else if (!Object.values(Stage).includes(stage.id)) {
|
|
769
|
+
errors.push(
|
|
770
|
+
createError(
|
|
771
|
+
"INVALID_VALUE",
|
|
772
|
+
`Invalid stage id: ${stage.id}`,
|
|
773
|
+
`${path}.id`,
|
|
774
|
+
stage.id,
|
|
775
|
+
),
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (!stage.name) {
|
|
780
|
+
errors.push(
|
|
781
|
+
createError("MISSING_REQUIRED", "Stage missing name", `${path}.name`),
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!stage.description) {
|
|
786
|
+
warnings.push(
|
|
787
|
+
createWarning(
|
|
788
|
+
"MISSING_OPTIONAL",
|
|
789
|
+
"Stage missing description",
|
|
790
|
+
`${path}.description`,
|
|
791
|
+
),
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Mode is now inferred from availableTools - no longer required
|
|
796
|
+
// Validate availableTools array
|
|
797
|
+
if (!stage.availableTools || !Array.isArray(stage.availableTools)) {
|
|
798
|
+
errors.push(
|
|
799
|
+
createError(
|
|
800
|
+
"MISSING_REQUIRED",
|
|
801
|
+
"Stage missing availableTools array",
|
|
802
|
+
`${path}.availableTools`,
|
|
803
|
+
),
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!stage.handoffs || !Array.isArray(stage.handoffs)) {
|
|
808
|
+
warnings.push(
|
|
809
|
+
createWarning(
|
|
810
|
+
"MISSING_OPTIONAL",
|
|
811
|
+
"Stage missing handoffs array",
|
|
812
|
+
`${path}.handoffs`,
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
} else {
|
|
816
|
+
stage.handoffs.forEach((handoff, hIndex) => {
|
|
817
|
+
if (!handoff.targetStage) {
|
|
818
|
+
errors.push(
|
|
819
|
+
createError(
|
|
820
|
+
"MISSING_REQUIRED",
|
|
821
|
+
"Handoff missing targetStage",
|
|
822
|
+
`${path}.handoffs[${hIndex}].targetStage`,
|
|
823
|
+
),
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if (!handoff.label) {
|
|
827
|
+
errors.push(
|
|
828
|
+
createError(
|
|
829
|
+
"MISSING_REQUIRED",
|
|
830
|
+
"Handoff missing label",
|
|
831
|
+
`${path}.handoffs[${hIndex}].label`,
|
|
832
|
+
),
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (!handoff.prompt) {
|
|
836
|
+
errors.push(
|
|
837
|
+
createError(
|
|
838
|
+
"MISSING_REQUIRED",
|
|
839
|
+
"Handoff missing prompt",
|
|
840
|
+
`${path}.handoffs[${hIndex}].prompt`,
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return { errors, warnings };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Validate a self-assessment object
|
|
852
|
+
* @param {import('./levels.js').SelfAssessment} selfAssessment - Self-assessment to validate
|
|
853
|
+
* @param {import('./levels.js').Skill[]} skills - Array of valid skills
|
|
854
|
+
* @param {import('./levels.js').Behaviour[]} behaviours - Array of valid behaviours
|
|
855
|
+
* @returns {import('./levels.js').ValidationResult}
|
|
856
|
+
*/
|
|
857
|
+
export function validateSelfAssessment(selfAssessment, skills, behaviours) {
|
|
858
|
+
const errors = [];
|
|
859
|
+
const warnings = [];
|
|
860
|
+
const skillIds = new Set(skills.map((s) => s.id));
|
|
861
|
+
const behaviourIds = new Set(behaviours.map((b) => b.id));
|
|
862
|
+
|
|
863
|
+
if (!selfAssessment) {
|
|
864
|
+
return createValidationResult(false, [
|
|
865
|
+
createError("MISSING_REQUIRED", "Self-assessment is required"),
|
|
866
|
+
]);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Validate skill assessments
|
|
870
|
+
if (
|
|
871
|
+
!selfAssessment.skillLevels ||
|
|
872
|
+
Object.keys(selfAssessment.skillLevels).length === 0
|
|
873
|
+
) {
|
|
874
|
+
warnings.push(
|
|
875
|
+
createWarning(
|
|
876
|
+
"MISSING_OPTIONAL",
|
|
877
|
+
"Self-assessment has no skill assessments",
|
|
878
|
+
),
|
|
879
|
+
);
|
|
880
|
+
} else {
|
|
881
|
+
Object.entries(selfAssessment.skillLevels).forEach(([skillId, level]) => {
|
|
882
|
+
if (!skillIds.has(skillId)) {
|
|
883
|
+
errors.push(
|
|
884
|
+
createError(
|
|
885
|
+
"INVALID_REFERENCE",
|
|
886
|
+
`Self-assessment references non-existent skill: ${skillId}`,
|
|
887
|
+
`selfAssessment.skillLevels.${skillId}`,
|
|
888
|
+
skillId,
|
|
889
|
+
),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (getSkillLevelIndex(level) === -1) {
|
|
893
|
+
errors.push(
|
|
894
|
+
createError(
|
|
895
|
+
"INVALID_VALUE",
|
|
896
|
+
`Self-assessment has invalid skill level for ${skillId}: ${level}`,
|
|
897
|
+
`selfAssessment.skillLevels.${skillId}`,
|
|
898
|
+
level,
|
|
899
|
+
),
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Validate behaviour assessments
|
|
906
|
+
if (
|
|
907
|
+
!selfAssessment.behaviourMaturities ||
|
|
908
|
+
Object.keys(selfAssessment.behaviourMaturities).length === 0
|
|
909
|
+
) {
|
|
910
|
+
warnings.push(
|
|
911
|
+
createWarning(
|
|
912
|
+
"MISSING_OPTIONAL",
|
|
913
|
+
"Self-assessment has no behaviour assessments",
|
|
914
|
+
),
|
|
915
|
+
);
|
|
916
|
+
} else {
|
|
917
|
+
Object.entries(selfAssessment.behaviourMaturities).forEach(
|
|
918
|
+
([behaviourId, maturity]) => {
|
|
919
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
920
|
+
errors.push(
|
|
921
|
+
createError(
|
|
922
|
+
"INVALID_REFERENCE",
|
|
923
|
+
`Self-assessment references non-existent behaviour: ${behaviourId}`,
|
|
924
|
+
`selfAssessment.behaviourMaturities.${behaviourId}`,
|
|
925
|
+
behaviourId,
|
|
926
|
+
),
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
if (getBehaviourMaturityIndex(maturity) === -1) {
|
|
930
|
+
errors.push(
|
|
931
|
+
createError(
|
|
932
|
+
"INVALID_VALUE",
|
|
933
|
+
`Self-assessment has invalid behaviour maturity for ${behaviourId}: ${maturity}`,
|
|
934
|
+
`selfAssessment.behaviourMaturities.${behaviourId}`,
|
|
935
|
+
maturity,
|
|
936
|
+
),
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return createValidationResult(errors.length === 0, errors, warnings);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Validate all data with referential integrity checks
|
|
948
|
+
* @param {Object} data - All data to validate
|
|
949
|
+
* @param {import('./levels.js').Driver[]} data.drivers - Drivers
|
|
950
|
+
* @param {import('./levels.js').Behaviour[]} data.behaviours - Behaviours
|
|
951
|
+
* @param {import('./levels.js').Skill[]} data.skills - Skills
|
|
952
|
+
* @param {import('./levels.js').Discipline[]} data.disciplines - Disciplines
|
|
953
|
+
* @param {import('./levels.js').Track[]} data.tracks - Tracks
|
|
954
|
+
* @param {import('./levels.js').Grade[]} data.grades - Grades
|
|
955
|
+
* @param {Object[]} data.capabilities - Capabilities
|
|
956
|
+
* @param {Object[]} [data.stages] - Stages
|
|
957
|
+
* @returns {import('./levels.js').ValidationResult}
|
|
958
|
+
*/
|
|
959
|
+
export function validateAllData({
|
|
960
|
+
drivers,
|
|
961
|
+
behaviours,
|
|
962
|
+
skills,
|
|
963
|
+
disciplines,
|
|
964
|
+
tracks,
|
|
965
|
+
grades,
|
|
966
|
+
capabilities,
|
|
967
|
+
stages,
|
|
968
|
+
}) {
|
|
969
|
+
const allErrors = [];
|
|
970
|
+
const allWarnings = [];
|
|
971
|
+
|
|
972
|
+
// Build ID sets for reference validation
|
|
973
|
+
const skillIds = new Set((skills || []).map((s) => s.id));
|
|
974
|
+
const behaviourIds = new Set((behaviours || []).map((b) => b.id));
|
|
975
|
+
const capabilityIds = new Set((capabilities || []).map((c) => c.id));
|
|
976
|
+
|
|
977
|
+
// Validate skills
|
|
978
|
+
if (!skills || skills.length === 0) {
|
|
979
|
+
allErrors.push(
|
|
980
|
+
createError("MISSING_REQUIRED", "At least one skill is required"),
|
|
981
|
+
);
|
|
982
|
+
} else {
|
|
983
|
+
skills.forEach((skill, index) => {
|
|
984
|
+
const { errors, warnings } = validateSkill(skill, index);
|
|
985
|
+
allErrors.push(...errors);
|
|
986
|
+
allWarnings.push(...warnings);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// Check for duplicate IDs
|
|
990
|
+
const seenIds = new Set();
|
|
991
|
+
skills.forEach((skill, index) => {
|
|
992
|
+
if (skill.id) {
|
|
993
|
+
if (seenIds.has(skill.id)) {
|
|
994
|
+
allErrors.push(
|
|
995
|
+
createError(
|
|
996
|
+
"DUPLICATE_ID",
|
|
997
|
+
`Duplicate skill ID: ${skill.id}`,
|
|
998
|
+
`skills[${index}]`,
|
|
999
|
+
skill.id,
|
|
1000
|
+
),
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
seenIds.add(skill.id);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Validate behaviours
|
|
1009
|
+
if (!behaviours || behaviours.length === 0) {
|
|
1010
|
+
allErrors.push(
|
|
1011
|
+
createError("MISSING_REQUIRED", "At least one behaviour is required"),
|
|
1012
|
+
);
|
|
1013
|
+
} else {
|
|
1014
|
+
behaviours.forEach((behaviour, index) => {
|
|
1015
|
+
const { errors, warnings } = validateBehaviour(behaviour, index);
|
|
1016
|
+
allErrors.push(...errors);
|
|
1017
|
+
allWarnings.push(...warnings);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// Check for duplicate IDs
|
|
1021
|
+
const seenIds = new Set();
|
|
1022
|
+
behaviours.forEach((behaviour, index) => {
|
|
1023
|
+
if (behaviour.id) {
|
|
1024
|
+
if (seenIds.has(behaviour.id)) {
|
|
1025
|
+
allErrors.push(
|
|
1026
|
+
createError(
|
|
1027
|
+
"DUPLICATE_ID",
|
|
1028
|
+
`Duplicate behaviour ID: ${behaviour.id}`,
|
|
1029
|
+
`behaviours[${index}]`,
|
|
1030
|
+
behaviour.id,
|
|
1031
|
+
),
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
seenIds.add(behaviour.id);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Validate disciplines
|
|
1040
|
+
if (!disciplines || disciplines.length === 0) {
|
|
1041
|
+
allErrors.push(
|
|
1042
|
+
createError("MISSING_REQUIRED", "At least one discipline is required"),
|
|
1043
|
+
);
|
|
1044
|
+
} else {
|
|
1045
|
+
disciplines.forEach((discipline, index) => {
|
|
1046
|
+
const { errors, warnings } = validateDiscipline(
|
|
1047
|
+
discipline,
|
|
1048
|
+
index,
|
|
1049
|
+
skillIds,
|
|
1050
|
+
behaviourIds,
|
|
1051
|
+
);
|
|
1052
|
+
allErrors.push(...errors);
|
|
1053
|
+
allWarnings.push(...warnings);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// Check for duplicate IDs
|
|
1057
|
+
const seenIds = new Set();
|
|
1058
|
+
disciplines.forEach((discipline, index) => {
|
|
1059
|
+
if (discipline.id) {
|
|
1060
|
+
if (seenIds.has(discipline.id)) {
|
|
1061
|
+
allErrors.push(
|
|
1062
|
+
createError(
|
|
1063
|
+
"DUPLICATE_ID",
|
|
1064
|
+
`Duplicate discipline ID: ${discipline.id}`,
|
|
1065
|
+
`disciplines[${index}]`,
|
|
1066
|
+
discipline.id,
|
|
1067
|
+
),
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
seenIds.add(discipline.id);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Get all skill IDs from disciplines for track validation
|
|
1076
|
+
const disciplineSkillIds = getAllDisciplineSkillIds(disciplines || []);
|
|
1077
|
+
|
|
1078
|
+
// Get discipline IDs for track validation
|
|
1079
|
+
const disciplineIdSet = new Set((disciplines || []).map((d) => d.id));
|
|
1080
|
+
|
|
1081
|
+
// Get grade IDs for track validation
|
|
1082
|
+
const gradeIdSet = new Set((grades || []).map((g) => g.id));
|
|
1083
|
+
|
|
1084
|
+
// Validate tracks
|
|
1085
|
+
if (!tracks || tracks.length === 0) {
|
|
1086
|
+
allErrors.push(
|
|
1087
|
+
createError("MISSING_REQUIRED", "At least one track is required"),
|
|
1088
|
+
);
|
|
1089
|
+
} else {
|
|
1090
|
+
tracks.forEach((track, index) => {
|
|
1091
|
+
const { errors, warnings } = validateTrack(
|
|
1092
|
+
track,
|
|
1093
|
+
index,
|
|
1094
|
+
disciplineSkillIds,
|
|
1095
|
+
behaviourIds,
|
|
1096
|
+
disciplineIdSet,
|
|
1097
|
+
gradeIdSet,
|
|
1098
|
+
);
|
|
1099
|
+
allErrors.push(...errors);
|
|
1100
|
+
allWarnings.push(...warnings);
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Check for duplicate IDs
|
|
1104
|
+
const seenIds = new Set();
|
|
1105
|
+
tracks.forEach((track, index) => {
|
|
1106
|
+
if (track.id) {
|
|
1107
|
+
if (seenIds.has(track.id)) {
|
|
1108
|
+
allErrors.push(
|
|
1109
|
+
createError(
|
|
1110
|
+
"DUPLICATE_ID",
|
|
1111
|
+
`Duplicate track ID: ${track.id}`,
|
|
1112
|
+
`tracks[${index}]`,
|
|
1113
|
+
track.id,
|
|
1114
|
+
),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
seenIds.add(track.id);
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Validate grades
|
|
1123
|
+
if (!grades || grades.length === 0) {
|
|
1124
|
+
allErrors.push(
|
|
1125
|
+
createError("MISSING_REQUIRED", "At least one grade is required"),
|
|
1126
|
+
);
|
|
1127
|
+
} else {
|
|
1128
|
+
grades.forEach((grade, index) => {
|
|
1129
|
+
const { errors, warnings } = validateGrade(grade, index);
|
|
1130
|
+
allErrors.push(...errors);
|
|
1131
|
+
allWarnings.push(...warnings);
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
// Check for duplicate IDs
|
|
1135
|
+
const seenIds = new Set();
|
|
1136
|
+
grades.forEach((grade, index) => {
|
|
1137
|
+
if (grade.id) {
|
|
1138
|
+
if (seenIds.has(grade.id)) {
|
|
1139
|
+
allErrors.push(
|
|
1140
|
+
createError(
|
|
1141
|
+
"DUPLICATE_ID",
|
|
1142
|
+
`Duplicate grade ID: ${grade.id}`,
|
|
1143
|
+
`grades[${index}]`,
|
|
1144
|
+
grade.id,
|
|
1145
|
+
),
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
seenIds.add(grade.id);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Validate capabilities (required)
|
|
1154
|
+
if (!capabilities || capabilities.length === 0) {
|
|
1155
|
+
allErrors.push(
|
|
1156
|
+
createError("MISSING_REQUIRED", "At least one capability is required"),
|
|
1157
|
+
);
|
|
1158
|
+
} else {
|
|
1159
|
+
capabilities.forEach((capability, index) => {
|
|
1160
|
+
const { errors, warnings } = validateCapability(capability, index);
|
|
1161
|
+
allErrors.push(...errors);
|
|
1162
|
+
allWarnings.push(...warnings);
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
// Check for duplicate IDs
|
|
1166
|
+
const seenIds = new Set();
|
|
1167
|
+
capabilities.forEach((capability, index) => {
|
|
1168
|
+
if (capability.id) {
|
|
1169
|
+
if (seenIds.has(capability.id)) {
|
|
1170
|
+
allErrors.push(
|
|
1171
|
+
createError(
|
|
1172
|
+
"DUPLICATE_ID",
|
|
1173
|
+
`Duplicate capability ID: ${capability.id}`,
|
|
1174
|
+
`capabilities[${index}]`,
|
|
1175
|
+
capability.id,
|
|
1176
|
+
),
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
seenIds.add(capability.id);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// Validate skill capability references against loaded capabilities
|
|
1184
|
+
if (skills && skills.length > 0) {
|
|
1185
|
+
skills.forEach((skill, index) => {
|
|
1186
|
+
if (skill.capability && !capabilityIds.has(skill.capability)) {
|
|
1187
|
+
allErrors.push(
|
|
1188
|
+
createError(
|
|
1189
|
+
"INVALID_REFERENCE",
|
|
1190
|
+
`Skill '${skill.id}' references unknown capability '${skill.capability}'`,
|
|
1191
|
+
`skills[${index}].capability`,
|
|
1192
|
+
skill.capability,
|
|
1193
|
+
),
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Validate stages (optional but validate if present)
|
|
1201
|
+
if (stages && stages.length > 0) {
|
|
1202
|
+
stages.forEach((stage, index) => {
|
|
1203
|
+
const { errors, warnings } = validateStage(stage, index);
|
|
1204
|
+
allErrors.push(...errors);
|
|
1205
|
+
allWarnings.push(...warnings);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// Check for duplicate IDs
|
|
1209
|
+
const seenIds = new Set();
|
|
1210
|
+
stages.forEach((stage, index) => {
|
|
1211
|
+
if (stage.id) {
|
|
1212
|
+
if (seenIds.has(stage.id)) {
|
|
1213
|
+
allErrors.push(
|
|
1214
|
+
createError(
|
|
1215
|
+
"DUPLICATE_ID",
|
|
1216
|
+
`Duplicate stage ID: ${stage.id}`,
|
|
1217
|
+
`stages[${index}]`,
|
|
1218
|
+
stage.id,
|
|
1219
|
+
),
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
seenIds.add(stage.id);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
// Validate handoff targets reference valid stages
|
|
1227
|
+
const stageIds = new Set(stages.map((s) => s.id));
|
|
1228
|
+
stages.forEach((stage, sIndex) => {
|
|
1229
|
+
if (stage.handoffs) {
|
|
1230
|
+
stage.handoffs.forEach((handoff, hIndex) => {
|
|
1231
|
+
if (handoff.target && !stageIds.has(handoff.target)) {
|
|
1232
|
+
allErrors.push(
|
|
1233
|
+
createError(
|
|
1234
|
+
"INVALID_REFERENCE",
|
|
1235
|
+
`Stage '${stage.id}' handoff references unknown stage '${handoff.target}'`,
|
|
1236
|
+
`stages[${sIndex}].handoffs[${hIndex}].target`,
|
|
1237
|
+
handoff.target,
|
|
1238
|
+
),
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Validate drivers (required)
|
|
1247
|
+
if (!drivers || drivers.length === 0) {
|
|
1248
|
+
allErrors.push(
|
|
1249
|
+
createError("MISSING_REQUIRED", "At least one driver is required"),
|
|
1250
|
+
);
|
|
1251
|
+
} else {
|
|
1252
|
+
drivers.forEach((driver, index) => {
|
|
1253
|
+
const { errors, warnings } = validateDriver(
|
|
1254
|
+
driver,
|
|
1255
|
+
index,
|
|
1256
|
+
skillIds,
|
|
1257
|
+
behaviourIds,
|
|
1258
|
+
);
|
|
1259
|
+
allErrors.push(...errors);
|
|
1260
|
+
allWarnings.push(...warnings);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
// Check for duplicate IDs
|
|
1264
|
+
const seenIds = new Set();
|
|
1265
|
+
drivers.forEach((driver, index) => {
|
|
1266
|
+
if (driver.id) {
|
|
1267
|
+
if (seenIds.has(driver.id)) {
|
|
1268
|
+
allErrors.push(
|
|
1269
|
+
createError(
|
|
1270
|
+
"DUPLICATE_ID",
|
|
1271
|
+
`Duplicate driver ID: ${driver.id}`,
|
|
1272
|
+
`drivers[${index}]`,
|
|
1273
|
+
driver.id,
|
|
1274
|
+
),
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
seenIds.add(driver.id);
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return createValidationResult(allErrors.length === 0, allErrors, allWarnings);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Validate question bank structure
|
|
1287
|
+
* @param {import('./levels.js').QuestionBank} questionBank - Question bank to validate
|
|
1288
|
+
* @param {import('./levels.js').Skill[]} skills - Valid skills
|
|
1289
|
+
* @param {import('./levels.js').Behaviour[]} behaviours - Valid behaviours
|
|
1290
|
+
* @returns {import('./levels.js').ValidationResult}
|
|
1291
|
+
*/
|
|
1292
|
+
export function validateQuestionBank(questionBank, skills, behaviours) {
|
|
1293
|
+
const errors = [];
|
|
1294
|
+
const warnings = [];
|
|
1295
|
+
const skillIds = new Set(skills.map((s) => s.id));
|
|
1296
|
+
const behaviourIds = new Set(behaviours.map((b) => b.id));
|
|
1297
|
+
|
|
1298
|
+
if (!questionBank) {
|
|
1299
|
+
return createValidationResult(false, [
|
|
1300
|
+
createError("MISSING_REQUIRED", "Question bank is required"),
|
|
1301
|
+
]);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Validate skill questions
|
|
1305
|
+
if (questionBank.skillLevels) {
|
|
1306
|
+
Object.entries(questionBank.skillLevels).forEach(
|
|
1307
|
+
([skillId, levelQuestions]) => {
|
|
1308
|
+
if (!skillIds.has(skillId)) {
|
|
1309
|
+
errors.push(
|
|
1310
|
+
createError(
|
|
1311
|
+
"INVALID_REFERENCE",
|
|
1312
|
+
`Question bank references non-existent skill: ${skillId}`,
|
|
1313
|
+
`questionBank.skillLevels.${skillId}`,
|
|
1314
|
+
skillId,
|
|
1315
|
+
),
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
Object.entries(levelQuestions || {}).forEach(([level, questions]) => {
|
|
1319
|
+
if (getSkillLevelIndex(level) === -1) {
|
|
1320
|
+
errors.push(
|
|
1321
|
+
createError(
|
|
1322
|
+
"INVALID_VALUE",
|
|
1323
|
+
`Question bank has invalid skill level: ${level}`,
|
|
1324
|
+
`questionBank.skillLevels.${skillId}.${level}`,
|
|
1325
|
+
level,
|
|
1326
|
+
),
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
1330
|
+
warnings.push(
|
|
1331
|
+
createWarning(
|
|
1332
|
+
"EMPTY_QUESTIONS",
|
|
1333
|
+
`No questions for skill ${skillId} at level ${level}`,
|
|
1334
|
+
`questionBank.skillLevels.${skillId}.${level}`,
|
|
1335
|
+
),
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
},
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Validate behaviour questions
|
|
1344
|
+
if (questionBank.behaviourMaturities) {
|
|
1345
|
+
Object.entries(questionBank.behaviourMaturities).forEach(
|
|
1346
|
+
([behaviourId, maturityQuestions]) => {
|
|
1347
|
+
if (!behaviourIds.has(behaviourId)) {
|
|
1348
|
+
errors.push(
|
|
1349
|
+
createError(
|
|
1350
|
+
"INVALID_REFERENCE",
|
|
1351
|
+
`Question bank references non-existent behaviour: ${behaviourId}`,
|
|
1352
|
+
`questionBank.behaviourMaturities.${behaviourId}`,
|
|
1353
|
+
behaviourId,
|
|
1354
|
+
),
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
Object.entries(maturityQuestions || {}).forEach(
|
|
1358
|
+
([maturity, questions]) => {
|
|
1359
|
+
if (getBehaviourMaturityIndex(maturity) === -1) {
|
|
1360
|
+
errors.push(
|
|
1361
|
+
createError(
|
|
1362
|
+
"INVALID_VALUE",
|
|
1363
|
+
`Question bank has invalid behaviour maturity: ${maturity}`,
|
|
1364
|
+
`questionBank.behaviourMaturities.${behaviourId}.${maturity}`,
|
|
1365
|
+
maturity,
|
|
1366
|
+
),
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
1370
|
+
warnings.push(
|
|
1371
|
+
createWarning(
|
|
1372
|
+
"EMPTY_QUESTIONS",
|
|
1373
|
+
`No questions for behaviour ${behaviourId} at maturity ${maturity}`,
|
|
1374
|
+
`questionBank.behaviourMaturities.${behaviourId}.${maturity}`,
|
|
1375
|
+
),
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
},
|
|
1379
|
+
);
|
|
1380
|
+
},
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return createValidationResult(errors.length === 0, errors, warnings);
|
|
1385
|
+
}
|