@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
@@ -26,18 +26,29 @@ import {
26
26
  * @param {Object} params - Route params
27
27
  */
28
28
  export function renderProgressDetail(params) {
29
- const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
29
+ const { discipline: disciplineId, grade: gradeId, track: trackId } = params;
30
30
  const { data } = getState();
31
31
 
32
32
  // Find the components
33
33
  const discipline = data.disciplines.find((d) => d.id === disciplineId);
34
- const track = data.tracks.find((t) => t.id === trackId);
35
34
  const grade = data.grades.find((g) => g.id === gradeId);
35
+ const track = trackId ? data.tracks.find((t) => t.id === trackId) : null;
36
36
 
37
- if (!discipline || !track || !grade) {
37
+ if (!discipline || !grade) {
38
38
  renderError({
39
39
  title: "Role Not Found",
40
- message: "Invalid role combination. One or more components are missing.",
40
+ message: "Invalid role combination. Discipline or grade not found.",
41
+ backPath: "/career-progress",
42
+ backText: "← Back to Career Progress",
43
+ });
44
+ return;
45
+ }
46
+
47
+ // If trackId was provided but not found, error
48
+ if (trackId && !track) {
49
+ renderError({
50
+ title: "Role Not Found",
51
+ message: `Track "${trackId}" not found.`,
41
52
  backPath: "/career-progress",
42
53
  backText: "← Back to Career Progress",
43
54
  });
@@ -79,8 +90,9 @@ export function renderProgressDetail(params) {
79
90
  a({ href: `#/discipline/${discipline.id}` }, discipline.specialization),
80
91
  " × ",
81
92
  a({ href: `#/grade/${grade.id}` }, grade.id),
82
- " × ",
83
- a({ href: `#/track/${track.id}` }, track.name),
93
+ track
94
+ ? [" × ", a({ href: `#/track/${track.id}` }, track.name)]
95
+ : " (Generalist)",
84
96
  ),
85
97
  ),
86
98
 
@@ -120,7 +132,9 @@ export function renderProgressDetail(params) {
120
132
  { className: "page-actions", style: "margin-top: 2rem" },
121
133
  a(
122
134
  {
123
- href: `#/job/${disciplineId}/${trackId}/${gradeId}`,
135
+ href: trackId
136
+ ? `#/job/${disciplineId}/${gradeId}/${trackId}`
137
+ : `#/job/${disciplineId}/${gradeId}`,
124
138
  className: "btn btn-secondary",
125
139
  },
126
140
  "View Full Job Definition",
@@ -160,17 +174,29 @@ function createComparisonSelectorsSection({
160
174
  // State to track current selections - default to same discipline, same track, next grade
161
175
  let selectedDisciplineId = discipline.id;
162
176
  let selectedGradeId = nextGrade?.id || "";
163
- let selectedTrackId = currentTrack.id;
177
+ let selectedTrackId = currentTrack?.id || "";
164
178
 
165
179
  // Get available options based on selected discipline
166
180
  function getAvailableOptions(disciplineId) {
167
181
  const selectedDisc = data.disciplines.find((d) => d.id === disciplineId);
168
- if (!selectedDisc) return { grades: [], tracks: [] };
182
+ if (!selectedDisc)
183
+ return { grades: [], tracks: [], allowsTrackless: false };
169
184
 
170
185
  const validGrades = [];
171
186
  const validTracks = new Set();
187
+ let allowsTrackless = false;
172
188
 
173
189
  for (const grade of data.grades) {
190
+ // Check trackless combination
191
+ if (
192
+ isValidCombination({ discipline: selectedDisc, grade, track: null })
193
+ ) {
194
+ if (!validGrades.find((g) => g.id === grade.id)) {
195
+ validGrades.push(grade);
196
+ }
197
+ allowsTrackless = true;
198
+ }
199
+ // Check each track combination
174
200
  for (const track of data.tracks) {
175
201
  if (isValidCombination({ discipline: selectedDisc, grade, track })) {
176
202
  if (!validGrades.find((g) => g.id === grade.id)) {
@@ -186,6 +212,7 @@ function createComparisonSelectorsSection({
186
212
  tracks: data.tracks
187
213
  .filter((t) => validTracks.has(t.id))
188
214
  .sort((a, b) => a.name.localeCompare(b.name)),
215
+ allowsTrackless,
189
216
  };
190
217
  }
191
218
 
@@ -196,13 +223,14 @@ function createComparisonSelectorsSection({
196
223
  // Clear previous results
197
224
  comparisonResultsContainer.innerHTML = "";
198
225
 
199
- if (!selectedDisciplineId || !selectedGradeId || !selectedTrackId) {
226
+ // Track can be empty string for generalist, but discipline and grade are required
227
+ if (!selectedDisciplineId || !selectedGradeId) {
200
228
  comparisonResultsContainer.appendChild(
201
229
  div(
202
230
  { className: "comparison-placeholder" },
203
231
  p(
204
232
  { className: "text-muted" },
205
- "Select a discipline, track, and grade to see the comparison.",
233
+ "Select a discipline and grade to see the comparison.",
206
234
  ),
207
235
  ),
208
236
  );
@@ -213,9 +241,12 @@ function createComparisonSelectorsSection({
213
241
  (d) => d.id === selectedDisciplineId,
214
242
  );
215
243
  const targetGrade = data.grades.find((g) => g.id === selectedGradeId);
216
- const targetTrack = data.tracks.find((t) => t.id === selectedTrackId);
244
+ // selectedTrackId can be empty string for generalist
245
+ const targetTrack = selectedTrackId
246
+ ? data.tracks.find((t) => t.id === selectedTrackId)
247
+ : null;
217
248
 
218
- if (!targetDiscipline || !targetGrade || !targetTrack) {
249
+ if (!targetDiscipline || !targetGrade) {
219
250
  return;
220
251
  }
221
252
 
@@ -223,7 +254,7 @@ function createComparisonSelectorsSection({
223
254
  if (
224
255
  targetDiscipline.id === discipline.id &&
225
256
  targetGrade.id === currentGrade.id &&
226
- targetTrack.id === currentTrack.id
257
+ targetTrack?.id === currentTrack?.id
227
258
  ) {
228
259
  comparisonResultsContainer.appendChild(
229
260
  div(
@@ -343,10 +374,12 @@ function createComparisonSelectorsSection({
343
374
  { className: "page-actions" },
344
375
  a(
345
376
  {
346
- href: `#/job/${targetDiscipline.id}/${targetTrack.id}/${targetGrade.id}`,
377
+ href: targetTrack
378
+ ? `#/job/${targetDiscipline.id}/${targetGrade.id}/${targetTrack.id}`
379
+ : `#/job/${targetDiscipline.id}/${targetGrade.id}`,
347
380
  className: "btn btn-secondary",
348
381
  },
349
- `View ${targetGrade.id} ${targetTrack.name} Job Definition →`,
382
+ `View ${targetGrade.id}${targetTrack ? ` ${targetTrack.name}` : ""} Job Definition →`,
350
383
  ),
351
384
  ),
352
385
  );
@@ -394,10 +427,20 @@ function createComparisonSelectorsSection({
394
427
  // Update track selector
395
428
  if (trackSelectEl) {
396
429
  trackSelectEl.innerHTML = "";
397
- const placeholderOpt = document.createElement("option");
398
- placeholderOpt.value = "";
399
- placeholderOpt.textContent = "Select track...";
400
- trackSelectEl.appendChild(placeholderOpt);
430
+
431
+ // Add generalist option if discipline allows trackless
432
+ if (availableOptions.allowsTrackless) {
433
+ const generalistOpt = document.createElement("option");
434
+ generalistOpt.value = "";
435
+ generalistOpt.textContent = "Generalist";
436
+ trackSelectEl.appendChild(generalistOpt);
437
+ } else {
438
+ const placeholderOpt = document.createElement("option");
439
+ placeholderOpt.value = "";
440
+ placeholderOpt.textContent = "Select track...";
441
+ placeholderOpt.disabled = true;
442
+ trackSelectEl.appendChild(placeholderOpt);
443
+ }
401
444
 
402
445
  for (const track of availableOptions.tracks) {
403
446
  const opt = document.createElement("option");
@@ -407,8 +450,16 @@ function createComparisonSelectorsSection({
407
450
  }
408
451
 
409
452
  // Try to keep current selection if valid
410
- if (availableOptions.tracks.find((t) => t.id === selectedTrackId)) {
453
+ const hasValidTrack = availableOptions.tracks.find(
454
+ (t) => t.id === selectedTrackId,
455
+ );
456
+ const isValidGeneralist =
457
+ selectedTrackId === "" && availableOptions.allowsTrackless;
458
+ if (hasValidTrack || isValidGeneralist) {
411
459
  trackSelectEl.value = selectedTrackId;
460
+ } else if (availableOptions.allowsTrackless) {
461
+ selectedTrackId = "";
462
+ trackSelectEl.value = "";
412
463
  } else {
413
464
  selectedTrackId = "";
414
465
  trackSelectEl.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/bin/pathway.js CHANGED
@@ -31,14 +31,10 @@
31
31
  import { fileURLToPath } from "url";
32
32
  import { dirname, join, resolve } from "path";
33
33
  import { existsSync } from "fs";
34
- import {
35
- loadAllData,
36
- loadAgentData,
37
- loadSkillsWithAgentData,
38
- loadQuestionBankFromFolder,
39
- } from "../app/model/loader.js";
34
+ import { loadAllData } from "../app/model/loader.js";
40
35
  import { generateAllIndexes } from "../app/model/index-generator.js";
41
36
  import { formatError } from "../app/lib/cli-output.js";
37
+ import { runSchemaValidation } from "../app/model/schema-validation.js";
42
38
 
43
39
  // Import command handlers
44
40
  import { runSkillCommand } from "../app/commands/skill.js";
@@ -260,7 +256,7 @@ function printHelp() {
260
256
  }
261
257
 
262
258
  /**
263
- * Run full data validation
259
+ * Run full data validation using JSON schemas
264
260
  * @param {string} dataDir - Path to data directory
265
261
  */
266
262
  async function runFullValidation(dataDir) {
@@ -268,77 +264,34 @@ async function runFullValidation(dataDir) {
268
264
 
269
265
  let hasErrors = false;
270
266
 
271
- // Load and validate core data
267
+ // Load data for referential integrity checking (without old validation)
272
268
  const data = await loadAllData(dataDir, {
273
- validate: true,
269
+ validate: false,
274
270
  throwOnError: false,
275
271
  });
276
272
 
277
- if (data.validation.valid) {
278
- console.log("✅ Core data validation passed");
273
+ // Run schema validation + referential integrity
274
+ const result = await runSchemaValidation(dataDir, data);
275
+
276
+ if (result.valid) {
277
+ console.log("✅ Schema validation passed");
279
278
  } else {
280
- console.log("❌ Core data validation failed");
279
+ console.log("❌ Schema validation failed");
281
280
  hasErrors = true;
282
- for (const e of data.validation.errors) {
283
- console.log(` - [${e.type}] ${e.message}`);
281
+ for (const e of result.errors) {
282
+ console.log(
283
+ ` - [${e.type}] ${e.message}${e.path ? ` (${e.path})` : ""}`,
284
+ );
284
285
  }
285
286
  }
286
287
 
287
- if (data.validation.warnings.length > 0) {
288
+ if (result.warnings.length > 0) {
288
289
  console.log("\n⚠️ Warnings:");
289
- for (const w of data.validation.warnings) {
290
+ for (const w of result.warnings) {
290
291
  console.log(` - [${w.type}] ${w.message}`);
291
292
  }
292
293
  }
293
294
 
294
- // Validate question bank
295
- try {
296
- const questionBank = await loadQuestionBankFromFolder(
297
- join(dataDir, "questions"),
298
- data.skills,
299
- data.behaviours,
300
- { validate: true, throwOnError: false },
301
- );
302
-
303
- if (questionBank.validation?.valid) {
304
- console.log("✅ Question bank validation passed");
305
- } else if (questionBank.validation) {
306
- console.log("❌ Question bank validation failed");
307
- hasErrors = true;
308
- for (const e of questionBank.validation.errors) {
309
- console.log(` - [${e.type}] ${e.message}`);
310
- }
311
- }
312
- } catch (err) {
313
- console.log("⚠️ Could not validate question bank:", err.message);
314
- }
315
-
316
- // Validate agent data
317
- try {
318
- const agentData = await loadAgentData(dataDir);
319
- const skillsWithAgent = await loadSkillsWithAgentData(dataDir);
320
-
321
- const skillsWithAgentCount = skillsWithAgent.filter((s) => s.agent).length;
322
-
323
- console.log(
324
- `✅ Agent data: ${agentData.disciplines.length} disciplines, ${agentData.tracks.length} tracks, ${skillsWithAgentCount} skills with agent sections`,
325
- );
326
-
327
- // Check for orphaned agent definitions
328
- for (const d of agentData.disciplines) {
329
- if (!data.disciplines.find((h) => h.id === d.id)) {
330
- console.log(` ⚠️ Agent discipline '${d.id}' has no human definition`);
331
- }
332
- }
333
- for (const t of agentData.tracks) {
334
- if (!data.tracks.find((h) => h.id === t.id)) {
335
- console.log(` ⚠️ Agent track '${t.id}' has no human definition`);
336
- }
337
- }
338
- } catch (err) {
339
- console.log("⚠️ Could not validate agent data:", err.message);
340
- }
341
-
342
295
  // Summary
343
296
  console.log("\n📊 Data Summary:");
344
297
  console.log(` Skills: ${data.skills?.length || 0}`);
@@ -347,6 +300,7 @@ async function runFullValidation(dataDir) {
347
300
  console.log(` Tracks: ${data.tracks?.length || 0}`);
348
301
  console.log(` Grades: ${data.grades?.length || 0}`);
349
302
  console.log(` Drivers: ${data.drivers?.length || 0}`);
303
+ console.log(` Stages: ${data.stages?.length || 0}`);
350
304
  console.log("");
351
305
 
352
306
  return hasErrors ? 1 : 0;