@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
@@ -38,6 +38,9 @@ const ALL_STAGES_VALUE = "all";
38
38
  /** @type {Object|null} Cached agent data */
39
39
  let agentDataCache = null;
40
40
 
41
+ /** @type {{agent: string, skill: string}|null} Cached templates */
42
+ let templateCache = null;
43
+
41
44
  /**
42
45
  * Load agent data with caching
43
46
  * @param {string} dataDir - Data directory path
@@ -50,6 +53,24 @@ async function getAgentData(dataDir = "./data") {
50
53
  return agentDataCache;
51
54
  }
52
55
 
56
+ /**
57
+ * Load templates with caching
58
+ * @returns {Promise<{agent: string, skill: string}>}
59
+ */
60
+ async function getTemplates() {
61
+ if (!templateCache) {
62
+ const [agentRes, skillRes] = await Promise.all([
63
+ fetch("./templates/agent.template.md"),
64
+ fetch("./templates/skill.template.md"),
65
+ ]);
66
+ templateCache = {
67
+ agent: await agentRes.text(),
68
+ skill: await skillRes.text(),
69
+ };
70
+ }
71
+ return templateCache;
72
+ }
73
+
53
74
  /**
54
75
  * Render agent builder page
55
76
  */
@@ -64,8 +85,11 @@ export async function renderAgentBuilder() {
64
85
  ),
65
86
  );
66
87
 
67
- // Load agent-specific data
68
- const agentData = await getAgentData();
88
+ // Load agent-specific data and templates
89
+ const [agentData, templates] = await Promise.all([
90
+ getAgentData(),
91
+ getTemplates(),
92
+ ]);
69
93
 
70
94
  // Filter to only disciplines/tracks that have agent definitions
71
95
  const agentDisciplineIds = new Set(agentData.disciplines.map((d) => d.id));
@@ -87,10 +111,13 @@ export async function renderAgentBuilder() {
87
111
  ];
88
112
 
89
113
  // Parse URL params for pre-selection
114
+ // Supports: /agent/discipline, /agent/discipline/track, /agent/discipline/track/stage
90
115
  const hash = window.location.hash;
91
- const pathMatch = hash.match(/#\/agent\/([^/]+)\/([^/]+)(?:\/([^/?]+))?/);
116
+ const pathMatch = hash.match(
117
+ /#\/agent\/([^/]+)(?:\/([^/]+))?(?:\/([^/?]+))?/,
118
+ );
92
119
  const initialDiscipline = pathMatch ? pathMatch[1] : "";
93
- const initialTrack = pathMatch ? pathMatch[2] : "";
120
+ const initialTrack = pathMatch && pathMatch[2] ? pathMatch[2] : "";
94
121
  const initialStage =
95
122
  pathMatch && pathMatch[3] ? pathMatch[3] : ALL_STAGES_VALUE;
96
123
 
@@ -113,9 +140,10 @@ export async function renderAgentBuilder() {
113
140
  */
114
141
  function updatePreview({ discipline, track, stage }) {
115
142
  // Update URL without triggering navigation
116
- if (discipline && track) {
143
+ if (discipline) {
144
+ const trackPart = track ? `/${track}` : "";
117
145
  const stagePart = stage && stage !== ALL_STAGES_VALUE ? `/${stage}` : "";
118
- const newHash = `#/agent/${discipline}/${track}${stagePart}`;
146
+ const newHash = `#/agent/${discipline}${trackPart}${stagePart}`;
119
147
  if (window.location.hash !== newHash) {
120
148
  history.replaceState(null, "", newHash);
121
149
  }
@@ -123,7 +151,7 @@ export async function renderAgentBuilder() {
123
151
 
124
152
  previewContainer.innerHTML = "";
125
153
 
126
- if (!discipline || !track) {
154
+ if (!discipline) {
127
155
  previewContainer.appendChild(
128
156
  createEmptyState(availableDisciplines.length, availableTracks.length),
129
157
  );
@@ -132,7 +160,7 @@ export async function renderAgentBuilder() {
132
160
 
133
161
  // Get full objects
134
162
  const humanDiscipline = data.disciplines.find((d) => d.id === discipline);
135
- const humanTrack = data.tracks.find((t) => t.id === track);
163
+ const humanTrack = track ? data.tracks.find((t) => t.id === track) : null;
136
164
  const agentDiscipline = agentData.disciplines.find(
137
165
  (d) => d.id === discipline,
138
166
  );
@@ -164,6 +192,8 @@ export async function renderAgentBuilder() {
164
192
  agentBehaviours: agentData.behaviours,
165
193
  capabilities: data.capabilities,
166
194
  vscodeSettings: agentData.vscodeSettings,
195
+ devcontainer: agentData.devcontainer,
196
+ templates,
167
197
  };
168
198
 
169
199
  // Generate preview based on stage selection
@@ -325,6 +355,8 @@ function createAllStagesPreview(context) {
325
355
  agentBehaviours,
326
356
  capabilities,
327
357
  vscodeSettings,
358
+ devcontainer,
359
+ templates,
328
360
  } = context;
329
361
 
330
362
  // Generate all stage agents
@@ -378,7 +410,13 @@ function createAllStagesPreview(context) {
378
410
  { className: "agent-deployment" },
379
411
 
380
412
  // Download all button
381
- createDownloadAllButton(stageAgents, skillFiles, vscodeSettings, context),
413
+ createDownloadAllButton(
414
+ stageAgents,
415
+ skillFiles,
416
+ vscodeSettings,
417
+ devcontainer,
418
+ context,
419
+ ),
382
420
 
383
421
  // Agents section
384
422
  section(
@@ -391,7 +429,7 @@ function createAllStagesPreview(context) {
391
429
  div(
392
430
  { className: "agent-cards-grid" },
393
431
  ...stageAgents.map(({ stage, profile }) =>
394
- createAgentCard(stage, profile, stages),
432
+ createAgentCard(stage, profile, stages, templates.agent),
395
433
  ),
396
434
  ),
397
435
  ),
@@ -403,7 +441,9 @@ function createAllStagesPreview(context) {
403
441
  skillFiles.length > 0
404
442
  ? div(
405
443
  { className: "skill-cards-grid" },
406
- ...skillFiles.map((skill) => createSkillCard(skill)),
444
+ ...skillFiles.map((skill) =>
445
+ createSkillCard(skill, templates.skill),
446
+ ),
407
447
  )
408
448
  : p(
409
449
  { className: "text-muted" },
@@ -434,7 +474,9 @@ function createSingleStagePreview(context, stage) {
434
474
  agentBehaviours,
435
475
  capabilities,
436
476
  vscodeSettings,
477
+ devcontainer,
437
478
  stages,
479
+ templates,
438
480
  } = context;
439
481
 
440
482
  // Derive stage agent
@@ -483,7 +525,13 @@ function createSingleStagePreview(context, stage) {
483
525
  { className: "agent-deployment" },
484
526
 
485
527
  // Download button for single stage
486
- createDownloadSingleButton(profile, skillFiles, vscodeSettings),
528
+ createDownloadSingleButton(
529
+ profile,
530
+ skillFiles,
531
+ vscodeSettings,
532
+ devcontainer,
533
+ templates,
534
+ ),
487
535
 
488
536
  // Agents section (single card)
489
537
  section(
@@ -491,7 +539,7 @@ function createSingleStagePreview(context, stage) {
491
539
  h2({}, "Agent"),
492
540
  div(
493
541
  { className: "agent-cards-grid single" },
494
- createAgentCard(stage, profile, stages, derived),
542
+ createAgentCard(stage, profile, stages, templates.agent, derived),
495
543
  ),
496
544
  ),
497
545
 
@@ -502,7 +550,9 @@ function createSingleStagePreview(context, stage) {
502
550
  skillFiles.length > 0
503
551
  ? div(
504
552
  { className: "skill-cards-grid" },
505
- ...skillFiles.map((skill) => createSkillCard(skill)),
553
+ ...skillFiles.map((skill) =>
554
+ createSkillCard(skill, templates.skill),
555
+ ),
506
556
  )
507
557
  : p(
508
558
  { className: "text-muted" },
@@ -520,11 +570,12 @@ function createSingleStagePreview(context, stage) {
520
570
  * @param {Object} stage - Stage object
521
571
  * @param {Object} profile - Generated profile
522
572
  * @param {Array} stages - All stages for emoji lookup
573
+ * @param {string} agentTemplate - Mustache template for agent profile
523
574
  * @param {Object} [_derived] - Optional derived agent data for extra info
524
575
  * @returns {HTMLElement}
525
576
  */
526
- function createAgentCard(stage, profile, stages, _derived) {
527
- const content = formatAgentProfile(profile);
577
+ function createAgentCard(stage, profile, stages, agentTemplate, _derived) {
578
+ const content = formatAgentProfile(profile, agentTemplate);
528
579
  const stageEmoji = getStageEmoji(stages, stage.id);
529
580
 
530
581
  const card = div(
@@ -548,10 +599,11 @@ function createAgentCard(stage, profile, stages, _derived) {
548
599
  /**
549
600
  * Create a skill card
550
601
  * @param {Object} skill - Skill with frontmatter and body
602
+ * @param {string} skillTemplate - Mustache template for skill
551
603
  * @returns {HTMLElement}
552
604
  */
553
- function createSkillCard(skill) {
554
- const content = formatAgentSkill(skill);
605
+ function createSkillCard(skill, skillTemplate) {
606
+ const content = formatAgentSkill(skill, skillTemplate);
555
607
  const filename = `${skill.dirname}/SKILL.md`;
556
608
 
557
609
  return div(
@@ -615,16 +667,18 @@ function createCopyButton(content) {
615
667
  * @param {Array} stageAgents - Array of {stage, derived, profile}
616
668
  * @param {Array} skillFiles - Array of skill file objects
617
669
  * @param {Object} vscodeSettings - VS Code settings
618
- * @param {Object} context - Context with discipline/track info
670
+ * @param {Object} devcontainer - Devcontainer config
671
+ * @param {Object} context - Context with discipline/track info and templates
619
672
  * @returns {HTMLElement}
620
673
  */
621
674
  function createDownloadAllButton(
622
675
  stageAgents,
623
676
  skillFiles,
624
677
  vscodeSettings,
678
+ devcontainer,
625
679
  context,
626
680
  ) {
627
- const { humanDiscipline, humanTrack } = context;
681
+ const { humanDiscipline, humanTrack, templates } = context;
628
682
  const agentName = `${humanDiscipline.id}-${humanTrack.id}`.replace(/_/g, "-");
629
683
 
630
684
  const btn = document.createElement("button");
@@ -641,13 +695,13 @@ function createDownloadAllButton(
641
695
 
642
696
  // Add all stage agent profiles
643
697
  for (const { profile } of stageAgents) {
644
- const content = formatAgentProfile(profile);
698
+ const content = formatAgentProfile(profile, templates.agent);
645
699
  zip.file(`.github/agents/${profile.filename}`, content);
646
700
  }
647
701
 
648
702
  // Add skills
649
703
  for (const skill of skillFiles) {
650
- const content = formatAgentSkill(skill);
704
+ const content = formatAgentSkill(skill, templates.skill);
651
705
  zip.file(`.claude/skills/${skill.dirname}/SKILL.md`, content);
652
706
  }
653
707
 
@@ -659,6 +713,22 @@ function createDownloadAllButton(
659
713
  );
660
714
  }
661
715
 
716
+ // Add devcontainer.json with VS Code settings embedded
717
+ if (devcontainer && Object.keys(devcontainer).length > 0) {
718
+ const devcontainerJson = {
719
+ ...devcontainer,
720
+ customizations: {
721
+ vscode: {
722
+ settings: vscodeSettings,
723
+ },
724
+ },
725
+ };
726
+ zip.file(
727
+ ".devcontainer/devcontainer.json",
728
+ JSON.stringify(devcontainerJson, null, 2) + "\n",
729
+ );
730
+ }
731
+
662
732
  // Generate and download
663
733
  const blob = await zip.generateAsync({ type: "blob" });
664
734
  const url = URL.createObjectURL(blob);
@@ -685,9 +755,17 @@ function createDownloadAllButton(
685
755
  * @param {Object} profile - Agent profile
686
756
  * @param {Array} skillFiles - Skill files
687
757
  * @param {Object} vscodeSettings - VS Code settings
758
+ * @param {Object} devcontainer - Devcontainer config
759
+ * @param {{agent: string, skill: string}} templates - Mustache templates
688
760
  * @returns {HTMLElement}
689
761
  */
690
- function createDownloadSingleButton(profile, skillFiles, vscodeSettings) {
762
+ function createDownloadSingleButton(
763
+ profile,
764
+ skillFiles,
765
+ vscodeSettings,
766
+ devcontainer,
767
+ templates,
768
+ ) {
691
769
  const btn = document.createElement("button");
692
770
  btn.className = "btn btn-primary download-all-btn";
693
771
  btn.textContent = "📥 Download Agent (.zip)";
@@ -701,12 +779,12 @@ function createDownloadSingleButton(profile, skillFiles, vscodeSettings) {
701
779
  const zip = new JSZip();
702
780
 
703
781
  // Add profile
704
- const content = formatAgentProfile(profile);
782
+ const content = formatAgentProfile(profile, templates.agent);
705
783
  zip.file(`.github/agents/${profile.filename}`, content);
706
784
 
707
785
  // Add skills
708
786
  for (const skill of skillFiles) {
709
- const skillContent = formatAgentSkill(skill);
787
+ const skillContent = formatAgentSkill(skill, templates.skill);
710
788
  zip.file(`.claude/skills/${skill.dirname}/SKILL.md`, skillContent);
711
789
  }
712
790
 
@@ -718,6 +796,22 @@ function createDownloadSingleButton(profile, skillFiles, vscodeSettings) {
718
796
  );
719
797
  }
720
798
 
799
+ // Add devcontainer.json with VS Code settings embedded
800
+ if (devcontainer && Object.keys(devcontainer).length > 0) {
801
+ const devcontainerJson = {
802
+ ...devcontainer,
803
+ customizations: {
804
+ vscode: {
805
+ settings: vscodeSettings,
806
+ },
807
+ },
808
+ };
809
+ zip.file(
810
+ ".devcontainer/devcontainer.json",
811
+ JSON.stringify(devcontainerJson, null, 2) + "\n",
812
+ );
813
+ }
814
+
721
815
  // Generate and download
722
816
  const blob = await zip.generateAsync({ type: "blob" });
723
817
  const url = URL.createObjectURL(blob);
@@ -379,7 +379,9 @@ function createMatchCard(match, _index, _selfAssessment, _data) {
379
379
  { className: "match-job-title" },
380
380
  a(
381
381
  {
382
- href: `#/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
382
+ href: job.track
383
+ ? `#/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
384
+ : `#/job/${job.discipline.id}/${job.grade.id}`,
383
385
  },
384
386
  job.title,
385
387
  ),
@@ -388,7 +390,7 @@ function createMatchCard(match, _index, _selfAssessment, _data) {
388
390
  { className: "match-badges" },
389
391
  createBadge(job.discipline.name, "default"),
390
392
  createBadge(job.grade.name, "secondary"),
391
- createBadge(job.track.name, "broad"),
393
+ job.track && createBadge(job.track.name, "broad"),
392
394
  ),
393
395
  ),
394
396
  div(
@@ -437,14 +439,18 @@ function createMatchCard(match, _index, _selfAssessment, _data) {
437
439
  { className: "match-card-actions" },
438
440
  a(
439
441
  {
440
- href: `#/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
442
+ href: job.track
443
+ ? `#/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
444
+ : `#/job/${job.discipline.id}/${job.grade.id}`,
441
445
  className: "btn btn-secondary btn-sm",
442
446
  },
443
447
  "View Job Details",
444
448
  ),
445
449
  a(
446
450
  {
447
- href: `#/interview/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
451
+ href: job.track
452
+ ? `#/interview/${job.discipline.id}/${job.grade.id}/${job.track.id}`
453
+ : `#/interview/${job.discipline.id}/${job.grade.id}`,
448
454
  className: "btn btn-secondary btn-sm",
449
455
  },
450
456
  "Interview Prep",
@@ -2,14 +2,40 @@
2
2
  * Disciplines pages
3
3
  */
4
4
 
5
- import { render, div, h1, p } from "../lib/render.js";
5
+ import { render, div, h1, h2, p } from "../lib/render.js";
6
6
  import { getState } from "../lib/state.js";
7
- import { createCardList } from "../components/list.js";
7
+ import { createGroupedList } from "../components/list.js";
8
+ import { createBadge } from "../components/card.js";
8
9
  import { renderNotFound } from "../components/error-page.js";
9
10
  import { prepareDisciplinesList } from "../formatters/discipline/shared.js";
10
11
  import { disciplineToDOM } from "../formatters/discipline/dom.js";
11
12
  import { disciplineToCardConfig } from "../lib/card-mappers.js";
12
13
 
14
+ /**
15
+ * Format discipline group name for display
16
+ * @param {string} groupName - Group name (professional/management)
17
+ * @returns {string}
18
+ */
19
+ function formatDisciplineGroupName(groupName) {
20
+ if (groupName === "professional") return "Professional";
21
+ if (groupName === "management") return "Management";
22
+ return groupName.charAt(0).toUpperCase() + groupName.slice(1);
23
+ }
24
+
25
+ /**
26
+ * Render discipline group header
27
+ * @param {string} groupName - Group name
28
+ * @param {number} count - Number of items in group
29
+ * @returns {HTMLElement}
30
+ */
31
+ function renderDisciplineGroupHeader(groupName, count) {
32
+ return div(
33
+ { className: "capability-header" },
34
+ h2({ className: "capability-title" }, formatDisciplineGroupName(groupName)),
35
+ createBadge(`${count}`, "default"),
36
+ );
37
+ }
38
+
13
39
  /**
14
40
  * Render disciplines list page
15
41
  */
@@ -17,8 +43,8 @@ export function renderDisciplinesList() {
17
43
  const { data } = getState();
18
44
  const { framework } = data;
19
45
 
20
- // Transform data for list view
21
- const { items } = prepareDisciplinesList(data.disciplines);
46
+ // Transform data for list view (grouped by professional/management)
47
+ const { groups } = prepareDisciplinesList(data.disciplines);
22
48
 
23
49
  const page = div(
24
50
  { className: "disciplines-page" },
@@ -35,8 +61,12 @@ export function renderDisciplinesList() {
35
61
  ),
36
62
  ),
37
63
 
38
- // Disciplines list
39
- createCardList(items, disciplineToCardConfig, "No disciplines found."),
64
+ // Disciplines list (grouped by type)
65
+ createGroupedList(
66
+ groups,
67
+ disciplineToCardConfig,
68
+ renderDisciplineGroupHeader,
69
+ ),
40
70
  );
41
71
 
42
72
  render(page);
@@ -2,15 +2,12 @@
2
2
  * Drivers pages
3
3
  */
4
4
 
5
- import { render, div, h1, h2, p, section } from "../lib/render.js";
5
+ import { render, div, h1, p } from "../lib/render.js";
6
6
  import { getState } from "../lib/state.js";
7
7
  import { createCardList } from "../components/list.js";
8
- import { createDetailHeader, createLinksList } from "../components/detail.js";
9
8
  import { renderNotFound } from "../components/error-page.js";
10
- import {
11
- prepareDriversList,
12
- prepareDriverDetail,
13
- } from "../formatters/driver/shared.js";
9
+ import { prepareDriversList } from "../formatters/driver/shared.js";
10
+ import { driverToDOM } from "../formatters/driver/dom.js";
14
11
  import { driverToCardConfig } from "../lib/card-mappers.js";
15
12
 
16
13
  /**
@@ -60,47 +57,12 @@ export function renderDriverDetail(params) {
60
57
  return;
61
58
  }
62
59
 
63
- // Transform data for detail view
64
- const view = prepareDriverDetail(driver, {
65
- skills: data.skills,
66
- behaviours: data.behaviours,
67
- });
68
-
69
- const page = div(
70
- { className: "driver-detail" },
71
- createDetailHeader({
72
- title: view.name,
73
- description: view.description,
74
- backLink: "/driver",
75
- backText: "← Back to Drivers",
60
+ // Use DOM formatter - it handles transformation internally
61
+ render(
62
+ driverToDOM(driver, {
63
+ skills: data.skills,
64
+ behaviours: data.behaviours,
65
+ framework: data.framework,
76
66
  }),
77
-
78
- // Contributing Skills and Contributing Behaviours in two columns
79
- view.contributingSkills.length > 0 || view.contributingBehaviours.length > 0
80
- ? section(
81
- { className: "section section-detail" },
82
- div(
83
- { className: "content-columns" },
84
- // Contributing Skills column
85
- view.contributingSkills.length > 0
86
- ? div(
87
- { className: "column" },
88
- h2({ className: "section-title" }, "Contributing Skills"),
89
- createLinksList(view.contributingSkills, "/skill"),
90
- )
91
- : null,
92
- // Contributing Behaviours column
93
- view.contributingBehaviours.length > 0
94
- ? div(
95
- { className: "column" },
96
- h2({ className: "section-title" }, "Contributing Behaviours"),
97
- createLinksList(view.contributingBehaviours, "/behaviour"),
98
- )
99
- : null,
100
- ),
101
- )
102
- : null,
103
67
  );
104
-
105
- render(page);
106
68
  }
@@ -29,7 +29,9 @@ export function renderInterviewPrep() {
29
29
  grades: data.grades,
30
30
  }),
31
31
  detailPath: (sel) =>
32
- `/interview/${sel.discipline}/${sel.track}/${sel.grade}`,
32
+ sel.track
33
+ ? `/interview/${sel.discipline}/${sel.grade}/${sel.track}`
34
+ : `/interview/${sel.discipline}/${sel.grade}`,
33
35
  renderPreview: createStandardPreview,
34
36
  helpItems: [
35
37
  {
@@ -32,18 +32,29 @@ import {
32
32
  * @param {Object} params - Route params
33
33
  */
34
34
  export function renderInterviewDetail(params) {
35
- const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
35
+ const { discipline: disciplineId, grade: gradeId, track: trackId } = params;
36
36
  const { data } = getState();
37
37
 
38
38
  // Find the components
39
39
  const discipline = data.disciplines.find((d) => d.id === disciplineId);
40
- const track = data.tracks.find((t) => t.id === trackId);
41
40
  const grade = data.grades.find((g) => g.id === gradeId);
41
+ const track = trackId ? data.tracks.find((t) => t.id === trackId) : null;
42
42
 
43
- if (!discipline || !track || !grade) {
43
+ if (!discipline || !grade) {
44
44
  renderError({
45
45
  title: "Interview Not Found",
46
- message: "Invalid combination. One or more components are missing.",
46
+ message: "Invalid combination. Discipline or grade not found.",
47
+ backPath: "/interview-prep",
48
+ backText: "← Back to Interview Prep",
49
+ });
50
+ return;
51
+ }
52
+
53
+ // If trackId was provided but not found, error
54
+ if (trackId && !track) {
55
+ renderError({
56
+ title: "Interview Not Found",
57
+ message: `Track "${trackId}" not found.`,
47
58
  backPath: "/interview-prep",
48
59
  backText: "← Back to Interview Prep",
49
60
  });
@@ -29,7 +29,10 @@ export function renderJobBuilder() {
29
29
  behaviourCount: data.behaviours.length,
30
30
  grades: data.grades,
31
31
  }),
32
- detailPath: (sel) => `/job/${sel.discipline}/${sel.track}/${sel.grade}`,
32
+ detailPath: (sel) =>
33
+ sel.track
34
+ ? `/job/${sel.discipline}/${sel.grade}/${sel.track}`
35
+ : `/job/${sel.discipline}/${sel.grade}`,
33
36
  renderPreview: createStandardPreview,
34
37
  helpItems: [
35
38
  {
package/app/pages/job.js CHANGED
@@ -13,18 +13,29 @@ import { jobToDOM } from "../formatters/job/dom.js";
13
13
  * @param {Object} params - Route params
14
14
  */
15
15
  export function renderJobDetail(params) {
16
- const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
16
+ const { discipline: disciplineId, grade: gradeId, track: trackId } = params;
17
17
  const { data } = getState();
18
18
 
19
19
  // Find the components
20
20
  const discipline = data.disciplines.find((d) => d.id === disciplineId);
21
- const track = data.tracks.find((t) => t.id === trackId);
22
21
  const grade = data.grades.find((g) => g.id === gradeId);
22
+ const track = trackId ? data.tracks.find((t) => t.id === trackId) : null;
23
23
 
24
- if (!discipline || !track || !grade) {
24
+ if (!discipline || !grade) {
25
25
  renderError({
26
26
  title: "Job Not Found",
27
- message: "Invalid job combination. One or more components are missing.",
27
+ message: "Invalid job combination. Discipline or grade not found.",
28
+ backPath: "/job-builder",
29
+ backText: "← Back to Job Builder",
30
+ });
31
+ return;
32
+ }
33
+
34
+ // If trackId was provided but not found, error
35
+ if (trackId && !track) {
36
+ renderError({
37
+ title: "Job Not Found",
38
+ message: `Track "${trackId}" not found.`,
28
39
  backPath: "/job-builder",
29
40
  backText: "← Back to Job Builder",
30
41
  });
@@ -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",