@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,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discipline formatting for microdata HTML output
|
|
3
|
+
*
|
|
4
|
+
* Generates clean, class-less HTML with microdata aligned with discipline.schema.json
|
|
5
|
+
* RDF vocab: https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
openTag,
|
|
10
|
+
prop,
|
|
11
|
+
metaTag,
|
|
12
|
+
linkTag,
|
|
13
|
+
section,
|
|
14
|
+
ul,
|
|
15
|
+
escapeHtml,
|
|
16
|
+
htmlDocument,
|
|
17
|
+
} from "../microdata-shared.js";
|
|
18
|
+
import { prepareDisciplinesList, prepareDisciplineDetail } from "./shared.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format discipline list as microdata HTML
|
|
22
|
+
* @param {Array} disciplines - Raw discipline entities
|
|
23
|
+
* @returns {string} HTML with microdata
|
|
24
|
+
*/
|
|
25
|
+
export function disciplineListToMicrodata(disciplines) {
|
|
26
|
+
const { items } = prepareDisciplinesList(disciplines);
|
|
27
|
+
|
|
28
|
+
const content = items
|
|
29
|
+
.map(
|
|
30
|
+
(
|
|
31
|
+
d,
|
|
32
|
+
) => `${openTag("article", { itemtype: "Discipline", itemid: `#${d.id}` })}
|
|
33
|
+
${prop("h2", "specialization", d.name)}
|
|
34
|
+
<p>Core: ${d.coreSkillsCount} | Supporting: ${d.supportingSkillsCount} | Broad: ${d.broadSkillsCount}</p>
|
|
35
|
+
</article>`,
|
|
36
|
+
)
|
|
37
|
+
.join("\n");
|
|
38
|
+
|
|
39
|
+
return htmlDocument(
|
|
40
|
+
"Disciplines",
|
|
41
|
+
`<main>
|
|
42
|
+
<h1>Disciplines</h1>
|
|
43
|
+
${content}
|
|
44
|
+
</main>`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format discipline detail as microdata HTML
|
|
50
|
+
* @param {Object} discipline - Raw discipline entity
|
|
51
|
+
* @param {Object} context - Additional context
|
|
52
|
+
* @param {Array} context.skills - All skills
|
|
53
|
+
* @param {Array} context.behaviours - All behaviours
|
|
54
|
+
* @param {boolean} [context.showBehaviourModifiers=true] - Whether to show behaviour modifiers section
|
|
55
|
+
* @returns {string} HTML with microdata
|
|
56
|
+
*/
|
|
57
|
+
export function disciplineToMicrodata(
|
|
58
|
+
discipline,
|
|
59
|
+
{ skills, behaviours, showBehaviourModifiers = true } = {},
|
|
60
|
+
) {
|
|
61
|
+
const view = prepareDisciplineDetail(discipline, { skills, behaviours });
|
|
62
|
+
|
|
63
|
+
if (!view) return "";
|
|
64
|
+
|
|
65
|
+
const sections = [];
|
|
66
|
+
|
|
67
|
+
// Core skills - using coreSkills property
|
|
68
|
+
if (view.coreSkills.length > 0) {
|
|
69
|
+
const skillLinks = view.coreSkills.map(
|
|
70
|
+
(s) =>
|
|
71
|
+
`${openTag("span", { itemprop: "coreSkills" })}<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a></span>`,
|
|
72
|
+
);
|
|
73
|
+
sections.push(section("Core Skills", ul(skillLinks), 2));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Supporting skills - using supportingSkills property
|
|
77
|
+
if (view.supportingSkills.length > 0) {
|
|
78
|
+
const skillLinks = view.supportingSkills.map(
|
|
79
|
+
(s) =>
|
|
80
|
+
`${openTag("span", { itemprop: "supportingSkills" })}<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a></span>`,
|
|
81
|
+
);
|
|
82
|
+
sections.push(section("Supporting Skills", ul(skillLinks), 2));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Broad skills - using broadSkills property
|
|
86
|
+
if (view.broadSkills.length > 0) {
|
|
87
|
+
const skillLinks = view.broadSkills.map(
|
|
88
|
+
(s) =>
|
|
89
|
+
`${openTag("span", { itemprop: "broadSkills" })}<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a></span>`,
|
|
90
|
+
);
|
|
91
|
+
sections.push(section("Broad Skills", ul(skillLinks), 2));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Behaviour modifiers - using BehaviourModifier itemtype
|
|
95
|
+
if (showBehaviourModifiers && view.behaviourModifiers.length > 0) {
|
|
96
|
+
const modifierItems = view.behaviourModifiers.map((b) => {
|
|
97
|
+
const modifierStr = b.modifier > 0 ? `+${b.modifier}` : `${b.modifier}`;
|
|
98
|
+
return `${openTag("span", { itemtype: "BehaviourModifier", itemprop: "behaviourModifiers" })}
|
|
99
|
+
${linkTag("targetBehaviour", `#${b.id}`)}
|
|
100
|
+
<a href="#${escapeHtml(b.id)}">${escapeHtml(b.name)}</a>: ${openTag("span", { itemprop: "modifierValue" })}${modifierStr}</span>
|
|
101
|
+
</span>`;
|
|
102
|
+
});
|
|
103
|
+
sections.push(section("Behaviour Modifiers", ul(modifierItems), 2));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const body = `<main>
|
|
107
|
+
${openTag("article", { itemtype: "Discipline", itemid: `#${view.id}` })}
|
|
108
|
+
${prop("h1", "specialization", view.name)}
|
|
109
|
+
${metaTag("id", view.id)}
|
|
110
|
+
${discipline.roleTitle ? prop("p", "roleTitle", discipline.roleTitle) : ""}
|
|
111
|
+
${prop("p", "description", view.description)}
|
|
112
|
+
${sections.join("\n")}
|
|
113
|
+
</article>
|
|
114
|
+
</main>`;
|
|
115
|
+
|
|
116
|
+
return htmlDocument(view.name, body);
|
|
117
|
+
}
|
|
@@ -35,16 +35,18 @@ export function getDisciplineDisplayName(discipline) {
|
|
|
35
35
|
* @property {number} coreSkillsCount
|
|
36
36
|
* @property {number} supportingSkillsCount
|
|
37
37
|
* @property {number} broadSkillsCount
|
|
38
|
+
* @property {boolean} isProfessional
|
|
39
|
+
* @property {boolean} isManagement
|
|
38
40
|
*/
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
|
-
* Transform
|
|
42
|
-
* @param {
|
|
43
|
-
* @param {number}
|
|
44
|
-
* @returns {
|
|
43
|
+
* Transform a single discipline to list item format
|
|
44
|
+
* @param {Object} discipline - Raw discipline entity
|
|
45
|
+
* @param {number} descriptionLimit - Maximum description length
|
|
46
|
+
* @returns {DisciplineListItem}
|
|
45
47
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
function disciplineToListItem(discipline, descriptionLimit) {
|
|
49
|
+
return {
|
|
48
50
|
id: discipline.id,
|
|
49
51
|
name: getDisciplineDisplayName(discipline),
|
|
50
52
|
description: discipline.description,
|
|
@@ -52,9 +54,48 @@ export function prepareDisciplinesList(disciplines, descriptionLimit = 120) {
|
|
|
52
54
|
coreSkillsCount: discipline.coreSkills?.length || 0,
|
|
53
55
|
supportingSkillsCount: discipline.supportingSkills?.length || 0,
|
|
54
56
|
broadSkillsCount: discipline.broadSkills?.length || 0,
|
|
55
|
-
|
|
57
|
+
isProfessional: discipline.isProfessional || false,
|
|
58
|
+
isManagement: discipline.isManagement || false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Transform disciplines for list view, grouped by type (professional/management)
|
|
64
|
+
* @param {Array} disciplines - Raw discipline entities
|
|
65
|
+
* @param {number} [descriptionLimit=120] - Maximum description length
|
|
66
|
+
* @returns {{ items: DisciplineListItem[], groups: Object<string, DisciplineListItem[]>, groupOrder: string[] }}
|
|
67
|
+
*/
|
|
68
|
+
export function prepareDisciplinesList(disciplines, descriptionLimit = 120) {
|
|
69
|
+
const professional = [];
|
|
70
|
+
const management = [];
|
|
71
|
+
|
|
72
|
+
for (const discipline of disciplines) {
|
|
73
|
+
const item = disciplineToListItem(discipline, descriptionLimit);
|
|
74
|
+
if (discipline.isManagement) {
|
|
75
|
+
management.push(item);
|
|
76
|
+
} else {
|
|
77
|
+
// Default to professional if not explicitly management
|
|
78
|
+
professional.push(item);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Groups in display order: professional first, then management
|
|
83
|
+
const groups = {};
|
|
84
|
+
const groupOrder = [];
|
|
85
|
+
|
|
86
|
+
if (professional.length > 0) {
|
|
87
|
+
groups.professional = professional;
|
|
88
|
+
groupOrder.push("professional");
|
|
89
|
+
}
|
|
90
|
+
if (management.length > 0) {
|
|
91
|
+
groups.management = management;
|
|
92
|
+
groupOrder.push("management");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// items maintains backward compatibility (professional first, then management)
|
|
96
|
+
const items = [...professional, ...management];
|
|
56
97
|
|
|
57
|
-
return { items };
|
|
98
|
+
return { items, groups, groupOrder };
|
|
58
99
|
}
|
|
59
100
|
|
|
60
101
|
/**
|
|
@@ -6,6 +6,7 @@ import { div, heading1, heading2, p, a, span } from "../../lib/render.js";
|
|
|
6
6
|
import { createBackLink } from "../../components/nav.js";
|
|
7
7
|
import { prepareDriverDetail } from "./shared.js";
|
|
8
8
|
import { getConceptEmoji } from "../../model/levels.js";
|
|
9
|
+
import { createJsonLdScript, driverToJsonLd } from "../json-ld.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Format driver detail as DOM elements
|
|
@@ -25,6 +26,8 @@ export function driverToDOM(
|
|
|
25
26
|
const emoji = framework ? getConceptEmoji(framework, "driver") : "🎯";
|
|
26
27
|
return div(
|
|
27
28
|
{ className: "detail-page driver-detail" },
|
|
29
|
+
// JSON-LD structured data
|
|
30
|
+
createJsonLdScript(driverToJsonLd(driver, { skills, behaviours })),
|
|
28
31
|
// Header
|
|
29
32
|
div(
|
|
30
33
|
{ className: "page-header" },
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver formatting for microdata HTML output
|
|
3
|
+
*
|
|
4
|
+
* Generates clean, class-less HTML with microdata aligned with drivers.schema.json
|
|
5
|
+
* RDF vocab: https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
openTag,
|
|
10
|
+
prop,
|
|
11
|
+
metaTag,
|
|
12
|
+
section,
|
|
13
|
+
ul,
|
|
14
|
+
escapeHtml,
|
|
15
|
+
htmlDocument,
|
|
16
|
+
} from "../microdata-shared.js";
|
|
17
|
+
import { prepareDriversList, prepareDriverDetail } from "./shared.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format driver list as microdata HTML
|
|
21
|
+
* @param {Array} drivers - Raw driver entities
|
|
22
|
+
* @returns {string} HTML with microdata
|
|
23
|
+
*/
|
|
24
|
+
export function driverListToMicrodata(drivers) {
|
|
25
|
+
const { items } = prepareDriversList(drivers);
|
|
26
|
+
|
|
27
|
+
const content = items
|
|
28
|
+
.map(
|
|
29
|
+
(
|
|
30
|
+
driver,
|
|
31
|
+
) => `${openTag("article", { itemtype: "Driver", itemid: `#${driver.id}` })}
|
|
32
|
+
${prop("h2", "name", driver.name)}
|
|
33
|
+
${prop("p", "description", driver.truncatedDescription)}
|
|
34
|
+
<p>Skills: ${driver.contributingSkillsCount} | Behaviours: ${driver.contributingBehavioursCount}</p>
|
|
35
|
+
</article>`,
|
|
36
|
+
)
|
|
37
|
+
.join("\n");
|
|
38
|
+
|
|
39
|
+
return htmlDocument(
|
|
40
|
+
"Drivers",
|
|
41
|
+
`<main>
|
|
42
|
+
<h1>Drivers</h1>
|
|
43
|
+
${content}
|
|
44
|
+
</main>`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format driver detail as microdata HTML
|
|
50
|
+
* @param {Object} driver - Raw driver entity
|
|
51
|
+
* @param {Object} context - Additional context
|
|
52
|
+
* @param {Array} context.skills - All skills
|
|
53
|
+
* @param {Array} context.behaviours - All behaviours
|
|
54
|
+
* @returns {string} HTML with microdata
|
|
55
|
+
*/
|
|
56
|
+
export function driverToMicrodata(driver, { skills, behaviours }) {
|
|
57
|
+
const view = prepareDriverDetail(driver, { skills, behaviours });
|
|
58
|
+
|
|
59
|
+
if (!view) return "";
|
|
60
|
+
|
|
61
|
+
const sections = [];
|
|
62
|
+
|
|
63
|
+
// Contributing skills - using contributingSkills property
|
|
64
|
+
if (view.contributingSkills.length > 0) {
|
|
65
|
+
const skillLinks = view.contributingSkills.map(
|
|
66
|
+
(s) =>
|
|
67
|
+
`${openTag("span", { itemprop: "contributingSkills" })}<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a></span>`,
|
|
68
|
+
);
|
|
69
|
+
sections.push(section("Contributing Skills", ul(skillLinks), 2));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Contributing behaviours - using contributingBehaviours property
|
|
73
|
+
if (view.contributingBehaviours.length > 0) {
|
|
74
|
+
const behaviourLinks = view.contributingBehaviours.map(
|
|
75
|
+
(b) =>
|
|
76
|
+
`${openTag("span", { itemprop: "contributingBehaviours" })}<a href="#${escapeHtml(b.id)}">${escapeHtml(b.name)}</a></span>`,
|
|
77
|
+
);
|
|
78
|
+
sections.push(section("Contributing Behaviours", ul(behaviourLinks), 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const body = `<main>
|
|
82
|
+
${openTag("article", { itemtype: "Driver", itemid: `#${view.id}` })}
|
|
83
|
+
${prop("h1", "name", view.name)}
|
|
84
|
+
${metaTag("id", view.id)}
|
|
85
|
+
${prop("p", "description", view.description)}
|
|
86
|
+
${sections.join("\n")}
|
|
87
|
+
</article>
|
|
88
|
+
</main>`;
|
|
89
|
+
|
|
90
|
+
return htmlDocument(view.name, body);
|
|
91
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
tr,
|
|
15
15
|
th,
|
|
16
16
|
td,
|
|
17
|
+
formatLevel,
|
|
17
18
|
} from "../../lib/render.js";
|
|
18
19
|
import { createBackLink } from "../../components/nav.js";
|
|
19
20
|
import { createLevelDots } from "../../components/detail.js";
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from "../../model/levels.js";
|
|
25
26
|
import { createJobBuilderButton } from "../../components/action-buttons.js";
|
|
26
27
|
import { prepareGradeDetail } from "./shared.js";
|
|
28
|
+
import { createJsonLdScript, gradeToJsonLd } from "../json-ld.js";
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Format grade detail as DOM elements
|
|
@@ -38,6 +40,8 @@ export function gradeToDOM(grade, { framework, showBackLink = true } = {}) {
|
|
|
38
40
|
const emoji = framework ? getConceptEmoji(framework, "grade") : "📊";
|
|
39
41
|
return div(
|
|
40
42
|
{ className: "detail-page grade-detail" },
|
|
43
|
+
// JSON-LD structured data
|
|
44
|
+
createJsonLdScript(gradeToJsonLd(grade)),
|
|
41
45
|
// Header
|
|
42
46
|
div(
|
|
43
47
|
{ className: "page-header" },
|
|
@@ -99,10 +103,7 @@ export function gradeToDOM(grade, { framework, showBackLink = true } = {}) {
|
|
|
99
103
|
...Object.entries(view.expectations).map(([key, value]) =>
|
|
100
104
|
div(
|
|
101
105
|
{ className: "list-item" },
|
|
102
|
-
p(
|
|
103
|
-
{ className: "label" },
|
|
104
|
-
key.charAt(0).toUpperCase() + key.slice(1),
|
|
105
|
-
),
|
|
106
|
+
p({ className: "label" }, formatLevel(key)),
|
|
106
107
|
p({}, value),
|
|
107
108
|
),
|
|
108
109
|
),
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grade formatting for microdata HTML output
|
|
3
|
+
*
|
|
4
|
+
* Generates clean, class-less HTML with microdata aligned with grades.schema.json
|
|
5
|
+
* RDF vocab: https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
openTag,
|
|
10
|
+
prop,
|
|
11
|
+
metaTag,
|
|
12
|
+
linkTag,
|
|
13
|
+
section,
|
|
14
|
+
dl,
|
|
15
|
+
escapeHtml,
|
|
16
|
+
formatLevelName,
|
|
17
|
+
htmlDocument,
|
|
18
|
+
} from "../microdata-shared.js";
|
|
19
|
+
import { prepareGradesList, prepareGradeDetail } from "./shared.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format grade list as microdata HTML
|
|
23
|
+
* @param {Array} grades - Raw grade entities
|
|
24
|
+
* @returns {string} HTML with microdata
|
|
25
|
+
*/
|
|
26
|
+
export function gradeListToMicrodata(grades) {
|
|
27
|
+
const { items } = prepareGradesList(grades);
|
|
28
|
+
|
|
29
|
+
const content = items
|
|
30
|
+
.map(
|
|
31
|
+
(g) => `${openTag("article", { itemtype: "Grade", itemid: `#${g.id}` })}
|
|
32
|
+
${prop("h2", "id", g.id)}
|
|
33
|
+
<p>${escapeHtml(g.displayName)}</p>
|
|
34
|
+
${g.typicalExperienceRange ? prop("p", "typicalExperienceRange", g.typicalExperienceRange) : ""}
|
|
35
|
+
${metaTag("ordinalRank", String(g.ordinalRank))}
|
|
36
|
+
</article>`,
|
|
37
|
+
)
|
|
38
|
+
.join("\n");
|
|
39
|
+
|
|
40
|
+
return htmlDocument(
|
|
41
|
+
"Grades",
|
|
42
|
+
`<main>
|
|
43
|
+
<h1>Grades</h1>
|
|
44
|
+
${content}
|
|
45
|
+
</main>`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format grade detail as microdata HTML
|
|
51
|
+
* @param {Object} grade - Raw grade entity
|
|
52
|
+
* @returns {string} HTML with microdata
|
|
53
|
+
*/
|
|
54
|
+
export function gradeToMicrodata(grade) {
|
|
55
|
+
const view = prepareGradeDetail(grade);
|
|
56
|
+
|
|
57
|
+
if (!view) return "";
|
|
58
|
+
|
|
59
|
+
const sections = [];
|
|
60
|
+
|
|
61
|
+
// Titles section
|
|
62
|
+
if (view.professionalTitle || view.managementTitle) {
|
|
63
|
+
const titlePairs = [];
|
|
64
|
+
if (view.professionalTitle) {
|
|
65
|
+
titlePairs.push({
|
|
66
|
+
term: "Professional Track",
|
|
67
|
+
definition: view.professionalTitle,
|
|
68
|
+
itemprop: "professionalTitle",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (view.managementTitle) {
|
|
72
|
+
titlePairs.push({
|
|
73
|
+
term: "Management Track",
|
|
74
|
+
definition: view.managementTitle,
|
|
75
|
+
itemprop: "managementTitle",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
sections.push(section("Titles", dl(titlePairs), 2));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Base skill levels - using BaseSkillLevels itemtype
|
|
82
|
+
if (view.baseSkillLevels && Object.keys(view.baseSkillLevels).length > 0) {
|
|
83
|
+
const levelPairs = Object.entries(view.baseSkillLevels).map(
|
|
84
|
+
([type, level]) => ({
|
|
85
|
+
term: formatLevelName(type),
|
|
86
|
+
definition: formatLevelName(level),
|
|
87
|
+
itemprop: type,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
sections.push(
|
|
91
|
+
section(
|
|
92
|
+
"Base Skill Levels",
|
|
93
|
+
`${openTag("div", { itemtype: "BaseSkillLevels", itemprop: "baseSkillLevels" })}
|
|
94
|
+
${dl(levelPairs)}
|
|
95
|
+
</div>`,
|
|
96
|
+
2,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Base behaviour maturity - link to BehaviourMaturity
|
|
102
|
+
if (
|
|
103
|
+
view.baseBehaviourMaturity &&
|
|
104
|
+
Object.keys(view.baseBehaviourMaturity).length > 0
|
|
105
|
+
) {
|
|
106
|
+
const maturityPairs = Object.entries(view.baseBehaviourMaturity).map(
|
|
107
|
+
([type, maturity]) => ({
|
|
108
|
+
term: formatLevelName(type),
|
|
109
|
+
definition: formatLevelName(maturity),
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
// Handle both single value and object cases
|
|
113
|
+
const maturityContent =
|
|
114
|
+
typeof grade.baseBehaviourMaturity === "string"
|
|
115
|
+
? `${linkTag("baseBehaviourMaturity", `#${grade.baseBehaviourMaturity}`)}
|
|
116
|
+
<p>${formatLevelName(grade.baseBehaviourMaturity)}</p>`
|
|
117
|
+
: dl(maturityPairs);
|
|
118
|
+
sections.push(section("Base Behaviour Maturity", maturityContent, 2));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Expectations - using GradeExpectations itemtype
|
|
122
|
+
if (view.expectations && Object.keys(view.expectations).length > 0) {
|
|
123
|
+
const expectationPairs = Object.entries(view.expectations).map(
|
|
124
|
+
([key, value]) => ({
|
|
125
|
+
term: formatLevelName(key),
|
|
126
|
+
definition: value,
|
|
127
|
+
itemprop: key,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
sections.push(
|
|
131
|
+
section(
|
|
132
|
+
"Expectations",
|
|
133
|
+
`${openTag("div", { itemtype: "GradeExpectations", itemprop: "expectations" })}
|
|
134
|
+
${dl(expectationPairs)}
|
|
135
|
+
</div>`,
|
|
136
|
+
2,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const body = `<main>
|
|
142
|
+
${openTag("article", { itemtype: "Grade", itemid: `#${view.id}` })}
|
|
143
|
+
<h1>${prop("span", "id", view.id)} — ${escapeHtml(view.displayName)}</h1>
|
|
144
|
+
${metaTag("ordinalRank", String(view.ordinalRank))}
|
|
145
|
+
${view.typicalExperienceRange ? prop("p", "typicalExperienceRange", `Experience: ${view.typicalExperienceRange}`) : ""}
|
|
146
|
+
${sections.join("\n")}
|
|
147
|
+
</article>
|
|
148
|
+
</main>`;
|
|
149
|
+
|
|
150
|
+
return htmlDocument(`${view.id} - ${view.displayName}`, body);
|
|
151
|
+
}
|
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,
|