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