@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
@@ -26,7 +26,10 @@ import {
26
26
  deriveAgentSkills,
27
27
  deriveReferenceGrade,
28
28
  } from "../model/agent.js";
29
- import { createSelectWithValue } from "../lib/form-controls.js";
29
+ import {
30
+ createSelectWithValue,
31
+ createDisciplineSelect,
32
+ } from "../lib/form-controls.js";
30
33
  import { createReactive } from "../lib/reactive.js";
31
34
  import { getStageEmoji } from "../formatters/stage/shared.js";
32
35
  import { formatAgentProfile } from "../formatters/agent/profile.js";
@@ -38,6 +41,9 @@ const ALL_STAGES_VALUE = "all";
38
41
  /** @type {Object|null} Cached agent data */
39
42
  let agentDataCache = null;
40
43
 
44
+ /** @type {{agent: string, skill: string}|null} Cached templates */
45
+ let templateCache = null;
46
+
41
47
  /**
42
48
  * Load agent data with caching
43
49
  * @param {string} dataDir - Data directory path
@@ -50,6 +56,24 @@ async function getAgentData(dataDir = "./data") {
50
56
  return agentDataCache;
51
57
  }
52
58
 
59
+ /**
60
+ * Load templates with caching
61
+ * @returns {Promise<{agent: string, skill: string}>}
62
+ */
63
+ async function getTemplates() {
64
+ if (!templateCache) {
65
+ const [agentRes, skillRes] = await Promise.all([
66
+ fetch("./templates/agent.template.md"),
67
+ fetch("./templates/skill.template.md"),
68
+ ]);
69
+ templateCache = {
70
+ agent: await agentRes.text(),
71
+ skill: await skillRes.text(),
72
+ };
73
+ }
74
+ return templateCache;
75
+ }
76
+
53
77
  /**
54
78
  * Render agent builder page
55
79
  */
@@ -64,8 +88,11 @@ export async function renderAgentBuilder() {
64
88
  ),
65
89
  );
66
90
 
67
- // Load agent-specific data
68
- const agentData = await getAgentData();
91
+ // Load agent-specific data and templates
92
+ const [agentData, templates] = await Promise.all([
93
+ getAgentData(),
94
+ getTemplates(),
95
+ ]);
69
96
 
70
97
  // Filter to only disciplines/tracks that have agent definitions
71
98
  const agentDisciplineIds = new Set(agentData.disciplines.map((d) => d.id));
@@ -87,10 +114,13 @@ export async function renderAgentBuilder() {
87
114
  ];
88
115
 
89
116
  // Parse URL params for pre-selection
117
+ // Supports: /agent/discipline, /agent/discipline/track, /agent/discipline/track/stage
90
118
  const hash = window.location.hash;
91
- const pathMatch = hash.match(/#\/agent\/([^/]+)\/([^/]+)(?:\/([^/?]+))?/);
119
+ const pathMatch = hash.match(
120
+ /#\/agent\/([^/]+)(?:\/([^/]+))?(?:\/([^/?]+))?/,
121
+ );
92
122
  const initialDiscipline = pathMatch ? pathMatch[1] : "";
93
- const initialTrack = pathMatch ? pathMatch[2] : "";
123
+ const initialTrack = pathMatch && pathMatch[2] ? pathMatch[2] : "";
94
124
  const initialStage =
95
125
  pathMatch && pathMatch[3] ? pathMatch[3] : ALL_STAGES_VALUE;
96
126
 
@@ -113,9 +143,10 @@ export async function renderAgentBuilder() {
113
143
  */
114
144
  function updatePreview({ discipline, track, stage }) {
115
145
  // Update URL without triggering navigation
116
- if (discipline && track) {
146
+ if (discipline) {
147
+ const trackPart = track ? `/${track}` : "";
117
148
  const stagePart = stage && stage !== ALL_STAGES_VALUE ? `/${stage}` : "";
118
- const newHash = `#/agent/${discipline}/${track}${stagePart}`;
149
+ const newHash = `#/agent/${discipline}${trackPart}${stagePart}`;
119
150
  if (window.location.hash !== newHash) {
120
151
  history.replaceState(null, "", newHash);
121
152
  }
@@ -123,7 +154,7 @@ export async function renderAgentBuilder() {
123
154
 
124
155
  previewContainer.innerHTML = "";
125
156
 
126
- if (!discipline || !track) {
157
+ if (!discipline) {
127
158
  previewContainer.appendChild(
128
159
  createEmptyState(availableDisciplines.length, availableTracks.length),
129
160
  );
@@ -132,7 +163,7 @@ export async function renderAgentBuilder() {
132
163
 
133
164
  // Get full objects
134
165
  const humanDiscipline = data.disciplines.find((d) => d.id === discipline);
135
- const humanTrack = data.tracks.find((t) => t.id === track);
166
+ const humanTrack = track ? data.tracks.find((t) => t.id === track) : null;
136
167
  const agentDiscipline = agentData.disciplines.find(
137
168
  (d) => d.id === discipline,
138
169
  );
@@ -164,6 +195,8 @@ export async function renderAgentBuilder() {
164
195
  agentBehaviours: agentData.behaviours,
165
196
  capabilities: data.capabilities,
166
197
  vscodeSettings: agentData.vscodeSettings,
198
+ devcontainer: agentData.devcontainer,
199
+ templates,
167
200
  };
168
201
 
169
202
  // Generate preview based on stage selection
@@ -212,9 +245,9 @@ export async function renderAgentBuilder() {
212
245
  { className: "form-group" },
213
246
  label({ className: "form-label" }, "Discipline"),
214
247
  availableDisciplines.length > 0
215
- ? createSelectWithValue({
248
+ ? createDisciplineSelect({
216
249
  id: "agent-discipline-select",
217
- items: availableDisciplines,
250
+ disciplines: availableDisciplines,
218
251
  initialValue: selection.get().discipline,
219
252
  placeholder: "Select a discipline...",
220
253
  onChange: (value) => {
@@ -325,6 +358,8 @@ function createAllStagesPreview(context) {
325
358
  agentBehaviours,
326
359
  capabilities,
327
360
  vscodeSettings,
361
+ devcontainer,
362
+ templates,
328
363
  } = context;
329
364
 
330
365
  // Generate all stage agents
@@ -378,7 +413,13 @@ function createAllStagesPreview(context) {
378
413
  { className: "agent-deployment" },
379
414
 
380
415
  // Download all button
381
- createDownloadAllButton(stageAgents, skillFiles, vscodeSettings, context),
416
+ createDownloadAllButton(
417
+ stageAgents,
418
+ skillFiles,
419
+ vscodeSettings,
420
+ devcontainer,
421
+ context,
422
+ ),
382
423
 
383
424
  // Agents section
384
425
  section(
@@ -391,7 +432,7 @@ function createAllStagesPreview(context) {
391
432
  div(
392
433
  { className: "agent-cards-grid" },
393
434
  ...stageAgents.map(({ stage, profile }) =>
394
- createAgentCard(stage, profile, stages),
435
+ createAgentCard(stage, profile, stages, templates.agent),
395
436
  ),
396
437
  ),
397
438
  ),
@@ -403,7 +444,9 @@ function createAllStagesPreview(context) {
403
444
  skillFiles.length > 0
404
445
  ? div(
405
446
  { className: "skill-cards-grid" },
406
- ...skillFiles.map((skill) => createSkillCard(skill)),
447
+ ...skillFiles.map((skill) =>
448
+ createSkillCard(skill, templates.skill),
449
+ ),
407
450
  )
408
451
  : p(
409
452
  { className: "text-muted" },
@@ -434,7 +477,9 @@ function createSingleStagePreview(context, stage) {
434
477
  agentBehaviours,
435
478
  capabilities,
436
479
  vscodeSettings,
480
+ devcontainer,
437
481
  stages,
482
+ templates,
438
483
  } = context;
439
484
 
440
485
  // Derive stage agent
@@ -483,7 +528,13 @@ function createSingleStagePreview(context, stage) {
483
528
  { className: "agent-deployment" },
484
529
 
485
530
  // Download button for single stage
486
- createDownloadSingleButton(profile, skillFiles, vscodeSettings),
531
+ createDownloadSingleButton(
532
+ profile,
533
+ skillFiles,
534
+ vscodeSettings,
535
+ devcontainer,
536
+ templates,
537
+ ),
487
538
 
488
539
  // Agents section (single card)
489
540
  section(
@@ -491,7 +542,7 @@ function createSingleStagePreview(context, stage) {
491
542
  h2({}, "Agent"),
492
543
  div(
493
544
  { className: "agent-cards-grid single" },
494
- createAgentCard(stage, profile, stages, derived),
545
+ createAgentCard(stage, profile, stages, templates.agent, derived),
495
546
  ),
496
547
  ),
497
548
 
@@ -502,7 +553,9 @@ function createSingleStagePreview(context, stage) {
502
553
  skillFiles.length > 0
503
554
  ? div(
504
555
  { className: "skill-cards-grid" },
505
- ...skillFiles.map((skill) => createSkillCard(skill)),
556
+ ...skillFiles.map((skill) =>
557
+ createSkillCard(skill, templates.skill),
558
+ ),
506
559
  )
507
560
  : p(
508
561
  { className: "text-muted" },
@@ -520,11 +573,12 @@ function createSingleStagePreview(context, stage) {
520
573
  * @param {Object} stage - Stage object
521
574
  * @param {Object} profile - Generated profile
522
575
  * @param {Array} stages - All stages for emoji lookup
576
+ * @param {string} agentTemplate - Mustache template for agent profile
523
577
  * @param {Object} [_derived] - Optional derived agent data for extra info
524
578
  * @returns {HTMLElement}
525
579
  */
526
- function createAgentCard(stage, profile, stages, _derived) {
527
- const content = formatAgentProfile(profile);
580
+ function createAgentCard(stage, profile, stages, agentTemplate, _derived) {
581
+ const content = formatAgentProfile(profile, agentTemplate);
528
582
  const stageEmoji = getStageEmoji(stages, stage.id);
529
583
 
530
584
  const card = div(
@@ -548,10 +602,11 @@ function createAgentCard(stage, profile, stages, _derived) {
548
602
  /**
549
603
  * Create a skill card
550
604
  * @param {Object} skill - Skill with frontmatter and body
605
+ * @param {string} skillTemplate - Mustache template for skill
551
606
  * @returns {HTMLElement}
552
607
  */
553
- function createSkillCard(skill) {
554
- const content = formatAgentSkill(skill);
608
+ function createSkillCard(skill, skillTemplate) {
609
+ const content = formatAgentSkill(skill, skillTemplate);
555
610
  const filename = `${skill.dirname}/SKILL.md`;
556
611
 
557
612
  return div(
@@ -615,16 +670,18 @@ function createCopyButton(content) {
615
670
  * @param {Array} stageAgents - Array of {stage, derived, profile}
616
671
  * @param {Array} skillFiles - Array of skill file objects
617
672
  * @param {Object} vscodeSettings - VS Code settings
618
- * @param {Object} context - Context with discipline/track info
673
+ * @param {Object} devcontainer - Devcontainer config
674
+ * @param {Object} context - Context with discipline/track info and templates
619
675
  * @returns {HTMLElement}
620
676
  */
621
677
  function createDownloadAllButton(
622
678
  stageAgents,
623
679
  skillFiles,
624
680
  vscodeSettings,
681
+ devcontainer,
625
682
  context,
626
683
  ) {
627
- const { humanDiscipline, humanTrack } = context;
684
+ const { humanDiscipline, humanTrack, templates } = context;
628
685
  const agentName = `${humanDiscipline.id}-${humanTrack.id}`.replace(/_/g, "-");
629
686
 
630
687
  const btn = document.createElement("button");
@@ -641,13 +698,13 @@ function createDownloadAllButton(
641
698
 
642
699
  // Add all stage agent profiles
643
700
  for (const { profile } of stageAgents) {
644
- const content = formatAgentProfile(profile);
701
+ const content = formatAgentProfile(profile, templates.agent);
645
702
  zip.file(`.github/agents/${profile.filename}`, content);
646
703
  }
647
704
 
648
705
  // Add skills
649
706
  for (const skill of skillFiles) {
650
- const content = formatAgentSkill(skill);
707
+ const content = formatAgentSkill(skill, templates.skill);
651
708
  zip.file(`.claude/skills/${skill.dirname}/SKILL.md`, content);
652
709
  }
653
710
 
@@ -659,6 +716,22 @@ function createDownloadAllButton(
659
716
  );
660
717
  }
661
718
 
719
+ // Add devcontainer.json with VS Code settings embedded
720
+ if (devcontainer && Object.keys(devcontainer).length > 0) {
721
+ const devcontainerJson = {
722
+ ...devcontainer,
723
+ customizations: {
724
+ vscode: {
725
+ settings: vscodeSettings,
726
+ },
727
+ },
728
+ };
729
+ zip.file(
730
+ ".devcontainer/devcontainer.json",
731
+ JSON.stringify(devcontainerJson, null, 2) + "\n",
732
+ );
733
+ }
734
+
662
735
  // Generate and download
663
736
  const blob = await zip.generateAsync({ type: "blob" });
664
737
  const url = URL.createObjectURL(blob);
@@ -685,9 +758,17 @@ function createDownloadAllButton(
685
758
  * @param {Object} profile - Agent profile
686
759
  * @param {Array} skillFiles - Skill files
687
760
  * @param {Object} vscodeSettings - VS Code settings
761
+ * @param {Object} devcontainer - Devcontainer config
762
+ * @param {{agent: string, skill: string}} templates - Mustache templates
688
763
  * @returns {HTMLElement}
689
764
  */
690
- function createDownloadSingleButton(profile, skillFiles, vscodeSettings) {
765
+ function createDownloadSingleButton(
766
+ profile,
767
+ skillFiles,
768
+ vscodeSettings,
769
+ devcontainer,
770
+ templates,
771
+ ) {
691
772
  const btn = document.createElement("button");
692
773
  btn.className = "btn btn-primary download-all-btn";
693
774
  btn.textContent = "📥 Download Agent (.zip)";
@@ -701,12 +782,12 @@ function createDownloadSingleButton(profile, skillFiles, vscodeSettings) {
701
782
  const zip = new JSZip();
702
783
 
703
784
  // Add profile
704
- const content = formatAgentProfile(profile);
785
+ const content = formatAgentProfile(profile, templates.agent);
705
786
  zip.file(`.github/agents/${profile.filename}`, content);
706
787
 
707
788
  // Add skills
708
789
  for (const skill of skillFiles) {
709
- const skillContent = formatAgentSkill(skill);
790
+ const skillContent = formatAgentSkill(skill, templates.skill);
710
791
  zip.file(`.claude/skills/${skill.dirname}/SKILL.md`, skillContent);
711
792
  }
712
793
 
@@ -718,6 +799,22 @@ function createDownloadSingleButton(profile, skillFiles, vscodeSettings) {
718
799
  );
719
800
  }
720
801
 
802
+ // Add devcontainer.json with VS Code settings embedded
803
+ if (devcontainer && Object.keys(devcontainer).length > 0) {
804
+ const devcontainerJson = {
805
+ ...devcontainer,
806
+ customizations: {
807
+ vscode: {
808
+ settings: vscodeSettings,
809
+ },
810
+ },
811
+ };
812
+ zip.file(
813
+ ".devcontainer/devcontainer.json",
814
+ JSON.stringify(devcontainerJson, null, 2) + "\n",
815
+ );
816
+ }
817
+
721
818
  // Generate and download
722
819
  const blob = await zip.generateAsync({ type: "blob" });
723
820
  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
@@ -2,29 +2,55 @@
2
2
  * Job detail page with visualizations
3
3
  */
4
4
 
5
- import { render } from "../lib/render.js";
5
+ import { render, div, p } from "../lib/render.js";
6
6
  import { getState } from "../lib/state.js";
7
7
  import { renderError } from "../components/error-page.js";
8
8
  import { prepareJobDetail } from "../model/job.js";
9
9
  import { jobToDOM } from "../formatters/job/dom.js";
10
10
 
11
+ /** @type {string|null} Cached job template */
12
+ let jobTemplateCache = null;
13
+
14
+ /**
15
+ * Load job template with caching
16
+ * @returns {Promise<string>}
17
+ */
18
+ async function getJobTemplate() {
19
+ if (!jobTemplateCache) {
20
+ const response = await fetch("./templates/job.template.md");
21
+ jobTemplateCache = await response.text();
22
+ }
23
+ return jobTemplateCache;
24
+ }
25
+
11
26
  /**
12
27
  * Render job detail page
13
28
  * @param {Object} params - Route params
14
29
  */
15
- export function renderJobDetail(params) {
16
- const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
30
+ export async function renderJobDetail(params) {
31
+ const { discipline: disciplineId, grade: gradeId, track: trackId } = params;
17
32
  const { data } = getState();
18
33
 
19
34
  // Find the components
20
35
  const discipline = data.disciplines.find((d) => d.id === disciplineId);
21
- const track = data.tracks.find((t) => t.id === trackId);
22
36
  const grade = data.grades.find((g) => g.id === gradeId);
37
+ const track = trackId ? data.tracks.find((t) => t.id === trackId) : null;
23
38
 
24
- if (!discipline || !track || !grade) {
39
+ if (!discipline || !grade) {
25
40
  renderError({
26
41
  title: "Job Not Found",
27
- message: "Invalid job combination. One or more components are missing.",
42
+ message: "Invalid job combination. Discipline or grade not found.",
43
+ backPath: "/job-builder",
44
+ backText: "← Back to Job Builder",
45
+ });
46
+ return;
47
+ }
48
+
49
+ // If trackId was provided but not found, error
50
+ if (trackId && !track) {
51
+ renderError({
52
+ title: "Job Not Found",
53
+ message: `Track "${trackId}" not found.`,
28
54
  backPath: "/job-builder",
29
55
  backText: "← Back to Job Builder",
30
56
  });
@@ -52,7 +78,16 @@ export function renderJobDetail(params) {
52
78
  return;
53
79
  }
54
80
 
55
- // Format using DOM formatter
56
- const page = jobToDOM(jobView, { discipline, grade, track });
81
+ // Show loading while fetching template
82
+ render(
83
+ div(
84
+ { className: "job-detail-page" },
85
+ div({ className: "loading" }, p({}, "Loading...")),
86
+ ),
87
+ );
88
+
89
+ // Load template and format
90
+ const jobTemplate = await getJobTemplate();
91
+ const page = jobToDOM(jobView, { discipline, grade, track, jobTemplate });
57
92
  render(page);
58
93
  }