@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
@@ -89,16 +89,16 @@ export function renderLanding() {
89
89
  label: "Disciplines",
90
90
  href: "/discipline",
91
91
  }),
92
- createStatCard({
93
- value: data.tracks.length,
94
- label: "Tracks",
95
- href: "/track",
96
- }),
97
92
  createStatCard({
98
93
  value: data.grades.length,
99
94
  label: "Grades",
100
95
  href: "/grade",
101
96
  }),
97
+ createStatCard({
98
+ value: data.tracks.length,
99
+ label: "Tracks",
100
+ href: "/track",
101
+ }),
102
102
  createStatCard({
103
103
  value: data.skills.length,
104
104
  label: "Skills",
@@ -140,16 +140,16 @@ export function renderLanding() {
140
140
  `${data.disciplines.length} ${framework.entityDefinitions.discipline.title.toLowerCase()} — ${framework.entityDefinitions.discipline.description.trim().split("\n")[0]}`,
141
141
  "/discipline",
142
142
  ),
143
- createQuickLinkCard(
144
- `${getConceptEmoji(framework, "track")} ${framework.entityDefinitions.track.title}`,
145
- `${data.tracks.length} ${framework.entityDefinitions.track.title.toLowerCase()} — ${framework.entityDefinitions.track.description.trim().split("\n")[0]}`,
146
- "/track",
147
- ),
148
143
  createQuickLinkCard(
149
144
  `${getConceptEmoji(framework, "grade")} ${framework.entityDefinitions.grade.title}`,
150
145
  `${data.grades.length} ${framework.entityDefinitions.grade.title.toLowerCase()} — ${framework.entityDefinitions.grade.description.trim().split("\n")[0]}`,
151
146
  "/grade",
152
147
  ),
148
+ createQuickLinkCard(
149
+ `${getConceptEmoji(framework, "track")} ${framework.entityDefinitions.track.title}`,
150
+ `${data.tracks.length} ${framework.entityDefinitions.track.title.toLowerCase()} — ${framework.entityDefinitions.track.description.trim().split("\n")[0]}`,
151
+ "/track",
152
+ ),
153
153
  createQuickLinkCard(
154
154
  `${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
155
155
  `${data.skills.length} ${framework.entityDefinitions.skill.title.toLowerCase()} across ${capabilityCount} capabilities — ${framework.entityDefinitions.skill.description.trim().split("\n")[0]}`,
@@ -30,7 +30,9 @@ export function renderCareerProgress() {
30
30
  tracks: data.tracks,
31
31
  }),
32
32
  detailPath: (sel) =>
33
- `/progress/${sel.discipline}/${sel.track}/${sel.grade}`,
33
+ sel.track
34
+ ? `/progress/${sel.discipline}/${sel.grade}/${sel.track}`
35
+ : `/progress/${sel.discipline}/${sel.grade}`,
34
36
  renderPreview: createProgressPreview,
35
37
  labels: {
36
38
  grade: "Current Grade",
@@ -13,7 +13,10 @@ import {
13
13
  } from "../components/comparison-radar.js";
14
14
  import { createProgressionTable } from "../components/progression-table.js";
15
15
  import { renderError } from "../components/error-page.js";
16
- import { createSelectWithValue } from "../lib/form-controls.js";
16
+ import {
17
+ createSelectWithValue,
18
+ createDisciplineSelect,
19
+ } from "../lib/form-controls.js";
17
20
  import {
18
21
  prepareCurrentJob,
19
22
  prepareCustomProgression,
@@ -26,18 +29,29 @@ import {
26
29
  * @param {Object} params - Route params
27
30
  */
28
31
  export function renderProgressDetail(params) {
29
- const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
32
+ const { discipline: disciplineId, grade: gradeId, track: trackId } = params;
30
33
  const { data } = getState();
31
34
 
32
35
  // Find the components
33
36
  const discipline = data.disciplines.find((d) => d.id === disciplineId);
34
- const track = data.tracks.find((t) => t.id === trackId);
35
37
  const grade = data.grades.find((g) => g.id === gradeId);
38
+ const track = trackId ? data.tracks.find((t) => t.id === trackId) : null;
36
39
 
37
- if (!discipline || !track || !grade) {
40
+ if (!discipline || !grade) {
38
41
  renderError({
39
42
  title: "Role Not Found",
40
- message: "Invalid role combination. One or more components are missing.",
43
+ message: "Invalid role combination. Discipline or grade not found.",
44
+ backPath: "/career-progress",
45
+ backText: "← Back to Career Progress",
46
+ });
47
+ return;
48
+ }
49
+
50
+ // If trackId was provided but not found, error
51
+ if (trackId && !track) {
52
+ renderError({
53
+ title: "Role Not Found",
54
+ message: `Track "${trackId}" not found.`,
41
55
  backPath: "/career-progress",
42
56
  backText: "← Back to Career Progress",
43
57
  });
@@ -79,8 +93,9 @@ export function renderProgressDetail(params) {
79
93
  a({ href: `#/discipline/${discipline.id}` }, discipline.specialization),
80
94
  " × ",
81
95
  a({ href: `#/grade/${grade.id}` }, grade.id),
82
- " × ",
83
- a({ href: `#/track/${track.id}` }, track.name),
96
+ track
97
+ ? [" × ", a({ href: `#/track/${track.id}` }, track.name)]
98
+ : " (Generalist)",
84
99
  ),
85
100
  ),
86
101
 
@@ -120,7 +135,9 @@ export function renderProgressDetail(params) {
120
135
  { className: "page-actions", style: "margin-top: 2rem" },
121
136
  a(
122
137
  {
123
- href: `#/job/${disciplineId}/${trackId}/${gradeId}`,
138
+ href: trackId
139
+ ? `#/job/${disciplineId}/${gradeId}/${trackId}`
140
+ : `#/job/${disciplineId}/${gradeId}`,
124
141
  className: "btn btn-secondary",
125
142
  },
126
143
  "View Full Job Definition",
@@ -160,17 +177,29 @@ function createComparisonSelectorsSection({
160
177
  // State to track current selections - default to same discipline, same track, next grade
161
178
  let selectedDisciplineId = discipline.id;
162
179
  let selectedGradeId = nextGrade?.id || "";
163
- let selectedTrackId = currentTrack.id;
180
+ let selectedTrackId = currentTrack?.id || "";
164
181
 
165
182
  // Get available options based on selected discipline
166
183
  function getAvailableOptions(disciplineId) {
167
184
  const selectedDisc = data.disciplines.find((d) => d.id === disciplineId);
168
- if (!selectedDisc) return { grades: [], tracks: [] };
185
+ if (!selectedDisc)
186
+ return { grades: [], tracks: [], allowsTrackless: false };
169
187
 
170
188
  const validGrades = [];
171
189
  const validTracks = new Set();
190
+ let allowsTrackless = false;
172
191
 
173
192
  for (const grade of data.grades) {
193
+ // Check trackless combination
194
+ if (
195
+ isValidCombination({ discipline: selectedDisc, grade, track: null })
196
+ ) {
197
+ if (!validGrades.find((g) => g.id === grade.id)) {
198
+ validGrades.push(grade);
199
+ }
200
+ allowsTrackless = true;
201
+ }
202
+ // Check each track combination
174
203
  for (const track of data.tracks) {
175
204
  if (isValidCombination({ discipline: selectedDisc, grade, track })) {
176
205
  if (!validGrades.find((g) => g.id === grade.id)) {
@@ -186,6 +215,7 @@ function createComparisonSelectorsSection({
186
215
  tracks: data.tracks
187
216
  .filter((t) => validTracks.has(t.id))
188
217
  .sort((a, b) => a.name.localeCompare(b.name)),
218
+ allowsTrackless,
189
219
  };
190
220
  }
191
221
 
@@ -196,13 +226,14 @@ function createComparisonSelectorsSection({
196
226
  // Clear previous results
197
227
  comparisonResultsContainer.innerHTML = "";
198
228
 
199
- if (!selectedDisciplineId || !selectedGradeId || !selectedTrackId) {
229
+ // Track can be empty string for generalist, but discipline and grade are required
230
+ if (!selectedDisciplineId || !selectedGradeId) {
200
231
  comparisonResultsContainer.appendChild(
201
232
  div(
202
233
  { className: "comparison-placeholder" },
203
234
  p(
204
235
  { className: "text-muted" },
205
- "Select a discipline, track, and grade to see the comparison.",
236
+ "Select a discipline and grade to see the comparison.",
206
237
  ),
207
238
  ),
208
239
  );
@@ -213,9 +244,12 @@ function createComparisonSelectorsSection({
213
244
  (d) => d.id === selectedDisciplineId,
214
245
  );
215
246
  const targetGrade = data.grades.find((g) => g.id === selectedGradeId);
216
- const targetTrack = data.tracks.find((t) => t.id === selectedTrackId);
247
+ // selectedTrackId can be empty string for generalist
248
+ const targetTrack = selectedTrackId
249
+ ? data.tracks.find((t) => t.id === selectedTrackId)
250
+ : null;
217
251
 
218
- if (!targetDiscipline || !targetGrade || !targetTrack) {
252
+ if (!targetDiscipline || !targetGrade) {
219
253
  return;
220
254
  }
221
255
 
@@ -223,7 +257,7 @@ function createComparisonSelectorsSection({
223
257
  if (
224
258
  targetDiscipline.id === discipline.id &&
225
259
  targetGrade.id === currentGrade.id &&
226
- targetTrack.id === currentTrack.id
260
+ targetTrack?.id === currentTrack?.id
227
261
  ) {
228
262
  comparisonResultsContainer.appendChild(
229
263
  div(
@@ -343,10 +377,12 @@ function createComparisonSelectorsSection({
343
377
  { className: "page-actions" },
344
378
  a(
345
379
  {
346
- href: `#/job/${targetDiscipline.id}/${targetTrack.id}/${targetGrade.id}`,
380
+ href: targetTrack
381
+ ? `#/job/${targetDiscipline.id}/${targetGrade.id}/${targetTrack.id}`
382
+ : `#/job/${targetDiscipline.id}/${targetGrade.id}`,
347
383
  className: "btn btn-secondary",
348
384
  },
349
- `View ${targetGrade.id} ${targetTrack.name} Job Definition →`,
385
+ `View ${targetGrade.id}${targetTrack ? ` ${targetTrack.name}` : ""} Job Definition →`,
350
386
  ),
351
387
  ),
352
388
  );
@@ -394,10 +430,20 @@ function createComparisonSelectorsSection({
394
430
  // Update track selector
395
431
  if (trackSelectEl) {
396
432
  trackSelectEl.innerHTML = "";
397
- const placeholderOpt = document.createElement("option");
398
- placeholderOpt.value = "";
399
- placeholderOpt.textContent = "Select track...";
400
- trackSelectEl.appendChild(placeholderOpt);
433
+
434
+ // Add generalist option if discipline allows trackless
435
+ if (availableOptions.allowsTrackless) {
436
+ const generalistOpt = document.createElement("option");
437
+ generalistOpt.value = "";
438
+ generalistOpt.textContent = "Generalist";
439
+ trackSelectEl.appendChild(generalistOpt);
440
+ } else {
441
+ const placeholderOpt = document.createElement("option");
442
+ placeholderOpt.value = "";
443
+ placeholderOpt.textContent = "Select track...";
444
+ placeholderOpt.disabled = true;
445
+ trackSelectEl.appendChild(placeholderOpt);
446
+ }
401
447
 
402
448
  for (const track of availableOptions.tracks) {
403
449
  const opt = document.createElement("option");
@@ -407,8 +453,16 @@ function createComparisonSelectorsSection({
407
453
  }
408
454
 
409
455
  // Try to keep current selection if valid
410
- if (availableOptions.tracks.find((t) => t.id === selectedTrackId)) {
456
+ const hasValidTrack = availableOptions.tracks.find(
457
+ (t) => t.id === selectedTrackId,
458
+ );
459
+ const isValidGeneralist =
460
+ selectedTrackId === "" && availableOptions.allowsTrackless;
461
+ if (hasValidTrack || isValidGeneralist) {
411
462
  trackSelectEl.value = selectedTrackId;
463
+ } else if (availableOptions.allowsTrackless) {
464
+ selectedTrackId = "";
465
+ trackSelectEl.value = "";
412
466
  } else {
413
467
  selectedTrackId = "";
414
468
  trackSelectEl.value = "";
@@ -462,11 +516,9 @@ function createComparisonSelectorsSection({
462
516
  div(
463
517
  { className: "form-group" },
464
518
  label({ for: "compare-discipline-select" }, "Target Discipline"),
465
- createSelectWithValue({
519
+ createDisciplineSelect({
466
520
  id: "compare-discipline-select",
467
- items: data.disciplines.sort((a, b) =>
468
- a.specialization.localeCompare(b.specialization),
469
- ),
521
+ disciplines: data.disciplines,
470
522
  initialValue: selectedDisciplineId,
471
523
  placeholder: "Select discipline...",
472
524
  getDisplayName: (d) => d.specialization,
@@ -17,7 +17,7 @@ import {
17
17
  } from "../lib/render.js";
18
18
  import { getState } from "../lib/state.js";
19
19
  import { createBadge } from "../components/card.js";
20
- import { createSelectWithValue } from "../lib/form-controls.js";
20
+ import { createDisciplineSelect } from "../lib/form-controls.js";
21
21
  import {
22
22
  SKILL_LEVEL_ORDER,
23
23
  BEHAVIOUR_MATURITY_ORDER,
@@ -306,9 +306,9 @@ function renderIntroStep(data) {
306
306
  "Select a discipline to highlight which skills are most relevant for that role. " +
307
307
  "You can still assess all skills.",
308
308
  ),
309
- createSelectWithValue({
309
+ createDisciplineSelect({
310
310
  id: "discipline-filter-select",
311
- items: data.disciplines,
311
+ disciplines: data.disciplines,
312
312
  initialValue: assessmentState.discipline || "",
313
313
  placeholder: "Select discipline",
314
314
  onChange: (value) => {
@@ -5,14 +5,8 @@
5
5
  import { render, div, h1, h2, p, span, a, section } from "../lib/render.js";
6
6
  import { getState } from "../lib/state.js";
7
7
  import { createCardList } from "../components/list.js";
8
- import { createDetailHeader } from "../components/detail.js";
9
8
  import { renderNotFound } from "../components/error-page.js";
10
- import {
11
- prepareStagesList,
12
- prepareStageDetail,
13
- getStageEmoji,
14
- } from "../formatters/stage/index.js";
15
- import { createBadge } from "../components/card.js";
9
+ import { prepareStagesList, stageToDOM } from "../formatters/stage/index.js";
16
10
 
17
11
  /**
18
12
  * Map stage to card configuration
@@ -24,7 +18,6 @@ function stageToCardConfig(stage) {
24
18
  title: `${stage.emoji || "🔄"} ${stage.name}`,
25
19
  description: stage.truncatedDescription,
26
20
  href: `/stage/${stage.id}`,
27
- meta: [createBadge(`${stage.tools.length} tools`, "default")],
28
21
  };
29
22
  }
30
23
 
@@ -110,122 +103,6 @@ export function renderStageDetail(params) {
110
103
  return;
111
104
  }
112
105
 
113
- // Transform data for detail view
114
- const view = prepareStageDetail(stage);
115
- const emoji = getStageEmoji(stages, stage.id);
116
-
117
- const page = div(
118
- { className: "stage-detail" },
119
- createDetailHeader({
120
- title: `${emoji} ${view.name}`,
121
- description: view.description,
122
- backLink: "/stage",
123
- backText: "← Back to Stages",
124
- }),
125
-
126
- // Tools section
127
- view.tools.length > 0
128
- ? section(
129
- { className: "section section-detail" },
130
- h2({ className: "section-title" }, "Available Tools"),
131
- div(
132
- { className: "tool-badges" },
133
- ...view.tools.map((tool) =>
134
- span(
135
- { className: "badge badge-tool", title: tool.label },
136
- `${tool.icon} ${tool.label}`,
137
- ),
138
- ),
139
- ),
140
- )
141
- : null,
142
-
143
- // Entry/Exit Criteria
144
- view.entryCriteria.length > 0 || view.exitCriteria.length > 0
145
- ? section(
146
- { className: "section section-detail" },
147
- div(
148
- { className: "content-columns" },
149
- // Entry criteria column
150
- view.entryCriteria.length > 0
151
- ? div(
152
- { className: "column" },
153
- h2({ className: "section-title" }, "Entry Criteria"),
154
- div(
155
- { className: "criteria-list" },
156
- ...view.entryCriteria.map((item) =>
157
- div({ className: "criteria-item" }, `✓ ${item}`),
158
- ),
159
- ),
160
- )
161
- : null,
162
- // Exit criteria column
163
- view.exitCriteria.length > 0
164
- ? div(
165
- { className: "column" },
166
- h2({ className: "section-title" }, "Exit Criteria"),
167
- div(
168
- { className: "criteria-list" },
169
- ...view.exitCriteria.map((item) =>
170
- div({ className: "criteria-item" }, `✓ ${item}`),
171
- ),
172
- ),
173
- )
174
- : null,
175
- ),
176
- )
177
- : null,
178
-
179
- // Constraints
180
- view.constraints.length > 0
181
- ? section(
182
- { className: "section section-detail" },
183
- h2({ className: "section-title" }, "Constraints"),
184
- div(
185
- { className: "constraint-list" },
186
- ...view.constraints.map((item) =>
187
- div({ className: "constraint-item" }, `⚠️ ${item}`),
188
- ),
189
- ),
190
- )
191
- : null,
192
-
193
- // Handoffs
194
- view.handoffs.length > 0
195
- ? section(
196
- { className: "section section-detail" },
197
- h2({ className: "section-title" }, "Handoffs"),
198
- div(
199
- { className: "handoff-list" },
200
- ...view.handoffs.map((handoff) => {
201
- const targetStage = stages.find((s) => s.id === handoff.target);
202
- const targetEmoji = getStageEmoji(stages, handoff.target);
203
- return div(
204
- { className: "card handoff-card" },
205
- div(
206
- { className: "handoff-header" },
207
- targetStage
208
- ? a(
209
- {
210
- href: `#/stage/${handoff.target}`,
211
- className: "handoff-link",
212
- },
213
- `${targetEmoji} ${handoff.label}`,
214
- )
215
- : span({}, `${targetEmoji} ${handoff.label}`),
216
- ),
217
- handoff.prompt
218
- ? p(
219
- { className: "handoff-prompt text-muted" },
220
- handoff.prompt,
221
- )
222
- : null,
223
- );
224
- }),
225
- ),
226
- )
227
- : null,
228
- );
229
-
230
- render(page);
106
+ // Use DOM formatter - it handles transformation internally
107
+ render(stageToDOM(stage, { stages }));
231
108
  }
package/app/slide-main.js CHANGED
@@ -9,7 +9,7 @@ import { setData, getState } from "./lib/state.js";
9
9
  import { loadAllData } from "./lib/yaml-loader.js";
10
10
  import { span, a } from "./lib/render.js";
11
11
  import { generateAllJobs } from "./model/derivation.js";
12
- import { sortTracksByType } from "./formatters/track/shared.js";
12
+ import { sortTracksByName } from "./formatters/track/shared.js";
13
13
 
14
14
  // Import slide renderers
15
15
  import { renderChapterSlide } from "./slides/chapter.js";
@@ -150,13 +150,21 @@ function setupRoutes() {
150
150
  renderTrackSlide({ render: renderSlide, data: getState().data, params });
151
151
  });
152
152
 
153
- // Jobs
154
- router.on("/job/:discipline/:track/:grade", (params) => {
153
+ // Jobs - new format: discipline/grade/track (track optional)
154
+ router.on("/job/:discipline/:grade/:track", (params) => {
155
155
  renderJobSlide({ render: renderSlide, data: getState().data, params });
156
156
  });
157
157
 
158
- // Interviews
159
- router.on("/interview/:discipline/:track/:grade", (params) => {
158
+ router.on("/job/:discipline/:grade", (params) => {
159
+ renderJobSlide({
160
+ render: renderSlide,
161
+ data: getState().data,
162
+ params: { ...params, track: null },
163
+ });
164
+ });
165
+
166
+ // Interviews - new format: discipline/grade/track (track optional)
167
+ router.on("/interview/:discipline/:grade/:track", (params) => {
160
168
  renderInterviewSlide({
161
169
  render: renderSlide,
162
170
  data: getState().data,
@@ -164,10 +172,26 @@ function setupRoutes() {
164
172
  });
165
173
  });
166
174
 
167
- // Progress
168
- router.on("/progress/:discipline/:track/:grade", (params) => {
175
+ router.on("/interview/:discipline/:grade", (params) => {
176
+ renderInterviewSlide({
177
+ render: renderSlide,
178
+ data: getState().data,
179
+ params: { ...params, track: null },
180
+ });
181
+ });
182
+
183
+ // Progress - new format: discipline/grade/track (track optional)
184
+ router.on("/progress/:discipline/:grade/:track", (params) => {
169
185
  renderProgressSlide({ render: renderSlide, data: getState().data, params });
170
186
  });
187
+
188
+ router.on("/progress/:discipline/:grade", (params) => {
189
+ renderProgressSlide({
190
+ render: renderSlide,
191
+ data: getState().data,
192
+ params: { ...params, track: null },
193
+ });
194
+ });
171
195
  }
172
196
 
173
197
  /**
@@ -187,15 +211,7 @@ function buildSlideOrder(data) {
187
211
  data.disciplines.forEach((d) => order.push(`/discipline/${d.id}`));
188
212
  }
189
213
 
190
- // Tracks
191
- if (data.tracks && data.tracks.length > 0) {
192
- boundaries.push(order.length);
193
- order.push("/chapter/track");
194
- order.push("/overview/track");
195
- sortTracksByType(data.tracks).forEach((t) => order.push(`/track/${t.id}`));
196
- }
197
-
198
- // Grades
214
+ // Grades (moved before Tracks)
199
215
  if (data.grades && data.grades.length > 0) {
200
216
  boundaries.push(order.length);
201
217
  order.push("/chapter/grade");
@@ -203,6 +219,14 @@ function buildSlideOrder(data) {
203
219
  data.grades.forEach((g) => order.push(`/grade/${g.id}`));
204
220
  }
205
221
 
222
+ // Tracks (moved after Grades)
223
+ if (data.tracks && data.tracks.length > 0) {
224
+ boundaries.push(order.length);
225
+ order.push("/chapter/track");
226
+ order.push("/overview/track");
227
+ sortTracksByName(data.tracks).forEach((t) => order.push(`/track/${t.id}`));
228
+ }
229
+
206
230
  // Skills
207
231
  if (data.skills && data.skills.length > 0) {
208
232
  boundaries.push(order.length);
@@ -241,7 +265,11 @@ function buildSlideOrder(data) {
241
265
  order.push("/chapter/job");
242
266
  order.push("/overview/job");
243
267
  jobs.forEach((job) =>
244
- order.push(`/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`),
268
+ order.push(
269
+ job.track
270
+ ? `/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
271
+ : `/job/${job.discipline.id}/${job.grade.id}`,
272
+ ),
245
273
  );
246
274
  }
247
275
 
@@ -184,7 +184,9 @@ export function renderSlideIndex({ render, data }) {
184
184
  {},
185
185
  a(
186
186
  {
187
- href: `#/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
187
+ href: job.track
188
+ ? `#/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
189
+ : `#/job/${job.discipline.id}/${job.grade.id}`,
188
190
  },
189
191
  job.title,
190
192
  ),
@@ -4,8 +4,9 @@
4
4
  * Displays overview slides for each chapter with cards for all entities.
5
5
  */
6
6
 
7
- import { div, h1, p, span } from "../lib/render.js";
8
- import { createCardList } from "../components/list.js";
7
+ import { div, h1, h2, p, span } from "../lib/render.js";
8
+ import { createCardList, createGroupedList } from "../components/list.js";
9
+ import { createBadge } from "../components/card.js";
9
10
  import {
10
11
  disciplineToCardConfig,
11
12
  skillToCardConfig,
@@ -23,6 +24,31 @@ import { prepareGradesList } from "../formatters/grade/shared.js";
23
24
  import { prepareTracksList } from "../formatters/track/shared.js";
24
25
  import { generateAllJobs } from "../model/derivation.js";
25
26
 
27
+ /**
28
+ * Format discipline group name for display
29
+ * @param {string} groupName - Group name (professional/management)
30
+ * @returns {string}
31
+ */
32
+ function formatDisciplineGroupName(groupName) {
33
+ if (groupName === "professional") return "Professional";
34
+ if (groupName === "management") return "Management";
35
+ return groupName.charAt(0).toUpperCase() + groupName.slice(1);
36
+ }
37
+
38
+ /**
39
+ * Render discipline group header
40
+ * @param {string} groupName - Group name
41
+ * @param {number} count - Number of items in group
42
+ * @returns {HTMLElement}
43
+ */
44
+ function renderDisciplineGroupHeader(groupName, count) {
45
+ return div(
46
+ { className: "capability-header" },
47
+ h2({ className: "capability-title" }, formatDisciplineGroupName(groupName)),
48
+ createBadge(`${count}`, "default"),
49
+ );
50
+ }
51
+
26
52
  /**
27
53
  * Render overview slide
28
54
  * @param {Object} params
@@ -62,8 +88,9 @@ export function renderOverviewSlide({ render, data, params }) {
62
88
  title: framework.entityDefinitions.discipline.title,
63
89
  emoji: framework.entityDefinitions.discipline.emoji,
64
90
  description: framework.entityDefinitions.discipline.description,
65
- entities: prepareDisciplinesList(data.disciplines).items,
91
+ groups: prepareDisciplinesList(data.disciplines).groups,
66
92
  mapper: disciplineToCardConfig,
93
+ isGrouped: true,
67
94
  },
68
95
  grade: {
69
96
  title: framework.entityDefinitions.grade.title,
@@ -108,6 +135,15 @@ export function renderOverviewSlide({ render, data, params }) {
108
135
  return;
109
136
  }
110
137
 
138
+ // Render content based on whether it's grouped or flat
139
+ const contentElement = config.isGrouped
140
+ ? createGroupedList(
141
+ config.groups,
142
+ config.mapper,
143
+ renderDisciplineGroupHeader,
144
+ )
145
+ : createCardList(config.entities, config.mapper, "No items found.");
146
+
111
147
  const slide = div(
112
148
  { className: "slide overview-slide" },
113
149
  div(
@@ -119,7 +155,7 @@ export function renderOverviewSlide({ render, data, params }) {
119
155
  ),
120
156
  p({ className: "overview-description" }, config.description.trim()),
121
157
  ),
122
- createCardList(config.entities, config.mapper, "No items found."),
158
+ contentElement,
123
159
  );
124
160
 
125
161
  render(slide);
@@ -21,9 +21,11 @@ import { progressToDOM } from "../formatters/index.js";
21
21
  export function renderProgressSlide({ render, data, params }) {
22
22
  const discipline = data.disciplines.find((d) => d.id === params.discipline);
23
23
  const grade = data.grades.find((g) => g.id === params.grade);
24
- const track = data.tracks.find((t) => t.id === params.track);
24
+ const track = params.track
25
+ ? data.tracks.find((t) => t.id === params.track)
26
+ : null;
25
27
 
26
- if (!discipline || !grade || !track) {
28
+ if (!discipline || !grade) {
27
29
  render(
28
30
  div(
29
31
  { className: "slide-error" },
package/app/slides.html CHANGED
@@ -5,6 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Engineering Pathway - Slide View</title>
7
7
  <link rel="stylesheet" href="css/bundles/slides.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">
10
17
  <header