@forwardimpact/pathway 0.1.0 → 0.3.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 +119 -31
- package/app/commands/command-factory.js +3 -3
- package/app/commands/interview.js +14 -7
- package/app/commands/job.js +52 -33
- 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 +117 -30
- package/app/css/components/surfaces.css +16 -0
- package/app/formatters/agent/profile.js +30 -115
- package/app/formatters/agent/skill.js +23 -44
- 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 +5 -4
- 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 +70 -81
- package/app/formatters/job/dom.js +40 -113
- package/app/formatters/job/markdown.js +17 -13
- 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/shared.js +7 -2
- 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/handout.html +7 -0
- package/app/index.html +11 -0
- package/app/lib/card-mappers.js +17 -12
- package/app/lib/form-controls.js +64 -1
- package/app/lib/job-cache.js +12 -9
- package/app/lib/render.js +8 -1
- package/app/lib/template-loader.js +75 -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 +125 -28
- 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 +43 -8
- package/app/pages/landing.js +10 -10
- package/app/pages/progress-builder.js +3 -1
- package/app/pages/progress.js +78 -26
- package/app/pages/self-assessment.js +3 -3
- 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/app/slides.html +7 -0
- package/bin/pathway.js +28 -75
- 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 +14 -15
- 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/job.template.md +47 -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
|
@@ -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,
|
package/app/formatters/shared.js
CHANGED
|
@@ -36,7 +36,7 @@ export function tableToMarkdown(headers, rows) {
|
|
|
36
36
|
export function objectToMarkdownList(obj, indent = 0) {
|
|
37
37
|
const prefix = " ".repeat(indent);
|
|
38
38
|
return Object.entries(obj)
|
|
39
|
-
.map(([key, value]) => `${prefix}- **${key}**: ${value}`)
|
|
39
|
+
.map(([key, value]) => `${prefix}- **${capitalize(key)}**: ${value}`)
|
|
40
40
|
.join("\n");
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -51,12 +51,17 @@ export function formatPercent(value) {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Capitalize first letter of each word
|
|
54
|
+
* Handles both snake_case and camelCase
|
|
54
55
|
* @param {string} str
|
|
55
56
|
* @returns {string}
|
|
56
57
|
*/
|
|
57
58
|
export function capitalize(str) {
|
|
58
59
|
if (!str) return "";
|
|
59
|
-
|
|
60
|
+
// Insert space before uppercase letters (for camelCase), then handle snake_case
|
|
61
|
+
return str
|
|
62
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
63
|
+
.replace(/_/g, " ")
|
|
64
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
/**
|
|
@@ -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" },
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill formatting for microdata HTML output
|
|
3
|
+
*
|
|
4
|
+
* Generates clean, class-less HTML with microdata aligned with capability.schema.json
|
|
5
|
+
* RDF vocab: https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
openTag,
|
|
10
|
+
prop,
|
|
11
|
+
propRaw,
|
|
12
|
+
metaTag,
|
|
13
|
+
section,
|
|
14
|
+
dl,
|
|
15
|
+
ul,
|
|
16
|
+
escapeHtml,
|
|
17
|
+
formatLevelName,
|
|
18
|
+
htmlDocument,
|
|
19
|
+
} from "../microdata-shared.js";
|
|
20
|
+
import { prepareSkillsList, prepareSkillDetail } from "./shared.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format skill list as microdata HTML
|
|
24
|
+
* @param {Array} skills - Raw skill entities
|
|
25
|
+
* @param {Array} capabilities - Capability entities
|
|
26
|
+
* @returns {string} HTML with microdata
|
|
27
|
+
*/
|
|
28
|
+
export function skillListToMicrodata(skills, capabilities) {
|
|
29
|
+
const { groups, groupOrder } = prepareSkillsList(skills, capabilities);
|
|
30
|
+
|
|
31
|
+
const content = groupOrder
|
|
32
|
+
.map((capability) => {
|
|
33
|
+
const capabilitySkills = groups[capability];
|
|
34
|
+
const skillItems = capabilitySkills
|
|
35
|
+
.map(
|
|
36
|
+
(
|
|
37
|
+
skill,
|
|
38
|
+
) => `${openTag("article", { itemtype: "Skill", itemid: `#${skill.id}` })}
|
|
39
|
+
${prop("h3", "name", skill.name)}
|
|
40
|
+
${prop("p", "description", skill.truncatedDescription)}
|
|
41
|
+
${metaTag("capability", capability)}
|
|
42
|
+
</article>`,
|
|
43
|
+
)
|
|
44
|
+
.join("\n");
|
|
45
|
+
|
|
46
|
+
return section(formatLevelName(capability), skillItems, 2);
|
|
47
|
+
})
|
|
48
|
+
.join("\n");
|
|
49
|
+
|
|
50
|
+
return htmlDocument(
|
|
51
|
+
"Skills",
|
|
52
|
+
`<main>
|
|
53
|
+
<h1>Skills</h1>
|
|
54
|
+
${content}
|
|
55
|
+
</main>`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format skill detail as microdata HTML
|
|
61
|
+
* @param {Object} skill - Raw skill entity
|
|
62
|
+
* @param {Object} context - Additional context
|
|
63
|
+
* @param {Array} context.disciplines - All disciplines
|
|
64
|
+
* @param {Array} context.tracks - All tracks
|
|
65
|
+
* @param {Array} context.drivers - All drivers
|
|
66
|
+
* @param {Array} context.capabilities - Capability entities
|
|
67
|
+
* @returns {string} HTML with microdata
|
|
68
|
+
*/
|
|
69
|
+
export function skillToMicrodata(
|
|
70
|
+
skill,
|
|
71
|
+
{ disciplines, tracks, drivers, capabilities },
|
|
72
|
+
) {
|
|
73
|
+
const view = prepareSkillDetail(skill, {
|
|
74
|
+
disciplines,
|
|
75
|
+
tracks,
|
|
76
|
+
drivers,
|
|
77
|
+
capabilities,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!view) return "";
|
|
81
|
+
|
|
82
|
+
const sections = [];
|
|
83
|
+
|
|
84
|
+
// Human-only badge
|
|
85
|
+
if (view.isHumanOnly) {
|
|
86
|
+
sections.push(`<p><strong>Human-Only</strong> — Requires interpersonal skills; excluded from agents</p>
|
|
87
|
+
${metaTag("isHumanOnly", "true")}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Level descriptions - uses LevelDescriptions itemtype
|
|
91
|
+
const levelPairs = Object.entries(view.levelDescriptions).map(
|
|
92
|
+
([level, desc]) => ({
|
|
93
|
+
term: formatLevelName(level),
|
|
94
|
+
definition: desc,
|
|
95
|
+
itemprop: `${level}Description`,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
sections.push(
|
|
99
|
+
section(
|
|
100
|
+
"Level Descriptions",
|
|
101
|
+
`${openTag("div", { itemtype: "LevelDescriptions", itemprop: "levelDescriptions" })}
|
|
102
|
+
${dl(levelPairs)}
|
|
103
|
+
</div>`,
|
|
104
|
+
2,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Related disciplines
|
|
109
|
+
if (view.relatedDisciplines.length > 0) {
|
|
110
|
+
const disciplineItems = view.relatedDisciplines.map(
|
|
111
|
+
(d) =>
|
|
112
|
+
`<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a> (${escapeHtml(d.skillType)})`,
|
|
113
|
+
);
|
|
114
|
+
sections.push(section("Used in Disciplines", ul(disciplineItems), 2));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Related tracks with modifiers
|
|
118
|
+
if (view.relatedTracks.length > 0) {
|
|
119
|
+
const trackItems = view.relatedTracks.map((t) => {
|
|
120
|
+
const modifierStr = t.modifier > 0 ? `+${t.modifier}` : `${t.modifier}`;
|
|
121
|
+
return `<a href="#${escapeHtml(t.id)}">${escapeHtml(t.name)}</a>: ${modifierStr}`;
|
|
122
|
+
});
|
|
123
|
+
sections.push(section("Modified by Tracks", ul(trackItems), 2));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Related drivers
|
|
127
|
+
if (view.relatedDrivers.length > 0) {
|
|
128
|
+
const driverItems = view.relatedDrivers.map(
|
|
129
|
+
(d) => `<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a>`,
|
|
130
|
+
);
|
|
131
|
+
sections.push(section("Linked to Drivers", ul(driverItems), 2));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const body = `<main>
|
|
135
|
+
${openTag("article", { itemtype: "Skill", itemid: `#${view.id}` })}
|
|
136
|
+
${prop("h1", "name", view.name)}
|
|
137
|
+
${metaTag("id", view.id)}
|
|
138
|
+
${metaTag("capability", view.capability)}
|
|
139
|
+
${propRaw(
|
|
140
|
+
"div",
|
|
141
|
+
"human",
|
|
142
|
+
`${openTag("div", { itemtype: "SkillHumanSection" })}
|
|
143
|
+
${prop("p", "description", view.description)}
|
|
144
|
+
${sections.join("\n")}
|
|
145
|
+
</div>`,
|
|
146
|
+
)}
|
|
147
|
+
</article>
|
|
148
|
+
</main>`;
|
|
149
|
+
|
|
150
|
+
return htmlDocument(view.name, body);
|
|
151
|
+
}
|