@forwardimpact/pathway 0.1.0 → 0.2.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/app/commands/agent.js +109 -21
- package/app/commands/command-factory.js +3 -3
- package/app/commands/interview.js +14 -7
- package/app/commands/job.js +43 -29
- package/app/commands/progress.js +14 -7
- package/app/commands/serve.js +5 -0
- package/app/commands/stage.js +0 -10
- package/app/commands/track.js +5 -8
- package/app/components/builder.js +111 -27
- package/app/css/components/surfaces.css +16 -0
- package/app/formatters/agent/profile.js +113 -87
- package/app/formatters/agent/skill.js +64 -31
- package/app/formatters/behaviour/dom.js +3 -0
- package/app/formatters/behaviour/microdata.js +106 -0
- package/app/formatters/discipline/dom.js +28 -1
- package/app/formatters/discipline/microdata.js +117 -0
- package/app/formatters/discipline/shared.js +49 -8
- package/app/formatters/driver/dom.js +3 -0
- package/app/formatters/driver/microdata.js +91 -0
- package/app/formatters/grade/dom.js +3 -0
- package/app/formatters/grade/microdata.js +151 -0
- package/app/formatters/index.js +32 -1
- package/app/formatters/interview/shared.js +13 -8
- package/app/formatters/job/description.js +5 -3
- package/app/formatters/json-ld.js +242 -0
- package/app/formatters/microdata-shared.js +184 -0
- package/app/formatters/progress/shared.js +14 -11
- package/app/formatters/skill/dom.js +3 -0
- package/app/formatters/skill/microdata.js +151 -0
- package/app/formatters/stage/dom.js +3 -18
- package/app/formatters/stage/microdata.js +110 -0
- package/app/formatters/stage/shared.js +0 -27
- package/app/formatters/track/dom.js +5 -30
- package/app/formatters/track/markdown.js +2 -25
- package/app/formatters/track/microdata.js +111 -0
- package/app/formatters/track/shared.js +6 -58
- package/app/handout-main.js +26 -12
- package/app/index.html +11 -0
- package/app/lib/card-mappers.js +17 -12
- package/app/lib/job-cache.js +12 -9
- package/app/lib/template-loader.js +66 -0
- package/app/lib/yaml-loader.js +25 -8
- package/app/main.js +8 -4
- package/app/model/agent.js +158 -130
- package/app/model/checklist.js +57 -91
- package/app/model/derivation.js +135 -68
- package/app/model/index-generator.js +1 -7
- package/app/model/job.js +19 -13
- package/app/model/levels.js +20 -12
- package/app/model/loader.js +41 -17
- package/app/model/matching.js +33 -3
- package/app/model/profile.js +38 -45
- package/app/model/schema-validation.js +438 -0
- package/app/model/validation.js +747 -68
- package/app/pages/agent-builder.js +119 -25
- package/app/pages/assessment-results.js +10 -4
- package/app/pages/discipline.js +36 -6
- package/app/pages/driver.js +9 -47
- package/app/pages/interview-builder.js +3 -1
- package/app/pages/interview.js +15 -4
- package/app/pages/job-builder.js +4 -1
- package/app/pages/job.js +15 -4
- package/app/pages/landing.js +10 -10
- package/app/pages/progress-builder.js +3 -1
- package/app/pages/progress.js +72 -21
- package/app/pages/stage.js +3 -126
- package/app/slide-main.js +45 -17
- package/app/slides/index.js +3 -1
- package/app/slides/overview.js +40 -4
- package/app/slides/progress.js +4 -2
- package/bin/pathway.js +18 -64
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
- package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
- package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
- package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
- package/examples/agents/.vscode/settings.json +1 -1
- package/examples/behaviours/outcome_ownership.yaml +1 -2
- package/examples/behaviours/polymathic_knowledge.yaml +1 -2
- package/examples/behaviours/precise_communication.yaml +1 -2
- package/examples/behaviours/relentless_curiosity.yaml +1 -2
- package/examples/behaviours/systems_thinking.yaml +1 -2
- package/examples/capabilities/business.yaml +80 -142
- package/examples/capabilities/delivery.yaml +155 -219
- package/examples/capabilities/people.yaml +2 -34
- package/examples/capabilities/reliability.yaml +161 -80
- package/examples/capabilities/scale.yaml +234 -252
- package/examples/copilot-setup-steps.yaml +25 -0
- package/examples/devcontainer.yaml +21 -0
- package/examples/disciplines/_index.yaml +1 -0
- package/examples/disciplines/data_engineering.yaml +14 -12
- package/examples/disciplines/engineering_management.yaml +63 -0
- package/examples/disciplines/software_engineering.yaml +14 -12
- package/examples/drivers.yaml +1 -4
- package/examples/framework.yaml +1 -2
- package/examples/grades.yaml +1 -3
- package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
- package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
- package/examples/questions/behaviours/precise_communication.yaml +1 -2
- package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
- package/examples/questions/behaviours/systems_thinking.yaml +1 -2
- package/examples/questions/skills/architecture_design.yaml +1 -2
- package/examples/questions/skills/cloud_platforms.yaml +1 -2
- package/examples/questions/skills/code_quality.yaml +1 -2
- package/examples/questions/skills/data_modeling.yaml +1 -2
- package/examples/questions/skills/devops.yaml +1 -2
- package/examples/questions/skills/full_stack_development.yaml +1 -2
- package/examples/questions/skills/sre_practices.yaml +1 -2
- package/examples/questions/skills/stakeholder_management.yaml +1 -2
- package/examples/questions/skills/team_collaboration.yaml +1 -2
- package/examples/questions/skills/technical_writing.yaml +1 -2
- package/examples/self-assessments.yaml +1 -3
- package/examples/stages.yaml +101 -46
- package/examples/tracks/_index.yaml +0 -1
- package/examples/tracks/platform.yaml +8 -13
- package/examples/tracks/sre.yaml +8 -18
- package/examples/vscode-settings.yaml +2 -7
- package/package.json +9 -3
- package/templates/agent.template.md +65 -0
- package/templates/skill.template.md +28 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
- package/examples/tracks/manager.yaml +0 -53
package/app/formatters/index.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Formatter Layer
|
|
3
3
|
*
|
|
4
4
|
* Export all formatters for easy importing.
|
|
5
|
-
* Formatters transform presenter output into specific formats (DOM, markdown)
|
|
5
|
+
* Formatters transform presenter output into specific formats (DOM, markdown, microdata)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Shared utilities
|
|
9
9
|
export * from "./shared.js";
|
|
10
|
+
export * from "./microdata-shared.js";
|
|
10
11
|
|
|
11
12
|
// Job formatters
|
|
12
13
|
export { jobToMarkdown } from "./job/markdown.js";
|
|
@@ -22,10 +23,15 @@ export { progressToDOM } from "./progress/dom.js";
|
|
|
22
23
|
|
|
23
24
|
// Driver formatters
|
|
24
25
|
export { driverToDOM } from "./driver/dom.js";
|
|
26
|
+
export {
|
|
27
|
+
driverListToMicrodata,
|
|
28
|
+
driverToMicrodata,
|
|
29
|
+
} from "./driver/microdata.js";
|
|
25
30
|
|
|
26
31
|
// Skill formatters
|
|
27
32
|
export { skillListToMarkdown, skillToMarkdown } from "./skill/markdown.js";
|
|
28
33
|
export { skillToDOM } from "./skill/dom.js";
|
|
34
|
+
export { skillListToMicrodata, skillToMicrodata } from "./skill/microdata.js";
|
|
29
35
|
|
|
30
36
|
// Behaviour formatters
|
|
31
37
|
export {
|
|
@@ -33,6 +39,10 @@ export {
|
|
|
33
39
|
behaviourToMarkdown,
|
|
34
40
|
} from "./behaviour/markdown.js";
|
|
35
41
|
export { behaviourToDOM } from "./behaviour/dom.js";
|
|
42
|
+
export {
|
|
43
|
+
behaviourListToMicrodata,
|
|
44
|
+
behaviourToMicrodata,
|
|
45
|
+
} from "./behaviour/microdata.js";
|
|
36
46
|
|
|
37
47
|
// Discipline formatters
|
|
38
48
|
export {
|
|
@@ -40,11 +50,32 @@ export {
|
|
|
40
50
|
disciplineToMarkdown,
|
|
41
51
|
} from "./discipline/markdown.js";
|
|
42
52
|
export { disciplineToDOM } from "./discipline/dom.js";
|
|
53
|
+
export {
|
|
54
|
+
disciplineListToMicrodata,
|
|
55
|
+
disciplineToMicrodata,
|
|
56
|
+
} from "./discipline/microdata.js";
|
|
43
57
|
|
|
44
58
|
// Grade formatters
|
|
45
59
|
export { gradeListToMarkdown, gradeToMarkdown } from "./grade/markdown.js";
|
|
46
60
|
export { gradeToDOM } from "./grade/dom.js";
|
|
61
|
+
export { gradeListToMicrodata, gradeToMicrodata } from "./grade/microdata.js";
|
|
47
62
|
|
|
48
63
|
// Track formatters
|
|
49
64
|
export { trackListToMarkdown, trackToMarkdown } from "./track/markdown.js";
|
|
50
65
|
export { trackToDOM } from "./track/dom.js";
|
|
66
|
+
export { trackListToMicrodata, trackToMicrodata } from "./track/microdata.js";
|
|
67
|
+
|
|
68
|
+
// Stage formatters
|
|
69
|
+
export { stageListToMicrodata, stageToMicrodata } from "./stage/microdata.js";
|
|
70
|
+
|
|
71
|
+
// JSON-LD formatters
|
|
72
|
+
export {
|
|
73
|
+
createJsonLdScript,
|
|
74
|
+
skillToJsonLd,
|
|
75
|
+
behaviourToJsonLd,
|
|
76
|
+
disciplineToJsonLd,
|
|
77
|
+
trackToJsonLd,
|
|
78
|
+
gradeToJsonLd,
|
|
79
|
+
driverToJsonLd,
|
|
80
|
+
stageToJsonLd,
|
|
81
|
+
} from "./json-ld.js";
|
|
@@ -116,7 +116,7 @@ export function prepareInterviewDetail({
|
|
|
116
116
|
questions,
|
|
117
117
|
interviewType = "full",
|
|
118
118
|
}) {
|
|
119
|
-
if (!discipline || !grade
|
|
119
|
+
if (!discipline || !grade) return null;
|
|
120
120
|
|
|
121
121
|
const job = getOrCreateJob({
|
|
122
122
|
discipline,
|
|
@@ -171,8 +171,8 @@ export function prepareInterviewDetail({
|
|
|
171
171
|
disciplineId: discipline.id,
|
|
172
172
|
disciplineName: discipline.specialization || discipline.name,
|
|
173
173
|
gradeId: grade.id,
|
|
174
|
-
trackId: track
|
|
175
|
-
trackName: track
|
|
174
|
+
trackId: track?.id || null,
|
|
175
|
+
trackName: track?.name || null,
|
|
176
176
|
sections: allSections,
|
|
177
177
|
totalQuestions,
|
|
178
178
|
expectedDurationMinutes: typeConfig.expectedDurationMinutes,
|
|
@@ -206,7 +206,8 @@ export function prepareInterviewBuilderPreview({
|
|
|
206
206
|
behaviourCount,
|
|
207
207
|
grades,
|
|
208
208
|
}) {
|
|
209
|
-
|
|
209
|
+
// Track is optional (null = generalist)
|
|
210
|
+
if (!discipline || !grade) {
|
|
210
211
|
return {
|
|
211
212
|
isValid: false,
|
|
212
213
|
title: null,
|
|
@@ -224,12 +225,15 @@ export function prepareInterviewBuilderPreview({
|
|
|
224
225
|
});
|
|
225
226
|
|
|
226
227
|
if (!validCombination) {
|
|
228
|
+
const reason = track
|
|
229
|
+
? `The ${track.name} track is not available for ${discipline.specialization}.`
|
|
230
|
+
: `${discipline.specialization} requires a track specialization.`;
|
|
227
231
|
return {
|
|
228
232
|
isValid: false,
|
|
229
233
|
title: null,
|
|
230
234
|
totalSkills: 0,
|
|
231
235
|
totalBehaviours: 0,
|
|
232
|
-
invalidReason:
|
|
236
|
+
invalidReason: reason,
|
|
233
237
|
};
|
|
234
238
|
}
|
|
235
239
|
|
|
@@ -275,7 +279,8 @@ export function prepareAllInterviews({
|
|
|
275
279
|
behaviours,
|
|
276
280
|
questions,
|
|
277
281
|
}) {
|
|
278
|
-
|
|
282
|
+
// Track is optional (null = generalist)
|
|
283
|
+
if (!discipline || !grade) return null;
|
|
279
284
|
|
|
280
285
|
const job = getOrCreateJob({
|
|
281
286
|
discipline,
|
|
@@ -313,8 +318,8 @@ export function prepareAllInterviews({
|
|
|
313
318
|
disciplineId: discipline.id,
|
|
314
319
|
disciplineName: discipline.specialization || discipline.name,
|
|
315
320
|
gradeId: grade.id,
|
|
316
|
-
trackId: track
|
|
317
|
-
trackName: track
|
|
321
|
+
trackId: track?.id || null,
|
|
322
|
+
trackName: track?.name || null,
|
|
318
323
|
interviews: {
|
|
319
324
|
short: {
|
|
320
325
|
...shortInterview,
|
|
@@ -29,7 +29,9 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
29
29
|
// Meta information
|
|
30
30
|
lines.push(`- **Level:** ${grade.id}`);
|
|
31
31
|
lines.push(`- **Experience:** ${grade.typicalExperienceRange}`);
|
|
32
|
-
|
|
32
|
+
if (track) {
|
|
33
|
+
lines.push(`- **Track:** ${track.name}`);
|
|
34
|
+
}
|
|
33
35
|
lines.push("");
|
|
34
36
|
|
|
35
37
|
// Role Summary
|
|
@@ -37,7 +39,7 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
37
39
|
lines.push("");
|
|
38
40
|
|
|
39
41
|
// Build role summary from discipline - use manager version if applicable
|
|
40
|
-
const isManagement =
|
|
42
|
+
const isManagement = discipline.isManagement === true;
|
|
41
43
|
let roleSummary =
|
|
42
44
|
isManagement && discipline.managementRoleSummary
|
|
43
45
|
? discipline.managementRoleSummary
|
|
@@ -50,7 +52,7 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
50
52
|
lines.push("");
|
|
51
53
|
|
|
52
54
|
// Add track context
|
|
53
|
-
if (track
|
|
55
|
+
if (track?.roleContext) {
|
|
54
56
|
lines.push(track.roleContext);
|
|
55
57
|
lines.push("");
|
|
56
58
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-LD structured data generation
|
|
3
|
+
*
|
|
4
|
+
* Generates JSON-LD for entity pages to enable machine-readable data.
|
|
5
|
+
* Aligns with the RDF schema at https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const VOCAB_BASE = "https://schema.forwardimpact.team/rdf/";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a JSON-LD script element
|
|
12
|
+
* @param {Object} data - JSON-LD data object
|
|
13
|
+
* @returns {HTMLScriptElement}
|
|
14
|
+
*/
|
|
15
|
+
export function createJsonLdScript(data) {
|
|
16
|
+
const script = document.createElement("script");
|
|
17
|
+
script.type = "application/ld+json";
|
|
18
|
+
script.textContent = JSON.stringify(data, null, 2);
|
|
19
|
+
return script;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build base JSON-LD context and type
|
|
24
|
+
* @param {string} type - Entity type (without vocab prefix)
|
|
25
|
+
* @param {string} id - Entity ID
|
|
26
|
+
* @returns {Object}
|
|
27
|
+
*/
|
|
28
|
+
function baseJsonLd(type, id) {
|
|
29
|
+
return {
|
|
30
|
+
"@context": VOCAB_BASE,
|
|
31
|
+
"@type": type,
|
|
32
|
+
"@id": `${VOCAB_BASE}${type}/${id}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate JSON-LD for a skill entity
|
|
38
|
+
* @param {Object} skill - Raw skill entity
|
|
39
|
+
* @param {Object} context - Additional context
|
|
40
|
+
* @param {Array} [context.capabilities] - Capability entities
|
|
41
|
+
* @returns {Object}
|
|
42
|
+
*/
|
|
43
|
+
export function skillToJsonLd(skill, { capabilities = [] } = {}) {
|
|
44
|
+
const capability = capabilities.find((c) => c.id === skill.capability);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...baseJsonLd("Skill", skill.id),
|
|
48
|
+
identifier: skill.id,
|
|
49
|
+
name: skill.name,
|
|
50
|
+
description: skill.description,
|
|
51
|
+
capability: skill.capability,
|
|
52
|
+
...(capability && { capabilityName: capability.name }),
|
|
53
|
+
...(skill.isHumanOnly && { isHumanOnly: true }),
|
|
54
|
+
levelDescriptions: Object.entries(skill.levelDescriptions || {}).map(
|
|
55
|
+
([level, description]) => ({
|
|
56
|
+
"@type": "SkillLevelDescription",
|
|
57
|
+
level: `${VOCAB_BASE}${level}`,
|
|
58
|
+
description,
|
|
59
|
+
}),
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate JSON-LD for a behaviour entity
|
|
66
|
+
* @param {Object} behaviour - Raw behaviour entity
|
|
67
|
+
* @returns {Object}
|
|
68
|
+
*/
|
|
69
|
+
export function behaviourToJsonLd(behaviour) {
|
|
70
|
+
return {
|
|
71
|
+
...baseJsonLd("Behaviour", behaviour.id),
|
|
72
|
+
identifier: behaviour.id,
|
|
73
|
+
name: behaviour.name,
|
|
74
|
+
description: behaviour.description,
|
|
75
|
+
maturityDescriptions: Object.entries(
|
|
76
|
+
behaviour.maturityDescriptions || {},
|
|
77
|
+
).map(([maturity, description]) => ({
|
|
78
|
+
"@type": "BehaviourMaturityDescription",
|
|
79
|
+
maturity: `${VOCAB_BASE}${maturity}`,
|
|
80
|
+
description,
|
|
81
|
+
})),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate JSON-LD for a discipline entity
|
|
87
|
+
* @param {Object} discipline - Raw discipline entity
|
|
88
|
+
* @param {Object} context - Additional context
|
|
89
|
+
* @param {Array} [context.skills] - All skills
|
|
90
|
+
* @returns {Object}
|
|
91
|
+
*/
|
|
92
|
+
export function disciplineToJsonLd(discipline, { skills = [] } = {}) {
|
|
93
|
+
const resolveSkillNames = (skillIds) =>
|
|
94
|
+
(skillIds || [])
|
|
95
|
+
.map((id) => {
|
|
96
|
+
const skill = skills.find((s) => s.id === id);
|
|
97
|
+
return skill
|
|
98
|
+
? { "@id": `${VOCAB_BASE}Skill/${id}`, name: skill.name }
|
|
99
|
+
: null;
|
|
100
|
+
})
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...baseJsonLd("Discipline", discipline.id),
|
|
105
|
+
identifier: discipline.id,
|
|
106
|
+
name: discipline.name,
|
|
107
|
+
...(discipline.specialization && {
|
|
108
|
+
specialization: discipline.specialization,
|
|
109
|
+
}),
|
|
110
|
+
description: discipline.description,
|
|
111
|
+
coreSkills: resolveSkillNames(discipline.coreSkills),
|
|
112
|
+
supportingSkills: resolveSkillNames(discipline.supportingSkills),
|
|
113
|
+
broadSkills: resolveSkillNames(discipline.broadSkills),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate JSON-LD for a track entity
|
|
119
|
+
* @param {Object} track - Raw track entity
|
|
120
|
+
* @returns {Object}
|
|
121
|
+
*/
|
|
122
|
+
export function trackToJsonLd(track) {
|
|
123
|
+
return {
|
|
124
|
+
...baseJsonLd("Track", track.id),
|
|
125
|
+
identifier: track.id,
|
|
126
|
+
name: track.name,
|
|
127
|
+
description: track.description,
|
|
128
|
+
...(track.skillModifiers && {
|
|
129
|
+
skillModifiers: Object.entries(track.skillModifiers).map(
|
|
130
|
+
([capability, modifier]) => ({
|
|
131
|
+
"@type": "SkillModifier",
|
|
132
|
+
capability,
|
|
133
|
+
modifier,
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
}),
|
|
137
|
+
...(track.behaviourModifiers && {
|
|
138
|
+
behaviourModifiers: Object.entries(track.behaviourModifiers).map(
|
|
139
|
+
([behaviour, modifier]) => ({
|
|
140
|
+
"@type": "BehaviourModifier",
|
|
141
|
+
behaviour,
|
|
142
|
+
modifier,
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate JSON-LD for a grade entity
|
|
151
|
+
* @param {Object} grade - Raw grade entity
|
|
152
|
+
* @returns {Object}
|
|
153
|
+
*/
|
|
154
|
+
export function gradeToJsonLd(grade) {
|
|
155
|
+
return {
|
|
156
|
+
...baseJsonLd("Grade", grade.id),
|
|
157
|
+
identifier: grade.id,
|
|
158
|
+
name: grade.displayName || grade.name,
|
|
159
|
+
...(grade.ordinalRank && { ordinalRank: grade.ordinalRank }),
|
|
160
|
+
...(grade.typicalExperienceRange && {
|
|
161
|
+
typicalExperienceRange: grade.typicalExperienceRange,
|
|
162
|
+
}),
|
|
163
|
+
...(grade.baseSkillLevels && {
|
|
164
|
+
baseSkillLevels: {
|
|
165
|
+
"@type": "BaseSkillLevels",
|
|
166
|
+
primary: `${VOCAB_BASE}${grade.baseSkillLevels.primary}`,
|
|
167
|
+
secondary: `${VOCAB_BASE}${grade.baseSkillLevels.secondary}`,
|
|
168
|
+
broad: `${VOCAB_BASE}${grade.baseSkillLevels.broad}`,
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
...(grade.baseBehaviourMaturity && {
|
|
172
|
+
baseBehaviourMaturity: `${VOCAB_BASE}${grade.baseBehaviourMaturity}`,
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Generate JSON-LD for a driver entity
|
|
179
|
+
* @param {Object} driver - Raw driver entity
|
|
180
|
+
* @param {Object} context - Additional context
|
|
181
|
+
* @param {Array} [context.skills] - All skills
|
|
182
|
+
* @param {Array} [context.behaviours] - All behaviours
|
|
183
|
+
* @returns {Object}
|
|
184
|
+
*/
|
|
185
|
+
export function driverToJsonLd(driver, { skills = [], behaviours = [] } = {}) {
|
|
186
|
+
const resolveSkills = (skillIds) =>
|
|
187
|
+
(skillIds || [])
|
|
188
|
+
.map((id) => {
|
|
189
|
+
const skill = skills.find((s) => s.id === id);
|
|
190
|
+
return skill
|
|
191
|
+
? { "@id": `${VOCAB_BASE}Skill/${id}`, name: skill.name }
|
|
192
|
+
: null;
|
|
193
|
+
})
|
|
194
|
+
.filter(Boolean);
|
|
195
|
+
|
|
196
|
+
const resolveBehaviours = (behaviourIds) =>
|
|
197
|
+
(behaviourIds || [])
|
|
198
|
+
.map((id) => {
|
|
199
|
+
const behaviour = behaviours.find((b) => b.id === id);
|
|
200
|
+
return behaviour
|
|
201
|
+
? { "@id": `${VOCAB_BASE}Behaviour/${id}`, name: behaviour.name }
|
|
202
|
+
: null;
|
|
203
|
+
})
|
|
204
|
+
.filter(Boolean);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
...baseJsonLd("Driver", driver.id),
|
|
208
|
+
identifier: driver.id,
|
|
209
|
+
name: driver.name,
|
|
210
|
+
description: driver.description,
|
|
211
|
+
...(driver.contributingSkills?.length > 0 && {
|
|
212
|
+
contributingSkills: resolveSkills(driver.contributingSkills),
|
|
213
|
+
}),
|
|
214
|
+
...(driver.contributingBehaviours?.length > 0 && {
|
|
215
|
+
contributingBehaviours: resolveBehaviours(driver.contributingBehaviours),
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generate JSON-LD for a stage entity
|
|
222
|
+
* @param {Object} stage - Raw stage entity
|
|
223
|
+
* @returns {Object}
|
|
224
|
+
*/
|
|
225
|
+
export function stageToJsonLd(stage) {
|
|
226
|
+
return {
|
|
227
|
+
...baseJsonLd("Stage", stage.id),
|
|
228
|
+
identifier: stage.id,
|
|
229
|
+
name: stage.name,
|
|
230
|
+
description: stage.description,
|
|
231
|
+
...(stage.emoji && { emoji: stage.emoji }),
|
|
232
|
+
...(stage.tools?.length > 0 && { tools: stage.tools }),
|
|
233
|
+
...(stage.constraints?.length > 0 && { constraints: stage.constraints }),
|
|
234
|
+
...(stage.handoffs && {
|
|
235
|
+
handoffs: Object.entries(stage.handoffs).map(([targetStage, config]) => ({
|
|
236
|
+
"@type": "StageHandoff",
|
|
237
|
+
targetStage: `${VOCAB_BASE}Stage/${targetStage}`,
|
|
238
|
+
...(config.prompt && { prompt: config.prompt }),
|
|
239
|
+
})),
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared microdata HTML utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for generating clean, class-less HTML with microdata attributes
|
|
5
|
+
* aligned with the RDF schema at https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const VOCAB_BASE = "https://schema.forwardimpact.team/rdf/";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create an opening tag with microdata attributes
|
|
12
|
+
* @param {string} tag - HTML tag name
|
|
13
|
+
* @param {Object} [attrs] - Optional attributes
|
|
14
|
+
* @param {string} [attrs.itemtype] - Microdata type (without vocab prefix)
|
|
15
|
+
* @param {string} [attrs.itemprop] - Microdata property name
|
|
16
|
+
* @param {string} [attrs.itemid] - Microdata item ID
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function openTag(tag, attrs = {}) {
|
|
20
|
+
const parts = [tag];
|
|
21
|
+
|
|
22
|
+
if (attrs.itemtype) {
|
|
23
|
+
parts.push(`itemscope`);
|
|
24
|
+
parts.push(`itemtype="${VOCAB_BASE}${attrs.itemtype}"`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (attrs.itemprop) {
|
|
28
|
+
parts.push(`itemprop="${attrs.itemprop}"`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (attrs.itemid) {
|
|
32
|
+
parts.push(`itemid="${attrs.itemid}"`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `<${parts.join(" ")}>`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a self-closing meta element with microdata
|
|
40
|
+
* @param {string} itemprop - Property name
|
|
41
|
+
* @param {string} content - Content value
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function metaTag(itemprop, content) {
|
|
45
|
+
return `<meta itemprop="${itemprop}" content="${escapeAttr(content)}">`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a link element with microdata
|
|
50
|
+
* @param {string} itemprop - Property name
|
|
51
|
+
* @param {string} href - Link target
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function linkTag(itemprop, href) {
|
|
55
|
+
return `<link itemprop="${itemprop}" href="${escapeAttr(href)}">`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wrap content in an element with itemprop
|
|
60
|
+
* @param {string} tag - HTML tag name
|
|
61
|
+
* @param {string} itemprop - Property name
|
|
62
|
+
* @param {string} content - Content to wrap
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function prop(tag, itemprop, content) {
|
|
66
|
+
return `<${tag} itemprop="${itemprop}">${escapeHtml(content)}</${tag}>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Wrap raw HTML content in an element with itemprop (no escaping)
|
|
71
|
+
* @param {string} tag - HTML tag name
|
|
72
|
+
* @param {string} itemprop - Property name
|
|
73
|
+
* @param {string} html - HTML content to wrap
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function propRaw(tag, itemprop, html) {
|
|
77
|
+
return `<${tag} itemprop="${itemprop}">${html}</${tag}>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a section with optional heading
|
|
82
|
+
* @param {string} heading - Section heading text
|
|
83
|
+
* @param {string} content - Section content
|
|
84
|
+
* @param {number} [level=2] - Heading level (2-6)
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function section(heading, content, level = 2) {
|
|
88
|
+
const hTag = `h${Math.min(Math.max(level, 1), 6)}`;
|
|
89
|
+
return `<section>
|
|
90
|
+
<${hTag}>${escapeHtml(heading)}</${hTag}>
|
|
91
|
+
${content}
|
|
92
|
+
</section>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create an unordered list
|
|
97
|
+
* @param {string[]} items - List items (already HTML)
|
|
98
|
+
* @param {string} [itemprop] - Optional property for list items
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
export function ul(items, itemprop) {
|
|
102
|
+
if (!items.length) return "";
|
|
103
|
+
const lis = items
|
|
104
|
+
.map((item) =>
|
|
105
|
+
itemprop ? `<li itemprop="${itemprop}">${item}</li>` : `<li>${item}</li>`,
|
|
106
|
+
)
|
|
107
|
+
.join("\n");
|
|
108
|
+
return `<ul>\n${lis}\n</ul>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create a definition list from key-value pairs
|
|
113
|
+
* @param {Array<{term: string, definition: string, itemprop?: string}>} pairs
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
export function dl(pairs) {
|
|
117
|
+
if (!pairs.length) return "";
|
|
118
|
+
const content = pairs
|
|
119
|
+
.map(({ term, definition, itemprop }) => {
|
|
120
|
+
const dd = itemprop
|
|
121
|
+
? `<dd itemprop="${itemprop}">${escapeHtml(definition)}</dd>`
|
|
122
|
+
: `<dd>${escapeHtml(definition)}</dd>`;
|
|
123
|
+
return `<dt>${escapeHtml(term)}</dt>\n${dd}`;
|
|
124
|
+
})
|
|
125
|
+
.join("\n");
|
|
126
|
+
return `<dl>\n${content}\n</dl>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Escape HTML special characters
|
|
131
|
+
* @param {string} str
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
export function escapeHtml(str) {
|
|
135
|
+
if (str == null) return "";
|
|
136
|
+
return String(str)
|
|
137
|
+
.replace(/&/g, "&")
|
|
138
|
+
.replace(/</g, "<")
|
|
139
|
+
.replace(/>/g, ">");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Escape attribute value
|
|
144
|
+
* @param {string} str
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
export function escapeAttr(str) {
|
|
148
|
+
if (str == null) return "";
|
|
149
|
+
return String(str)
|
|
150
|
+
.replace(/&/g, "&")
|
|
151
|
+
.replace(/"/g, """)
|
|
152
|
+
.replace(/</g, "<")
|
|
153
|
+
.replace(/>/g, ">");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Format level name for display (capitalize, replace underscores)
|
|
158
|
+
* @param {string} level
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
export function formatLevelName(level) {
|
|
162
|
+
if (!level) return "";
|
|
163
|
+
return level.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generate a full microdata HTML document
|
|
168
|
+
* @param {string} title - Document title
|
|
169
|
+
* @param {string} body - Body content
|
|
170
|
+
* @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
export function htmlDocument(title, body) {
|
|
173
|
+
return `<!DOCTYPE html>
|
|
174
|
+
<html lang="en">
|
|
175
|
+
<head>
|
|
176
|
+
<meta charset="UTF-8">
|
|
177
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
178
|
+
<title>${escapeHtml(title)}</title>
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
${body}
|
|
182
|
+
</body>
|
|
183
|
+
</html>`;
|
|
184
|
+
}
|
|
@@ -67,7 +67,7 @@ export function prepareCurrentJob({
|
|
|
67
67
|
behaviours,
|
|
68
68
|
capabilities,
|
|
69
69
|
}) {
|
|
70
|
-
if (!discipline || !grade
|
|
70
|
+
if (!discipline || !grade) return null;
|
|
71
71
|
|
|
72
72
|
const job = getOrCreateJob({
|
|
73
73
|
discipline,
|
|
@@ -117,7 +117,8 @@ export function prepareCareerProgressPreview({
|
|
|
117
117
|
grades,
|
|
118
118
|
tracks,
|
|
119
119
|
}) {
|
|
120
|
-
|
|
120
|
+
// Track is optional (null = generalist)
|
|
121
|
+
if (!discipline || !grade) {
|
|
121
122
|
return {
|
|
122
123
|
isValid: false,
|
|
123
124
|
title: null,
|
|
@@ -135,12 +136,13 @@ export function prepareCareerProgressPreview({
|
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
if (!validCombination) {
|
|
139
|
+
const reason = track
|
|
140
|
+
? `The ${track.name} track is not available for ${discipline.specialization}.`
|
|
141
|
+
: `${discipline.specialization} requires a track specialization.`;
|
|
138
142
|
return {
|
|
139
143
|
isValid: false,
|
|
140
144
|
title: null,
|
|
141
|
-
invalidReason:
|
|
142
|
-
? `The ${track.name} track is only available for certain disciplines.`
|
|
143
|
-
: "This combination is not valid.",
|
|
145
|
+
invalidReason: reason,
|
|
144
146
|
nextGrade: null,
|
|
145
147
|
validTracks: [],
|
|
146
148
|
};
|
|
@@ -149,10 +151,10 @@ export function prepareCareerProgressPreview({
|
|
|
149
151
|
const title = generateJobTitle(discipline, grade, track);
|
|
150
152
|
const nextGrade = getNextGrade(grade, grades);
|
|
151
153
|
|
|
152
|
-
// Find other valid tracks for comparison
|
|
154
|
+
// Find other valid tracks for comparison (exclude current track if any)
|
|
153
155
|
const validTracks = tracks.filter(
|
|
154
156
|
(t) =>
|
|
155
|
-
t.id !== track.id &&
|
|
157
|
+
(!track || t.id !== track.id) &&
|
|
156
158
|
isValidJobCombination({ discipline, grade, track: t, grades }),
|
|
157
159
|
);
|
|
158
160
|
|
|
@@ -203,8 +205,9 @@ export function prepareProgressDetail({
|
|
|
203
205
|
behaviours,
|
|
204
206
|
capabilities,
|
|
205
207
|
}) {
|
|
206
|
-
|
|
207
|
-
if (!
|
|
208
|
+
// Track is optional (null = generalist)
|
|
209
|
+
if (!fromDiscipline || !fromGrade) return null;
|
|
210
|
+
if (!toDiscipline || !toGrade) return null;
|
|
208
211
|
|
|
209
212
|
const fromJob = getOrCreateJob({
|
|
210
213
|
discipline: fromDiscipline,
|
|
@@ -263,12 +266,12 @@ export function prepareProgressDetail({
|
|
|
263
266
|
fromJob: {
|
|
264
267
|
disciplineId: fromDiscipline.id,
|
|
265
268
|
gradeId: fromGrade.id,
|
|
266
|
-
trackId: fromTrack
|
|
269
|
+
trackId: fromTrack?.id || null,
|
|
267
270
|
},
|
|
268
271
|
toJob: {
|
|
269
272
|
disciplineId: toDiscipline.id,
|
|
270
273
|
gradeId: toGrade.id,
|
|
271
|
-
trackId: toTrack
|
|
274
|
+
trackId: toTrack?.id || null,
|
|
272
275
|
},
|
|
273
276
|
skillChanges,
|
|
274
277
|
behaviourChanges,
|
|
@@ -20,6 +20,7 @@ import { createBackLink } from "../../components/nav.js";
|
|
|
20
20
|
import { createLevelCell } from "../../components/detail.js";
|
|
21
21
|
import { SKILL_LEVEL_ORDER } from "../../model/levels.js";
|
|
22
22
|
import { prepareSkillDetail, formatCapability } from "./shared.js";
|
|
23
|
+
import { createJsonLdScript, skillToJsonLd } from "../json-ld.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Format skill detail as DOM elements
|
|
@@ -44,6 +45,8 @@ export function skillToDOM(
|
|
|
44
45
|
});
|
|
45
46
|
return div(
|
|
46
47
|
{ className: "detail-page skill-detail" },
|
|
48
|
+
// JSON-LD structured data
|
|
49
|
+
createJsonLdScript(skillToJsonLd(skill, { capabilities })),
|
|
47
50
|
// Header
|
|
48
51
|
div(
|
|
49
52
|
{ className: "page-header" },
|