@forwardimpact/pathway 0.1.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 (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. package/package.json +68 -0
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Drivers pages
3
+ */
4
+
5
+ import { render, div, h1, h2, p, section } from "../lib/render.js";
6
+ import { getState } from "../lib/state.js";
7
+ import { createCardList } from "../components/list.js";
8
+ import { createDetailHeader, createLinksList } from "../components/detail.js";
9
+ import { renderNotFound } from "../components/error-page.js";
10
+ import {
11
+ prepareDriversList,
12
+ prepareDriverDetail,
13
+ } from "../formatters/driver/shared.js";
14
+ import { driverToCardConfig } from "../lib/card-mappers.js";
15
+
16
+ /**
17
+ * Render drivers list page
18
+ */
19
+ export function renderDriversList() {
20
+ const { data } = getState();
21
+ const { framework } = data;
22
+
23
+ // Transform data for list view
24
+ const { items } = prepareDriversList(data.drivers);
25
+
26
+ const page = div(
27
+ { className: "drivers-page" },
28
+ // Header
29
+ div(
30
+ { className: "page-header" },
31
+ h1({ className: "page-title" }, framework.entityDefinitions.driver.title),
32
+ p(
33
+ { className: "page-description" },
34
+ framework.entityDefinitions.driver.description.trim(),
35
+ ),
36
+ ),
37
+
38
+ // Drivers list
39
+ createCardList(items, driverToCardConfig, "No drivers found."),
40
+ );
41
+
42
+ render(page);
43
+ }
44
+
45
+ /**
46
+ * Render driver detail page
47
+ * @param {Object} params - Route params
48
+ */
49
+ export function renderDriverDetail(params) {
50
+ const { data } = getState();
51
+ const driver = data.drivers.find((d) => d.id === params.id);
52
+
53
+ if (!driver) {
54
+ renderNotFound({
55
+ entityType: "Driver",
56
+ entityId: params.id,
57
+ backPath: "/driver",
58
+ backText: "← Back to Drivers",
59
+ });
60
+ return;
61
+ }
62
+
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",
76
+ }),
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
+ );
104
+
105
+ render(page);
106
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Grades pages
3
+ */
4
+
5
+ import { render, div, h1, h3, p, formatLevel } from "../lib/render.js";
6
+ import { getState } from "../lib/state.js";
7
+ import { createBadge } from "../components/card.js";
8
+ import { renderNotFound } from "../components/error-page.js";
9
+ import { prepareGradesList } from "../formatters/grade/shared.js";
10
+ import { gradeToDOM } from "../formatters/grade/dom.js";
11
+
12
+ /**
13
+ * Render grades list page
14
+ */
15
+ export function renderGradesList() {
16
+ const { data } = getState();
17
+ const { framework } = data;
18
+
19
+ // Transform data for list view
20
+ const { items } = prepareGradesList(data.grades);
21
+
22
+ const page = div(
23
+ { className: "grades-page" },
24
+ // Header
25
+ div(
26
+ { className: "page-header" },
27
+ h1({ className: "page-title" }, framework.entityDefinitions.grade.title),
28
+ p(
29
+ { className: "page-description" },
30
+ framework.entityDefinitions.grade.description.trim(),
31
+ ),
32
+ ),
33
+
34
+ // Grades timeline
35
+ div(
36
+ { className: "grades-timeline" },
37
+ ...items.map((grade) => createGradeTimelineItem(grade)),
38
+ ),
39
+ );
40
+
41
+ render(page);
42
+ }
43
+
44
+ /**
45
+ * Create a grade timeline item
46
+ * @param {Object} grade
47
+ * @returns {HTMLElement}
48
+ */
49
+ function createGradeTimelineItem(grade) {
50
+ const item = div(
51
+ { className: "grade-timeline-item" },
52
+ div({ className: "grade-level-marker" }, String(grade.ordinalRank)),
53
+ div(
54
+ { className: "grade-timeline-content card card-clickable" },
55
+ div(
56
+ { className: "card-header" },
57
+ h3({ className: "card-title" }, grade.displayName),
58
+ createBadge(grade.id, "default"),
59
+ ),
60
+ grade.typicalExperienceRange
61
+ ? p(
62
+ { className: "text-muted", style: "margin: 0.25rem 0" },
63
+ `${grade.typicalExperienceRange} experience`,
64
+ )
65
+ : null,
66
+ div(
67
+ { className: "card-meta", style: "margin-top: 0.5rem" },
68
+ createBadge(
69
+ `Primary: ${formatLevel(grade.baseSkillLevels?.primary)}`,
70
+ "primary",
71
+ ),
72
+ createBadge(
73
+ `Secondary: ${formatLevel(grade.baseSkillLevels?.secondary)}`,
74
+ "secondary",
75
+ ),
76
+ createBadge(
77
+ `Broad: ${formatLevel(grade.baseSkillLevels?.broad)}`,
78
+ "broad",
79
+ ),
80
+ ),
81
+ grade.scope
82
+ ? p(
83
+ { className: "card-description", style: "margin-top: 0.75rem" },
84
+ `Scope: ${grade.scope}`,
85
+ )
86
+ : null,
87
+ ),
88
+ );
89
+
90
+ item.querySelector(".card").addEventListener("click", () => {
91
+ window.location.hash = `/grade/${grade.id}`;
92
+ });
93
+
94
+ return item;
95
+ }
96
+
97
+ /**
98
+ * Render grade detail page
99
+ * @param {Object} params - Route params
100
+ */
101
+ export function renderGradeDetail(params) {
102
+ const { data } = getState();
103
+ const grade = data.grades.find((g) => g.id === params.id);
104
+
105
+ if (!grade) {
106
+ renderNotFound({
107
+ entityType: "Grade",
108
+ entityId: params.id,
109
+ backPath: "/grade",
110
+ backText: "← Back to Grades",
111
+ });
112
+ return;
113
+ }
114
+
115
+ // Use DOM formatter - it handles transformation internally
116
+ render(gradeToDOM(grade, { framework: data.framework }));
117
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Interview prep page
3
+ */
4
+
5
+ import { render } from "../lib/render.js";
6
+ import { getState } from "../lib/state.js";
7
+ import { createBuilder, createStandardPreview } from "../components/builder.js";
8
+ import { prepareInterviewBuilderPreview } from "../formatters/interview/shared.js";
9
+
10
+ /**
11
+ * Render interview prep page
12
+ */
13
+ export function renderInterviewPrep() {
14
+ const { data } = getState();
15
+
16
+ render(
17
+ createBuilder({
18
+ title: "Interview Prep",
19
+ description:
20
+ "Select a discipline, track, and grade to generate tailored interview questions " +
21
+ "based on the role's skill requirements and expected behaviours.",
22
+ formTitle: "Select Role",
23
+ emptyPreviewText: "Select all three components to preview the interview.",
24
+ buttonText: "View Interview Questions →",
25
+ previewPresenter: (selection) =>
26
+ prepareInterviewBuilderPreview({
27
+ ...selection,
28
+ behaviourCount: data.behaviours.length,
29
+ grades: data.grades,
30
+ }),
31
+ detailPath: (sel) =>
32
+ `/interview/${sel.discipline}/${sel.track}/${sel.grade}`,
33
+ renderPreview: createStandardPreview,
34
+ helpItems: [
35
+ {
36
+ label: "Role Selection",
37
+ text: "Choose a discipline, track, and grade to define the target role for the interview.",
38
+ },
39
+ {
40
+ label: "Skill Questions",
41
+ text: "Questions are generated based on the required skill levels for the role.",
42
+ },
43
+ {
44
+ label: "Behaviour Questions",
45
+ text: "Behavioural questions assess mindsets and ways of working at the expected maturity.",
46
+ },
47
+ ],
48
+ }),
49
+ );
50
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Interview detail page with interview questions
3
+ */
4
+
5
+ import {
6
+ render,
7
+ div,
8
+ h1,
9
+ h2,
10
+ h4,
11
+ p,
12
+ a,
13
+ button,
14
+ span,
15
+ ul,
16
+ li,
17
+ formatLevel,
18
+ } from "../lib/render.js";
19
+ import { getState } from "../lib/state.js";
20
+ import { createBadge } from "../components/card.js";
21
+ import { createBackLink } from "../components/nav.js";
22
+ import { createDetailSection } from "../components/detail.js";
23
+ import { renderError } from "../components/error-page.js";
24
+ import { getConceptEmoji } from "../model/levels.js";
25
+ import {
26
+ prepareAllInterviews,
27
+ INTERVIEW_TYPES,
28
+ } from "../formatters/interview/shared.js";
29
+
30
+ /**
31
+ * Render interview detail page
32
+ * @param {Object} params - Route params
33
+ */
34
+ export function renderInterviewDetail(params) {
35
+ const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
36
+ const { data } = getState();
37
+
38
+ // Find the components
39
+ const discipline = data.disciplines.find((d) => d.id === disciplineId);
40
+ const track = data.tracks.find((t) => t.id === trackId);
41
+ const grade = data.grades.find((g) => g.id === gradeId);
42
+
43
+ if (!discipline || !track || !grade) {
44
+ renderError({
45
+ title: "Interview Not Found",
46
+ message: "Invalid combination. One or more components are missing.",
47
+ backPath: "/interview-prep",
48
+ backText: "← Back to Interview Prep",
49
+ });
50
+ return;
51
+ }
52
+
53
+ // Use formatter shared module to get all interview types
54
+ const interviewsView = prepareAllInterviews({
55
+ discipline,
56
+ grade,
57
+ track,
58
+ skills: data.skills,
59
+ behaviours: data.behaviours,
60
+ questions: data.questions,
61
+ });
62
+
63
+ if (!interviewsView) {
64
+ renderError({
65
+ title: "Invalid Combination",
66
+ message: "This discipline, track, and grade combination is not valid.",
67
+ backPath: "/interview-prep",
68
+ backText: "← Back to Interview Prep",
69
+ });
70
+ return;
71
+ }
72
+
73
+ // State for current interview type (default to first: Screening)
74
+ let currentType = "short";
75
+
76
+ const page = div(
77
+ { className: "interview-detail-page" },
78
+ // Header
79
+ div(
80
+ { className: "page-header" },
81
+ createBackLink("/interview-prep", "← Back to Interview Prep"),
82
+ h1({ className: "page-title" }, `Interview: ${interviewsView.title}`),
83
+ div(
84
+ { className: "page-description" },
85
+ "Interview questions for: ",
86
+ a(
87
+ { href: `#/discipline/${interviewsView.disciplineId}` },
88
+ interviewsView.disciplineName,
89
+ ),
90
+ " × ",
91
+ a(
92
+ { href: `#/grade/${interviewsView.gradeId}` },
93
+ interviewsView.gradeId,
94
+ ),
95
+ " × ",
96
+ a(
97
+ { href: `#/track/${interviewsView.trackId}` },
98
+ interviewsView.trackName,
99
+ ),
100
+ ),
101
+ ),
102
+
103
+ // Interview type toggle
104
+ div(
105
+ { className: "interview-type-toggle", id: "interview-type-toggle" },
106
+ ...Object.values(INTERVIEW_TYPES).map((type) =>
107
+ createTypeButton(type, type.id === currentType, () => {
108
+ currentType = type.id;
109
+ updateTypeToggle();
110
+ updateQuestions();
111
+ }),
112
+ ),
113
+ ),
114
+
115
+ // Questions container
116
+ div({ id: "interview-questions-container" }),
117
+ );
118
+
119
+ render(page);
120
+
121
+ // Generate and display initial questions
122
+ updateQuestions();
123
+
124
+ function updateTypeToggle() {
125
+ const toggleEl = document.getElementById("interview-type-toggle");
126
+ toggleEl.innerHTML = "";
127
+ Object.values(INTERVIEW_TYPES).forEach((type) => {
128
+ toggleEl.appendChild(
129
+ createTypeButton(type, type.id === currentType, () => {
130
+ currentType = type.id;
131
+ updateTypeToggle();
132
+ updateQuestions();
133
+ }),
134
+ );
135
+ });
136
+ }
137
+
138
+ function updateQuestions() {
139
+ const container = document.getElementById("interview-questions-container");
140
+ container.innerHTML = "";
141
+
142
+ // Get interview data from presenter
143
+ const interview = interviewsView.interviews[currentType];
144
+
145
+ container.appendChild(
146
+ div(
147
+ {},
148
+ // Interview summary
149
+ createInterviewSummary(interview),
150
+
151
+ // Questions sections
152
+ createQuestionsDisplay(interview, data.framework),
153
+ ),
154
+ );
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Create type button
160
+ */
161
+ function createTypeButton(type, isActive, onClick) {
162
+ const btn = button(
163
+ {
164
+ className: `interview-type-btn ${isActive ? "active" : ""}`,
165
+ },
166
+ span({ className: "interview-type-icon" }, type.icon),
167
+ span({ className: "interview-type-name" }, type.name),
168
+ );
169
+
170
+ btn.addEventListener("click", onClick);
171
+ return btn;
172
+ }
173
+
174
+ /**
175
+ * Create interview summary
176
+ */
177
+ function createInterviewSummary(interview) {
178
+ const typeInfo = interview.typeInfo;
179
+
180
+ return div(
181
+ { className: "interview-summary card" },
182
+ div(
183
+ { className: "interview-summary-header" },
184
+ h2({}, `${typeInfo.icon} ${typeInfo.name}`),
185
+ p({ className: "text-muted" }, typeInfo.description),
186
+ ),
187
+ div(
188
+ { className: "interview-summary-stats" },
189
+ createBadge(`${interview.questions.length} questions`, "default"),
190
+ createBadge(`~${interview.expectedDurationMinutes} minutes`, "secondary"),
191
+ interview.coverage.skills.length > 0
192
+ ? createBadge(
193
+ `${interview.coverage.skills.length} skills covered`,
194
+ "primary",
195
+ )
196
+ : null,
197
+ interview.coverage.behaviours.length > 0
198
+ ? createBadge(
199
+ `${interview.coverage.behaviours.length} behaviours covered`,
200
+ "primary",
201
+ )
202
+ : null,
203
+ ),
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Create questions display
209
+ */
210
+ function createQuestionsDisplay(interview, framework) {
211
+ // Group questions by type
212
+ const skillQuestions = interview.questions.filter(
213
+ (q) => q.targetType === "skill",
214
+ );
215
+ const behaviourQuestions = interview.questions.filter(
216
+ (q) => q.targetType === "behaviour",
217
+ );
218
+
219
+ const sections = [];
220
+
221
+ if (skillQuestions.length > 0) {
222
+ sections.push(
223
+ createDetailSection({
224
+ title: `${getConceptEmoji(framework, "skill")} Skill Questions (${skillQuestions.length})`,
225
+ content: createQuestionsList(skillQuestions),
226
+ }),
227
+ );
228
+ }
229
+
230
+ if (behaviourQuestions.length > 0) {
231
+ sections.push(
232
+ createDetailSection({
233
+ title: `${getConceptEmoji(framework, "behaviour")} Behaviour Questions (${behaviourQuestions.length})`,
234
+ content: createQuestionsList(behaviourQuestions),
235
+ }),
236
+ );
237
+ }
238
+
239
+ if (sections.length === 0) {
240
+ return div(
241
+ { className: "card" },
242
+ p(
243
+ { className: "text-muted" },
244
+ "No questions available for this combination. Please check that question data exists for the required skills and behaviours.",
245
+ ),
246
+ );
247
+ }
248
+
249
+ return div({}, ...sections);
250
+ }
251
+
252
+ /**
253
+ * Create questions list
254
+ */
255
+ function createQuestionsList(questions) {
256
+ return div(
257
+ { className: "questions-list" },
258
+ ...questions.map((q, index) => createQuestionCard(q, index + 1)),
259
+ );
260
+ }
261
+
262
+ /**
263
+ * Create question card
264
+ */
265
+ function createQuestionCard(questionEntry, number) {
266
+ const { question, targetName, targetLevel } = questionEntry;
267
+
268
+ const followUpsList =
269
+ question.followUps && question.followUps.length > 0
270
+ ? div(
271
+ { className: "question-followups" },
272
+ h4({}, "Follow-up questions:"),
273
+ ul({}, ...question.followUps.map((fu) => li({}, fu))),
274
+ )
275
+ : null;
276
+
277
+ const lookingForList =
278
+ question.lookingFor && question.lookingFor.length > 0
279
+ ? div(
280
+ { className: "question-looking-for" },
281
+ h4({}, "What to look for:"),
282
+ ul({}, ...question.lookingFor.map((lf) => li({}, lf))),
283
+ )
284
+ : null;
285
+
286
+ return div(
287
+ { className: "question-card" },
288
+ div(
289
+ { className: "question-header" },
290
+ span({ className: "question-number" }, `Q${number}`),
291
+ div(
292
+ { className: "question-meta" },
293
+ createBadge(targetName, "default"),
294
+ createBadge(formatLevel(targetLevel), "secondary"),
295
+ question.expectedDurationMinutes
296
+ ? createBadge(`~${question.expectedDurationMinutes} min`, "secondary")
297
+ : null,
298
+ ),
299
+ ),
300
+ div({ className: "question-text" }, question.text),
301
+ followUpsList,
302
+ lookingForList,
303
+ );
304
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Job builder page
3
+ */
4
+
5
+ import { render } from "../lib/render.js";
6
+ import { getState } from "../lib/state.js";
7
+ import { createBuilder, createStandardPreview } from "../components/builder.js";
8
+ import { prepareJobBuilderPreview } from "../model/job.js";
9
+
10
+ /**
11
+ * Render job builder page
12
+ */
13
+ export function renderJobBuilder() {
14
+ const { data } = getState();
15
+
16
+ render(
17
+ createBuilder({
18
+ title: "Job Builder",
19
+ description:
20
+ "Combine a discipline, track, and grade to generate a complete job definition " +
21
+ "with skill matrix and behaviour profile.",
22
+ formTitle: "Select Components",
23
+ emptyPreviewText:
24
+ "Select all three components to preview the job definition.",
25
+ buttonText: "View Full Job Definition →",
26
+ previewPresenter: (selection) =>
27
+ prepareJobBuilderPreview({
28
+ ...selection,
29
+ behaviourCount: data.behaviours.length,
30
+ grades: data.grades,
31
+ }),
32
+ detailPath: (sel) => `/job/${sel.discipline}/${sel.track}/${sel.grade}`,
33
+ renderPreview: createStandardPreview,
34
+ helpItems: [
35
+ {
36
+ label: "Discipline",
37
+ text: "Defines the T-shaped skill profile with primary, secondary, and broad skills.",
38
+ },
39
+ {
40
+ label: "Grade",
41
+ text: "Sets base skill levels and behaviour maturity expectations for career level.",
42
+ },
43
+ {
44
+ label: "Track",
45
+ text: "Modifies skill and behaviour expectations based on the nature of work.",
46
+ },
47
+ ],
48
+ }),
49
+ );
50
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Job detail page with visualizations
3
+ */
4
+
5
+ import { render } from "../lib/render.js";
6
+ import { getState } from "../lib/state.js";
7
+ import { renderError } from "../components/error-page.js";
8
+ import { prepareJobDetail } from "../model/job.js";
9
+ import { jobToDOM } from "../formatters/job/dom.js";
10
+
11
+ /**
12
+ * Render job detail page
13
+ * @param {Object} params - Route params
14
+ */
15
+ export function renderJobDetail(params) {
16
+ const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
17
+ const { data } = getState();
18
+
19
+ // Find the components
20
+ const discipline = data.disciplines.find((d) => d.id === disciplineId);
21
+ const track = data.tracks.find((t) => t.id === trackId);
22
+ const grade = data.grades.find((g) => g.id === gradeId);
23
+
24
+ if (!discipline || !track || !grade) {
25
+ renderError({
26
+ title: "Job Not Found",
27
+ message: "Invalid job combination. One or more components are missing.",
28
+ backPath: "/job-builder",
29
+ backText: "← Back to Job Builder",
30
+ });
31
+ return;
32
+ }
33
+
34
+ // Use formatter shared module to get job detail view
35
+ const jobView = prepareJobDetail({
36
+ discipline,
37
+ grade,
38
+ track,
39
+ skills: data.skills,
40
+ behaviours: data.behaviours,
41
+ drivers: data.drivers,
42
+ capabilities: data.capabilities,
43
+ });
44
+
45
+ if (!jobView) {
46
+ renderError({
47
+ title: "Invalid Combination",
48
+ message: "This discipline, track, and grade combination is not valid.",
49
+ backPath: "/job-builder",
50
+ backText: "← Back to Job Builder",
51
+ });
52
+ return;
53
+ }
54
+
55
+ // Format using DOM formatter
56
+ const page = jobToDOM(jobView, { discipline, grade, track });
57
+ render(page);
58
+ }