@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,151 @@
1
+ /**
2
+ * Skill formatting for microdata HTML output
3
+ *
4
+ * Generates clean, class-less HTML with microdata aligned with capability.schema.json
5
+ * RDF vocab: https://schema.forwardimpact.team/rdf/
6
+ */
7
+
8
+ import {
9
+ openTag,
10
+ prop,
11
+ propRaw,
12
+ metaTag,
13
+ section,
14
+ dl,
15
+ ul,
16
+ escapeHtml,
17
+ formatLevelName,
18
+ htmlDocument,
19
+ } from "../microdata-shared.js";
20
+ import { prepareSkillsList, prepareSkillDetail } from "./shared.js";
21
+
22
+ /**
23
+ * Format skill list as microdata HTML
24
+ * @param {Array} skills - Raw skill entities
25
+ * @param {Array} capabilities - Capability entities
26
+ * @returns {string} HTML with microdata
27
+ */
28
+ export function skillListToMicrodata(skills, capabilities) {
29
+ const { groups, groupOrder } = prepareSkillsList(skills, capabilities);
30
+
31
+ const content = groupOrder
32
+ .map((capability) => {
33
+ const capabilitySkills = groups[capability];
34
+ const skillItems = capabilitySkills
35
+ .map(
36
+ (
37
+ skill,
38
+ ) => `${openTag("article", { itemtype: "Skill", itemid: `#${skill.id}` })}
39
+ ${prop("h3", "name", skill.name)}
40
+ ${prop("p", "description", skill.truncatedDescription)}
41
+ ${metaTag("capability", capability)}
42
+ </article>`,
43
+ )
44
+ .join("\n");
45
+
46
+ return section(formatLevelName(capability), skillItems, 2);
47
+ })
48
+ .join("\n");
49
+
50
+ return htmlDocument(
51
+ "Skills",
52
+ `<main>
53
+ <h1>Skills</h1>
54
+ ${content}
55
+ </main>`,
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Format skill detail as microdata HTML
61
+ * @param {Object} skill - Raw skill entity
62
+ * @param {Object} context - Additional context
63
+ * @param {Array} context.disciplines - All disciplines
64
+ * @param {Array} context.tracks - All tracks
65
+ * @param {Array} context.drivers - All drivers
66
+ * @param {Array} context.capabilities - Capability entities
67
+ * @returns {string} HTML with microdata
68
+ */
69
+ export function skillToMicrodata(
70
+ skill,
71
+ { disciplines, tracks, drivers, capabilities },
72
+ ) {
73
+ const view = prepareSkillDetail(skill, {
74
+ disciplines,
75
+ tracks,
76
+ drivers,
77
+ capabilities,
78
+ });
79
+
80
+ if (!view) return "";
81
+
82
+ const sections = [];
83
+
84
+ // Human-only badge
85
+ if (view.isHumanOnly) {
86
+ sections.push(`<p><strong>Human-Only</strong> — Requires interpersonal skills; excluded from agents</p>
87
+ ${metaTag("isHumanOnly", "true")}`);
88
+ }
89
+
90
+ // Level descriptions - uses LevelDescriptions itemtype
91
+ const levelPairs = Object.entries(view.levelDescriptions).map(
92
+ ([level, desc]) => ({
93
+ term: formatLevelName(level),
94
+ definition: desc,
95
+ itemprop: `${level}Description`,
96
+ }),
97
+ );
98
+ sections.push(
99
+ section(
100
+ "Level Descriptions",
101
+ `${openTag("div", { itemtype: "LevelDescriptions", itemprop: "levelDescriptions" })}
102
+ ${dl(levelPairs)}
103
+ </div>`,
104
+ 2,
105
+ ),
106
+ );
107
+
108
+ // Related disciplines
109
+ if (view.relatedDisciplines.length > 0) {
110
+ const disciplineItems = view.relatedDisciplines.map(
111
+ (d) =>
112
+ `<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a> (${escapeHtml(d.skillType)})`,
113
+ );
114
+ sections.push(section("Used in Disciplines", ul(disciplineItems), 2));
115
+ }
116
+
117
+ // Related tracks with modifiers
118
+ if (view.relatedTracks.length > 0) {
119
+ const trackItems = view.relatedTracks.map((t) => {
120
+ const modifierStr = t.modifier > 0 ? `+${t.modifier}` : `${t.modifier}`;
121
+ return `<a href="#${escapeHtml(t.id)}">${escapeHtml(t.name)}</a>: ${modifierStr}`;
122
+ });
123
+ sections.push(section("Modified by Tracks", ul(trackItems), 2));
124
+ }
125
+
126
+ // Related drivers
127
+ if (view.relatedDrivers.length > 0) {
128
+ const driverItems = view.relatedDrivers.map(
129
+ (d) => `<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a>`,
130
+ );
131
+ sections.push(section("Linked to Drivers", ul(driverItems), 2));
132
+ }
133
+
134
+ const body = `<main>
135
+ ${openTag("article", { itemtype: "Skill", itemid: `#${view.id}` })}
136
+ ${prop("h1", "name", view.name)}
137
+ ${metaTag("id", view.id)}
138
+ ${metaTag("capability", view.capability)}
139
+ ${propRaw(
140
+ "div",
141
+ "human",
142
+ `${openTag("div", { itemtype: "SkillHumanSection" })}
143
+ ${prop("p", "description", view.description)}
144
+ ${sections.join("\n")}
145
+ </div>`,
146
+ )}
147
+ </article>
148
+ </main>`;
149
+
150
+ return htmlDocument(view.name, body);
151
+ }
@@ -5,6 +5,7 @@
5
5
  import { div, h2, p, a, span, ul, li } from "../../lib/render.js";
6
6
  import { createBackLink } from "../../components/nav.js";
7
7
  import { prepareStageDetail, getStageEmoji } from "./shared.js";
8
+ import { createJsonLdScript, stageToJsonLd } from "../json-ld.js";
8
9
 
9
10
  /**
10
11
  * Format stage detail as DOM elements
@@ -20,6 +21,8 @@ export function stageToDOM(stage, { stages = [], showBackLink = true } = {}) {
20
21
 
21
22
  return div(
22
23
  { className: "detail-page stage-detail" },
24
+ // JSON-LD structured data
25
+ createJsonLdScript(stageToJsonLd(stage)),
23
26
  // Header
24
27
  div(
25
28
  { className: "page-header" },
@@ -27,28 +30,10 @@ export function stageToDOM(stage, { stages = [], showBackLink = true } = {}) {
27
30
  div(
28
31
  { className: "page-title-row" },
29
32
  span({ className: "page-title" }, `${emoji} ${view.name}`),
30
- span({ className: `badge ${view.modeClassName}` }, view.modeBadge),
31
33
  ),
32
34
  p({ className: "page-description" }, view.description),
33
35
  ),
34
36
 
35
- // Tools section
36
- view.tools.length > 0
37
- ? div(
38
- { className: "section section-detail" },
39
- h2({ className: "section-title" }, "Available Tools"),
40
- div(
41
- { className: "tool-badges" },
42
- ...view.tools.map((tool) =>
43
- span(
44
- { className: "badge badge-tool", title: tool.label },
45
- `${tool.icon} ${tool.label}`,
46
- ),
47
- ),
48
- ),
49
- )
50
- : null,
51
-
52
37
  // Entry/Exit Criteria
53
38
  view.entryCriteria.length > 0 || view.exitCriteria.length > 0
54
39
  ? div(
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Stage formatting for microdata HTML output
3
+ *
4
+ * Generates clean, class-less HTML with microdata aligned with stages.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 { prepareStagesList, prepareStageDetail } from "./shared.js";
19
+
20
+ /**
21
+ * Format stage list as microdata HTML
22
+ * @param {Array} stages - Raw stage entities
23
+ * @returns {string} HTML with microdata
24
+ */
25
+ export function stageListToMicrodata(stages) {
26
+ const { items } = prepareStagesList(stages);
27
+
28
+ const content = items
29
+ .map((stage) => {
30
+ const handoffText =
31
+ stage.handoffs.length > 0
32
+ ? `→ ${stage.handoffs.map((h) => h.target).join(", ")}`
33
+ : "";
34
+ return `${openTag("article", { itemtype: "Stage", itemid: `#${stage.id}` })}
35
+ ${prop("h2", "name", `${stage.emoji || ""} ${stage.name}`)}
36
+ ${prop("p", "description", stage.truncatedDescription)}
37
+ ${handoffText ? `<p>Handoffs: ${handoffText}</p>` : ""}
38
+ </article>`;
39
+ })
40
+ .join("\n");
41
+
42
+ return htmlDocument(
43
+ "Stages",
44
+ `<main>
45
+ <h1>Stages</h1>
46
+ ${content}
47
+ </main>`,
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Format stage detail as microdata HTML
53
+ * @param {Object} stage - Raw stage entity
54
+ * @returns {string} HTML with microdata
55
+ */
56
+ export function stageToMicrodata(stage) {
57
+ const view = prepareStageDetail(stage);
58
+
59
+ if (!view) return "";
60
+
61
+ const sections = [];
62
+
63
+ // Entry criteria
64
+ if (view.entryCriteria.length > 0) {
65
+ const criteriaItems = view.entryCriteria.map((c) => escapeHtml(c));
66
+ sections.push(
67
+ section("Entry Criteria", ul(criteriaItems, "entryCriteria"), 2),
68
+ );
69
+ }
70
+
71
+ // Constraints
72
+ if (view.constraints.length > 0) {
73
+ const constraintItems = view.constraints.map((c) => escapeHtml(c));
74
+ sections.push(
75
+ section("Constraints", ul(constraintItems, "constraints"), 2),
76
+ );
77
+ }
78
+
79
+ // Exit criteria
80
+ if (view.exitCriteria.length > 0) {
81
+ const exitItems = view.exitCriteria.map((c) => escapeHtml(c));
82
+ sections.push(section("Exit Criteria", ul(exitItems, "exitCriteria"), 2));
83
+ }
84
+
85
+ // Handoffs - using Handoff itemtype
86
+ if (view.handoffs.length > 0) {
87
+ const handoffItems = view.handoffs.map(
88
+ (
89
+ h,
90
+ ) => `${openTag("article", { itemtype: "Handoff", itemprop: "handoffs" })}
91
+ ${linkTag("targetStage", `#${h.target}`)}
92
+ <p><strong>${prop("span", "label", h.label)}</strong> → ${escapeHtml(h.target)}</p>
93
+ ${prop("p", "prompt", h.prompt)}
94
+ </article>`,
95
+ );
96
+ sections.push(section("Handoffs", handoffItems.join("\n"), 2));
97
+ }
98
+
99
+ const body = `<main>
100
+ ${openTag("article", { itemtype: "Stage", itemid: `#${view.id}` })}
101
+ ${prop("h1", "name", view.name)}
102
+ ${metaTag("id", view.id)}
103
+ ${stage.emoji ? metaTag("emoji", stage.emoji) : ""}
104
+ ${prop("p", "description", view.description)}
105
+ ${sections.join("\n")}
106
+ </article>
107
+ </main>`;
108
+
109
+ return htmlDocument(view.name, body);
110
+ }
@@ -6,19 +6,6 @@
6
6
 
7
7
  import { truncate } from "../shared.js";
8
8
 
9
- /**
10
- * Tool display configuration
11
- * @type {Object<string, {icon: string, label: string}>}
12
- */
13
- const TOOL_CONFIG = {
14
- search: { icon: "🔍", label: "Search" },
15
- fetch: { icon: "🌐", label: "Fetch" },
16
- codebase: { icon: "📂", label: "Codebase" },
17
- read: { icon: "📖", label: "Read" },
18
- edit: { icon: "✏️", label: "Edit" },
19
- terminal: { icon: "💻", label: "Terminal" },
20
- };
21
-
22
9
  /**
23
10
  * @typedef {Object} StageListItem
24
11
  * @property {string} id
@@ -26,7 +13,6 @@ const TOOL_CONFIG = {
26
13
  * @property {string} emoji
27
14
  * @property {string} description
28
15
  * @property {string} truncatedDescription
29
- * @property {Array<{id: string, icon: string, label: string}>} tools
30
16
  * @property {Array<{target: string, label: string}>} handoffs
31
17
  */
32
18
 
@@ -38,18 +24,12 @@ const TOOL_CONFIG = {
38
24
  */
39
25
  export function prepareStagesList(stages, descriptionLimit = 150) {
40
26
  const items = stages.map((stage) => {
41
- const tools = stage.availableTools || [];
42
27
  return {
43
28
  id: stage.id,
44
29
  name: stage.name,
45
30
  emoji: stage.emoji,
46
31
  description: stage.description,
47
32
  truncatedDescription: truncate(stage.description, descriptionLimit),
48
- tools: tools.map((toolId) => ({
49
- id: toolId,
50
- icon: TOOL_CONFIG[toolId]?.icon || "🔧",
51
- label: TOOL_CONFIG[toolId]?.label || toolId,
52
- })),
53
33
  handoffs: (stage.handoffs || []).map((h) => ({
54
34
  target: h.targetStage,
55
35
  label: h.label,
@@ -65,7 +45,6 @@ export function prepareStagesList(stages, descriptionLimit = 150) {
65
45
  * @property {string} id
66
46
  * @property {string} name
67
47
  * @property {string} description
68
- * @property {Array<{id: string, icon: string, label: string}>} tools
69
48
  * @property {string[]} constraints
70
49
  * @property {string[]} entryCriteria
71
50
  * @property {string[]} exitCriteria
@@ -78,16 +57,10 @@ export function prepareStagesList(stages, descriptionLimit = 150) {
78
57
  * @returns {StageDetailView}
79
58
  */
80
59
  export function prepareStageDetail(stage) {
81
- const tools = stage.availableTools || [];
82
60
  return {
83
61
  id: stage.id,
84
62
  name: stage.name,
85
63
  description: stage.description,
86
- tools: tools.map((toolId) => ({
87
- id: toolId,
88
- icon: TOOL_CONFIG[toolId]?.icon || "🔧",
89
- label: TOOL_CONFIG[toolId]?.label || toolId,
90
- })),
91
64
  constraints: stage.constraints || [],
92
65
  entryCriteria: stage.entryCriteria || [],
93
66
  exitCriteria: stage.exitCriteria || [],
@@ -4,12 +4,9 @@
4
4
 
5
5
  import { div, h1, p } from "../../lib/render.js";
6
6
  import { createBackLink } from "../../components/nav.js";
7
- import { createBadge, createStatCard } from "../../components/card.js";
7
+ import { createStatCard } from "../../components/card.js";
8
8
  import { createStatsGrid } from "../../components/grid.js";
9
- import {
10
- createDetailSection,
11
- createLinksList,
12
- } from "../../components/detail.js";
9
+ import { createDetailSection } from "../../components/detail.js";
13
10
  import {
14
11
  createJobBuilderButton,
15
12
  createInterviewPrepButton,
@@ -20,22 +17,7 @@ import {
20
17
  } from "../../components/modifier-table.js";
21
18
  import { getConceptEmoji } from "../../model/levels.js";
22
19
  import { prepareTrackDetail } from "./shared.js";
23
-
24
- /**
25
- * Get track type badge(s)
26
- * @param {Object} view
27
- * @returns {HTMLElement[]}
28
- */
29
- function getTrackTypeBadges(view) {
30
- const badges = [];
31
- if (view.isProfessional) {
32
- badges.push(createBadge("Professional", "secondary"));
33
- }
34
- if (view.isManagement) {
35
- badges.push(createBadge("Management", "default"));
36
- }
37
- return badges;
38
- }
20
+ import { createJsonLdScript, trackToJsonLd } from "../json-ld.js";
39
21
 
40
22
  /**
41
23
  * Format track detail as DOM elements
@@ -80,12 +62,13 @@ export function trackToDOM(
80
62
 
81
63
  return div(
82
64
  { className: "detail-page track-detail" },
65
+ // JSON-LD structured data
66
+ createJsonLdScript(trackToJsonLd(track)),
83
67
  // Header
84
68
  div(
85
69
  { className: "page-header" },
86
70
  createBackLink("/track", "← Back to Tracks"),
87
71
  h1({ className: "page-title" }, `${emoji} `, view.name),
88
- div({ className: "page-meta" }, ...getTrackTypeBadges(view)),
89
72
  p(
90
73
  { className: "text-muted", style: "margin-top: 0.5rem" },
91
74
  view.description,
@@ -97,14 +80,6 @@ export function trackToDOM(
97
80
  ),
98
81
  ),
99
82
 
100
- // Valid disciplines (if restricted)
101
- view.validDisciplines.length > 0
102
- ? createDetailSection({
103
- title: "Valid Disciplines",
104
- content: createLinksList(view.validDisciplines, "/discipline"),
105
- })
106
- : null,
107
-
108
83
  // Matching weights (stat cards)
109
84
  track.matchingWeights
110
85
  ? createDetailSection({
@@ -18,10 +18,7 @@ export function trackListToMarkdown(tracks, framework) {
18
18
  const lines = [`# ${emoji} Tracks`, ""];
19
19
 
20
20
  for (const track of items) {
21
- const types = [];
22
- if (track.isProfessional) types.push("Professional");
23
- if (track.isManagement) types.push("Management");
24
- lines.push(`- **${track.name}**: ${types.join(", ")}`);
21
+ lines.push(`- **${track.name}**`);
25
22
  }
26
23
  lines.push("");
27
24
 
@@ -44,19 +41,8 @@ export function trackToMarkdown(
44
41
  ) {
45
42
  const view = prepareTrackDetail(track, { skills, behaviours, disciplines });
46
43
 
47
- const types = [];
48
- if (view.isProfessional) types.push("Professional");
49
- if (view.isManagement) types.push("Management");
50
-
51
44
  const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
52
- const lines = [
53
- `# ${emoji} ${view.name}`,
54
- "",
55
- `**Type**: ${types.join(", ")}`,
56
- "",
57
- view.description,
58
- "",
59
- ];
45
+ const lines = [`# ${emoji} ${view.name}`, "", view.description, ""];
60
46
 
61
47
  // Skill modifiers - show expanded skills for capabilities
62
48
  if (view.skillModifiers.length > 0) {
@@ -92,14 +78,5 @@ export function trackToMarkdown(
92
78
  lines.push("");
93
79
  }
94
80
 
95
- // Valid disciplines
96
- if (view.validDisciplines.length > 0) {
97
- lines.push("## Valid Disciplines", "");
98
- for (const d of view.validDisciplines) {
99
- lines.push(`- ${d.name}`);
100
- }
101
- lines.push("");
102
- }
103
-
104
81
  return lines.join("\n");
105
82
  }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Track formatting for microdata HTML output
3
+ *
4
+ * Generates clean, class-less HTML with microdata aligned with track.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 { prepareTracksList, prepareTrackDetail } from "./shared.js";
19
+
20
+ /**
21
+ * Format track list as microdata HTML
22
+ * @param {Array} tracks - Raw track entities
23
+ * @returns {string} HTML with microdata
24
+ */
25
+ export function trackListToMicrodata(tracks) {
26
+ const { items } = prepareTracksList(tracks);
27
+
28
+ const content = items
29
+ .map((track) => {
30
+ return `${openTag("article", { itemtype: "Track", itemid: `#${track.id}` })}
31
+ ${prop("h2", "name", track.name)}
32
+ </article>`;
33
+ })
34
+ .join("\n");
35
+
36
+ return htmlDocument(
37
+ "Tracks",
38
+ `<main>
39
+ <h1>Tracks</h1>
40
+ ${content}
41
+ </main>`,
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Format track detail as microdata HTML
47
+ * @param {Object} track - Raw track entity
48
+ * @param {Object} context - Additional context
49
+ * @param {Array} context.skills - All skills
50
+ * @param {Array} context.behaviours - All behaviours
51
+ * @param {Array} context.disciplines - All disciplines
52
+ * @returns {string} HTML with microdata
53
+ */
54
+ export function trackToMicrodata(track, { skills, behaviours, disciplines }) {
55
+ const view = prepareTrackDetail(track, { skills, behaviours, disciplines });
56
+
57
+ if (!view) return "";
58
+
59
+ const sections = [];
60
+
61
+ // Skill modifiers - using SkillModifier itemtype with targetCapability
62
+ if (view.skillModifiers.length > 0) {
63
+ const modifierItems = view.skillModifiers.map((m) => {
64
+ const modifierStr = m.modifier > 0 ? `+${m.modifier}` : `${m.modifier}`;
65
+
66
+ if (m.isCapability && m.skills && m.skills.length > 0) {
67
+ // Capability with expanded skills
68
+ const skillLinks = m.skills
69
+ .map(
70
+ (s) => `<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a>`,
71
+ )
72
+ .join(", ");
73
+ return `${openTag("div", { itemtype: "SkillModifier", itemprop: "skillModifiers" })}
74
+ ${linkTag("targetCapability", `#${m.id}`)}
75
+ <strong>${escapeHtml(m.name)} Capability</strong> (${openTag("span", { itemprop: "modifierValue" })}${modifierStr}</span>)
76
+ <p>${skillLinks}</p>
77
+ </div>`;
78
+ } else {
79
+ // Individual skill or capability without skills
80
+ return `${openTag("span", { itemtype: "SkillModifier", itemprop: "skillModifiers" })}
81
+ ${linkTag("targetCapability", `#${m.id}`)}
82
+ <strong>${escapeHtml(m.name)}</strong>: ${openTag("span", { itemprop: "modifierValue" })}${modifierStr}</span>
83
+ </span>`;
84
+ }
85
+ });
86
+ sections.push(section("Skill Modifiers", modifierItems.join("\n"), 2));
87
+ }
88
+
89
+ // Behaviour modifiers - using BehaviourModifier itemtype
90
+ if (view.behaviourModifiers.length > 0) {
91
+ const modifierItems = view.behaviourModifiers.map((b) => {
92
+ const modifierStr = b.modifier > 0 ? `+${b.modifier}` : `${b.modifier}`;
93
+ return `${openTag("span", { itemtype: "BehaviourModifier", itemprop: "behaviourModifiers" })}
94
+ ${linkTag("targetBehaviour", `#${b.id}`)}
95
+ <a href="#${escapeHtml(b.id)}">${escapeHtml(b.name)}</a>: ${openTag("span", { itemprop: "modifierValue" })}${modifierStr}</span>
96
+ </span>`;
97
+ });
98
+ sections.push(section("Behaviour Modifiers", ul(modifierItems), 2));
99
+ }
100
+
101
+ const body = `<main>
102
+ ${openTag("article", { itemtype: "Track", itemid: `#${view.id}` })}
103
+ ${prop("h1", "name", view.name)}
104
+ ${metaTag("id", view.id)}
105
+ ${prop("p", "description", view.description)}
106
+ ${sections.join("\n")}
107
+ </article>
108
+ </main>`;
109
+
110
+ return htmlDocument(view.name, body);
111
+ }