@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour formatting for microdata HTML output
|
|
3
|
+
*
|
|
4
|
+
* Generates clean, class-less HTML with microdata aligned with behaviour.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 { prepareBehavioursList, prepareBehaviourDetail } from "./shared.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format behaviour list as microdata HTML
|
|
24
|
+
* @param {Array} behaviours - Raw behaviour entities
|
|
25
|
+
* @returns {string} HTML with microdata
|
|
26
|
+
*/
|
|
27
|
+
export function behaviourListToMicrodata(behaviours) {
|
|
28
|
+
const { items } = prepareBehavioursList(behaviours);
|
|
29
|
+
|
|
30
|
+
const content = items
|
|
31
|
+
.map(
|
|
32
|
+
(
|
|
33
|
+
behaviour,
|
|
34
|
+
) => `${openTag("article", { itemtype: "Behaviour", itemid: `#${behaviour.id}` })}
|
|
35
|
+
${prop("h2", "name", behaviour.name)}
|
|
36
|
+
${prop("p", "description", behaviour.truncatedDescription)}
|
|
37
|
+
</article>`,
|
|
38
|
+
)
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
return htmlDocument(
|
|
42
|
+
"Behaviours",
|
|
43
|
+
`<main>
|
|
44
|
+
<h1>Behaviours</h1>
|
|
45
|
+
${content}
|
|
46
|
+
</main>`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format behaviour detail as microdata HTML
|
|
52
|
+
* @param {Object} behaviour - Raw behaviour entity
|
|
53
|
+
* @param {Object} context - Additional context
|
|
54
|
+
* @param {Array} context.drivers - All drivers
|
|
55
|
+
* @returns {string} HTML with microdata
|
|
56
|
+
*/
|
|
57
|
+
export function behaviourToMicrodata(behaviour, { drivers }) {
|
|
58
|
+
const view = prepareBehaviourDetail(behaviour, { drivers });
|
|
59
|
+
|
|
60
|
+
if (!view) return "";
|
|
61
|
+
|
|
62
|
+
const sections = [];
|
|
63
|
+
|
|
64
|
+
// Maturity descriptions - uses MaturityDescriptions itemtype
|
|
65
|
+
const maturityPairs = Object.entries(view.maturityDescriptions).map(
|
|
66
|
+
([maturity, desc]) => ({
|
|
67
|
+
term: formatLevelName(maturity),
|
|
68
|
+
definition: desc,
|
|
69
|
+
itemprop: `${maturity.replace(/_([a-z])/g, (_, c) => c.toUpperCase())}Description`,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
sections.push(
|
|
73
|
+
section(
|
|
74
|
+
"Maturity Levels",
|
|
75
|
+
`${openTag("div", { itemtype: "MaturityDescriptions", itemprop: "maturityDescriptions" })}
|
|
76
|
+
${dl(maturityPairs)}
|
|
77
|
+
</div>`,
|
|
78
|
+
2,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Related drivers
|
|
83
|
+
if (view.relatedDrivers.length > 0) {
|
|
84
|
+
const driverItems = view.relatedDrivers.map(
|
|
85
|
+
(d) => `<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a>`,
|
|
86
|
+
);
|
|
87
|
+
sections.push(section("Linked to Drivers", ul(driverItems), 2));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const body = `<main>
|
|
91
|
+
${openTag("article", { itemtype: "Behaviour", itemid: `#${view.id}` })}
|
|
92
|
+
${prop("h1", "name", view.name)}
|
|
93
|
+
${metaTag("id", view.id)}
|
|
94
|
+
${propRaw(
|
|
95
|
+
"div",
|
|
96
|
+
"human",
|
|
97
|
+
`${openTag("div", { itemtype: "BehaviourHumanSection" })}
|
|
98
|
+
${prop("p", "description", view.description)}
|
|
99
|
+
${sections.join("\n")}
|
|
100
|
+
</div>`,
|
|
101
|
+
)}
|
|
102
|
+
</article>
|
|
103
|
+
</main>`;
|
|
104
|
+
|
|
105
|
+
return htmlDocument(view.name, body);
|
|
106
|
+
}
|
|
@@ -20,6 +20,24 @@ import {
|
|
|
20
20
|
} from "../../components/action-buttons.js";
|
|
21
21
|
import { getConceptEmoji } from "../../model/levels.js";
|
|
22
22
|
import { prepareDisciplineDetail } from "./shared.js";
|
|
23
|
+
import { createJsonLdScript, disciplineToJsonLd } from "../json-ld.js";
|
|
24
|
+
import { createBadge } from "../../components/card.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get type badges for discipline (Management/Professional)
|
|
28
|
+
* @param {Object} discipline - Raw discipline entity
|
|
29
|
+
* @returns {HTMLElement[]}
|
|
30
|
+
*/
|
|
31
|
+
function getDisciplineTypeBadges(discipline) {
|
|
32
|
+
const badges = [];
|
|
33
|
+
if (discipline.isProfessional) {
|
|
34
|
+
badges.push(createBadge("Professional", "secondary"));
|
|
35
|
+
}
|
|
36
|
+
if (discipline.isManagement) {
|
|
37
|
+
badges.push(createBadge("Management", "primary"));
|
|
38
|
+
}
|
|
39
|
+
return badges;
|
|
40
|
+
}
|
|
23
41
|
|
|
24
42
|
/**
|
|
25
43
|
* Format discipline detail as DOM elements
|
|
@@ -44,15 +62,24 @@ export function disciplineToDOM(
|
|
|
44
62
|
) {
|
|
45
63
|
const view = prepareDisciplineDetail(discipline, { skills, behaviours });
|
|
46
64
|
const emoji = getConceptEmoji(framework, "discipline");
|
|
65
|
+
const typeBadges = getDisciplineTypeBadges(discipline);
|
|
47
66
|
return div(
|
|
48
67
|
{ className: "detail-page discipline-detail" },
|
|
68
|
+
// JSON-LD structured data
|
|
69
|
+
createJsonLdScript(disciplineToJsonLd(discipline, { skills })),
|
|
49
70
|
// Header
|
|
50
71
|
div(
|
|
51
72
|
{ className: "page-header" },
|
|
52
73
|
showBackLink
|
|
53
74
|
? createBackLink("/discipline", "← Back to Disciplines")
|
|
54
75
|
: null,
|
|
55
|
-
|
|
76
|
+
div(
|
|
77
|
+
{ className: "page-title-row" },
|
|
78
|
+
heading1({ className: "page-title" }, `${emoji} `, view.name),
|
|
79
|
+
typeBadges.length > 0
|
|
80
|
+
? div({ className: "page-title-badges" }, ...typeBadges)
|
|
81
|
+
: null,
|
|
82
|
+
),
|
|
56
83
|
p({ className: "page-description" }, view.description),
|
|
57
84
|
showBackLink
|
|
58
85
|
? div(
|
|
@@ -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
|
+
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../../model/levels.js";
|
|
25
25
|
import { createJobBuilderButton } from "../../components/action-buttons.js";
|
|
26
26
|
import { prepareGradeDetail } from "./shared.js";
|
|
27
|
+
import { createJsonLdScript, gradeToJsonLd } from "../json-ld.js";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Format grade detail as DOM elements
|
|
@@ -38,6 +39,8 @@ export function gradeToDOM(grade, { framework, showBackLink = true } = {}) {
|
|
|
38
39
|
const emoji = framework ? getConceptEmoji(framework, "grade") : "📊";
|
|
39
40
|
return div(
|
|
40
41
|
{ className: "detail-page grade-detail" },
|
|
42
|
+
// JSON-LD structured data
|
|
43
|
+
createJsonLdScript(gradeToJsonLd(grade)),
|
|
41
44
|
// Header
|
|
42
45
|
div(
|
|
43
46
|
{ className: "page-header" },
|
|
@@ -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
|
+
}
|