@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.
Files changed (131) hide show
  1. package/app/commands/agent.js +109 -21
  2. package/app/commands/command-factory.js +3 -3
  3. package/app/commands/interview.js +14 -7
  4. package/app/commands/job.js +43 -29
  5. package/app/commands/progress.js +14 -7
  6. package/app/commands/serve.js +5 -0
  7. package/app/commands/stage.js +0 -10
  8. package/app/commands/track.js +5 -8
  9. package/app/components/builder.js +111 -27
  10. package/app/css/components/surfaces.css +16 -0
  11. package/app/formatters/agent/profile.js +113 -87
  12. package/app/formatters/agent/skill.js +64 -31
  13. package/app/formatters/behaviour/dom.js +3 -0
  14. package/app/formatters/behaviour/microdata.js +106 -0
  15. package/app/formatters/discipline/dom.js +28 -1
  16. package/app/formatters/discipline/microdata.js +117 -0
  17. package/app/formatters/discipline/shared.js +49 -8
  18. package/app/formatters/driver/dom.js +3 -0
  19. package/app/formatters/driver/microdata.js +91 -0
  20. package/app/formatters/grade/dom.js +3 -0
  21. package/app/formatters/grade/microdata.js +151 -0
  22. package/app/formatters/index.js +32 -1
  23. package/app/formatters/interview/shared.js +13 -8
  24. package/app/formatters/job/description.js +5 -3
  25. package/app/formatters/json-ld.js +242 -0
  26. package/app/formatters/microdata-shared.js +184 -0
  27. package/app/formatters/progress/shared.js +14 -11
  28. package/app/formatters/skill/dom.js +3 -0
  29. package/app/formatters/skill/microdata.js +151 -0
  30. package/app/formatters/stage/dom.js +3 -18
  31. package/app/formatters/stage/microdata.js +110 -0
  32. package/app/formatters/stage/shared.js +0 -27
  33. package/app/formatters/track/dom.js +5 -30
  34. package/app/formatters/track/markdown.js +2 -25
  35. package/app/formatters/track/microdata.js +111 -0
  36. package/app/formatters/track/shared.js +6 -58
  37. package/app/handout-main.js +26 -12
  38. package/app/index.html +11 -0
  39. package/app/lib/card-mappers.js +17 -12
  40. package/app/lib/job-cache.js +12 -9
  41. package/app/lib/template-loader.js +66 -0
  42. package/app/lib/yaml-loader.js +25 -8
  43. package/app/main.js +8 -4
  44. package/app/model/agent.js +158 -130
  45. package/app/model/checklist.js +57 -91
  46. package/app/model/derivation.js +135 -68
  47. package/app/model/index-generator.js +1 -7
  48. package/app/model/job.js +19 -13
  49. package/app/model/levels.js +20 -12
  50. package/app/model/loader.js +41 -17
  51. package/app/model/matching.js +33 -3
  52. package/app/model/profile.js +38 -45
  53. package/app/model/schema-validation.js +438 -0
  54. package/app/model/validation.js +747 -68
  55. package/app/pages/agent-builder.js +119 -25
  56. package/app/pages/assessment-results.js +10 -4
  57. package/app/pages/discipline.js +36 -6
  58. package/app/pages/driver.js +9 -47
  59. package/app/pages/interview-builder.js +3 -1
  60. package/app/pages/interview.js +15 -4
  61. package/app/pages/job-builder.js +4 -1
  62. package/app/pages/job.js +15 -4
  63. package/app/pages/landing.js +10 -10
  64. package/app/pages/progress-builder.js +3 -1
  65. package/app/pages/progress.js +72 -21
  66. package/app/pages/stage.js +3 -126
  67. package/app/slide-main.js +45 -17
  68. package/app/slides/index.js +3 -1
  69. package/app/slides/overview.js +40 -4
  70. package/app/slides/progress.js +4 -2
  71. package/bin/pathway.js +18 -64
  72. package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
  73. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
  74. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
  75. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
  76. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
  77. package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
  78. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
  79. package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
  80. package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
  81. package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
  82. package/examples/agents/.vscode/settings.json +1 -1
  83. package/examples/behaviours/outcome_ownership.yaml +1 -2
  84. package/examples/behaviours/polymathic_knowledge.yaml +1 -2
  85. package/examples/behaviours/precise_communication.yaml +1 -2
  86. package/examples/behaviours/relentless_curiosity.yaml +1 -2
  87. package/examples/behaviours/systems_thinking.yaml +1 -2
  88. package/examples/capabilities/business.yaml +80 -142
  89. package/examples/capabilities/delivery.yaml +155 -219
  90. package/examples/capabilities/people.yaml +2 -34
  91. package/examples/capabilities/reliability.yaml +161 -80
  92. package/examples/capabilities/scale.yaml +234 -252
  93. package/examples/copilot-setup-steps.yaml +25 -0
  94. package/examples/devcontainer.yaml +21 -0
  95. package/examples/disciplines/_index.yaml +1 -0
  96. package/examples/disciplines/data_engineering.yaml +14 -12
  97. package/examples/disciplines/engineering_management.yaml +63 -0
  98. package/examples/disciplines/software_engineering.yaml +14 -12
  99. package/examples/drivers.yaml +1 -4
  100. package/examples/framework.yaml +1 -2
  101. package/examples/grades.yaml +1 -3
  102. package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
  103. package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
  104. package/examples/questions/behaviours/precise_communication.yaml +1 -2
  105. package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
  106. package/examples/questions/behaviours/systems_thinking.yaml +1 -2
  107. package/examples/questions/skills/architecture_design.yaml +1 -2
  108. package/examples/questions/skills/cloud_platforms.yaml +1 -2
  109. package/examples/questions/skills/code_quality.yaml +1 -2
  110. package/examples/questions/skills/data_modeling.yaml +1 -2
  111. package/examples/questions/skills/devops.yaml +1 -2
  112. package/examples/questions/skills/full_stack_development.yaml +1 -2
  113. package/examples/questions/skills/sre_practices.yaml +1 -2
  114. package/examples/questions/skills/stakeholder_management.yaml +1 -2
  115. package/examples/questions/skills/team_collaboration.yaml +1 -2
  116. package/examples/questions/skills/technical_writing.yaml +1 -2
  117. package/examples/self-assessments.yaml +1 -3
  118. package/examples/stages.yaml +101 -46
  119. package/examples/tracks/_index.yaml +0 -1
  120. package/examples/tracks/platform.yaml +8 -13
  121. package/examples/tracks/sre.yaml +8 -18
  122. package/examples/vscode-settings.yaml +2 -7
  123. package/package.json +9 -3
  124. package/templates/agent.template.md +65 -0
  125. package/templates/skill.template.md +28 -0
  126. package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
  127. package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
  128. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
  129. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
  130. package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
  131. 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
- heading1({ className: "page-title" }, `${emoji} `, view.name),
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 disciplines for list view
42
- * @param {Array} disciplines - Raw discipline entities
43
- * @param {number} [descriptionLimit=120] - Maximum description length
44
- * @returns {{ items: DisciplineListItem[] }}
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
- export function prepareDisciplinesList(disciplines, descriptionLimit = 120) {
47
- const items = disciplines.map((discipline) => ({
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
+ }