@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
@@ -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
+ }
@@ -8,43 +8,12 @@ import { isCapability, getSkillsByCapability } from "../../model/modifiers.js";
8
8
  import { truncate } from "../shared.js";
9
9
 
10
10
  /**
11
- * Sort tracks by type: professional tracks first, then management tracks.
12
- * Within each type, preserves original order.
11
+ * Sort tracks alphabetically by name.
13
12
  * @param {Array} tracks - Raw track entities
14
13
  * @returns {Array} Sorted tracks array
15
14
  */
16
- export function sortTracksByType(tracks) {
17
- return [...tracks].sort((a, b) => {
18
- const aFlags = getTrackTypeFlags(a);
19
- const bFlags = getTrackTypeFlags(b);
20
-
21
- // Professional tracks come first
22
- if (aFlags.isProfessional && !bFlags.isProfessional) return -1;
23
- if (!aFlags.isProfessional && bFlags.isProfessional) return 1;
24
-
25
- // Preserve original order within same type
26
- return 0;
27
- });
28
- }
29
-
30
- /**
31
- * Determine track type flags from track data.
32
- *
33
- * Logic: Only one flag needs to be explicitly set to true; the other defaults to false.
34
- * - If isManagement: true → management track (isProfessional = false)
35
- * - If isProfessional: true (or neither set) → professional track (isManagement = false)
36
- *
37
- * @param {Object} track
38
- * @param {boolean} [track.isProfessional] - Whether this is a professional/IC track
39
- * @param {boolean} [track.isManagement] - Whether this is a management track
40
- * @returns {{isProfessional: boolean, isManagement: boolean}}
41
- */
42
- export function getTrackTypeFlags(track) {
43
- // Management takes precedence if explicitly set to true
44
- const isManagement = track.isManagement === true;
45
- // Professional is true if management is not true (default behavior)
46
- const isProfessional = !isManagement && track.isProfessional !== false;
47
- return { isProfessional, isManagement };
15
+ export function sortTracksByName(tracks) {
16
+ return [...tracks].sort((a, b) => a.name.localeCompare(b.name));
48
17
  }
49
18
 
50
19
  /**
@@ -53,8 +22,6 @@ export function getTrackTypeFlags(track) {
53
22
  * @property {string} name
54
23
  * @property {string} description
55
24
  * @property {string} truncatedDescription
56
- * @property {boolean} isProfessional
57
- * @property {boolean} isManagement
58
25
  */
59
26
 
60
27
  /**
@@ -64,16 +31,13 @@ export function getTrackTypeFlags(track) {
64
31
  * @returns {{ items: TrackListItem[] }}
65
32
  */
66
33
  export function prepareTracksList(tracks, descriptionLimit = 120) {
67
- const sortedTracks = sortTracksByType(tracks);
34
+ const sortedTracks = sortTracksByName(tracks);
68
35
  const items = sortedTracks.map((track) => {
69
- const { isProfessional, isManagement } = getTrackTypeFlags(track);
70
36
  return {
71
37
  id: track.id,
72
38
  name: track.name,
73
39
  description: track.description,
74
40
  truncatedDescription: truncate(track.description, descriptionLimit),
75
- isProfessional,
76
- isManagement,
77
41
  };
78
42
  });
79
43
 
@@ -101,11 +65,8 @@ export function prepareTracksList(tracks, descriptionLimit = 120) {
101
65
  * @property {string} id
102
66
  * @property {string} name
103
67
  * @property {string} description
104
- * @property {boolean} isProfessional
105
- * @property {boolean} isManagement
106
68
  * @property {SkillModifierRow[]} skillModifiers
107
69
  * @property {BehaviourModifierRow[]} behaviourModifiers
108
- * @property {Array<{id: string, name: string}>} validDisciplines
109
70
  */
110
71
 
111
72
  /**
@@ -114,14 +75,12 @@ export function prepareTracksList(tracks, descriptionLimit = 120) {
114
75
  * @param {Object} context - Additional context
115
76
  * @param {Array} context.skills - All skills
116
77
  * @param {Array} context.behaviours - All behaviours
117
- * @param {Array} context.disciplines - All disciplines
78
+ * @param {Array} context.disciplines - All disciplines (unused but kept for API compatibility)
118
79
  * @returns {TrackDetailView|null}
119
80
  */
120
- export function prepareTrackDetail(track, { skills, behaviours, disciplines }) {
81
+ export function prepareTrackDetail(track, { skills, behaviours }) {
121
82
  if (!track) return null;
122
83
 
123
- const { isProfessional, isManagement } = getTrackTypeFlags(track);
124
-
125
84
  // Build skill modifiers
126
85
  const skillModifiers = track.skillModifiers
127
86
  ? Object.entries(track.skillModifiers).map(([key, modifier]) => {
@@ -160,22 +119,11 @@ export function prepareTrackDetail(track, { skills, behaviours, disciplines }) {
160
119
  )
161
120
  : [];
162
121
 
163
- // Get valid disciplines
164
- const validDisciplines = track.validDisciplines
165
- ? track.validDisciplines
166
- .map((id) => disciplines.find((d) => d.id === id))
167
- .filter(Boolean)
168
- .map((d) => ({ id: d.id, name: d.specialization || d.name }))
169
- : [];
170
-
171
122
  return {
172
123
  id: track.id,
173
124
  name: track.name,
174
125
  description: track.description,
175
- isProfessional,
176
- isManagement,
177
126
  skillModifiers,
178
127
  behaviourModifiers,
179
- validDisciplines,
180
128
  };
181
129
  }
@@ -36,7 +36,7 @@ import {
36
36
  gradeToDOM,
37
37
  trackToDOM,
38
38
  } from "./formatters/index.js";
39
- import { sortTracksByType } from "./formatters/track/shared.js";
39
+ import { sortTracksByName } from "./formatters/track/shared.js";
40
40
 
41
41
  /**
42
42
  * Create a chapter cover page
@@ -271,6 +271,17 @@ function renderBehaviourHandout(data) {
271
271
  renderHandout(content);
272
272
  }
273
273
 
274
+ /**
275
+ * Sort disciplines by type (professional first, then management)
276
+ * @param {Array} disciplines - Raw discipline entities
277
+ * @returns {Array} - Sorted disciplines
278
+ */
279
+ function sortDisciplinesByType(disciplines) {
280
+ const professional = disciplines.filter((d) => !d.isManagement);
281
+ const management = disciplines.filter((d) => d.isManagement);
282
+ return [...professional, ...management];
283
+ }
284
+
274
285
  /**
275
286
  * Render all job component slides (disciplines, grades, tracks)
276
287
  * @param {Object} data
@@ -278,7 +289,10 @@ function renderBehaviourHandout(data) {
278
289
  function renderJobHandout(data) {
279
290
  const { framework } = data;
280
291
 
281
- const disciplineSlides = data.disciplines.map((discipline) => {
292
+ // Sort disciplines by type: professional first, then management
293
+ const sortedDisciplines = sortDisciplinesByType(data.disciplines);
294
+
295
+ const disciplineSlides = sortedDisciplines.map((discipline) => {
282
296
  return disciplineToDOM(discipline, {
283
297
  skills: data.skills,
284
298
  behaviours: data.behaviours,
@@ -295,7 +309,7 @@ function renderJobHandout(data) {
295
309
  });
296
310
  });
297
311
 
298
- const trackSlides = sortTracksByType(data.tracks).map((track) => {
312
+ const trackSlides = sortTracksByName(data.tracks).map((track) => {
299
313
  return trackToDOM(track, {
300
314
  skills: data.skills,
301
315
  behaviours: data.behaviours,
@@ -314,21 +328,21 @@ function renderJobHandout(data) {
314
328
  }),
315
329
  ...disciplineSlides,
316
330
 
317
- // Tracks chapter
318
- createChapterCover({
319
- emoji: getConceptEmoji(framework, "track"),
320
- title: framework.entityDefinitions.track.title,
321
- description: framework.entityDefinitions.track.description,
322
- }),
323
- ...trackSlides,
324
-
325
- // Grades chapter
331
+ // Grades chapter (moved before Tracks)
326
332
  createChapterCover({
327
333
  emoji: getConceptEmoji(framework, "grade"),
328
334
  title: framework.entityDefinitions.grade.title,
329
335
  description: framework.entityDefinitions.grade.description,
330
336
  }),
331
337
  ...gradeSlides,
338
+
339
+ // Tracks chapter (moved after Grades)
340
+ createChapterCover({
341
+ emoji: getConceptEmoji(framework, "track"),
342
+ title: framework.entityDefinitions.track.title,
343
+ description: framework.entityDefinitions.track.description,
344
+ }),
345
+ ...trackSlides,
332
346
  );
333
347
 
334
348
  renderHandout(content);
package/app/handout.html CHANGED
@@ -5,6 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Engineering Pathway - Handout View</title>
7
7
  <link rel="stylesheet" href="css/bundles/handout.css" />
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "mustache": "https://esm.sh/mustache@4.2.0"
12
+ }
13
+ }
14
+ </script>
8
15
  </head>
9
16
  <body class="slide-view handout-view">
10
17
  <header