@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,507 @@
1
+ /**
2
+ * Self-assessment results page
3
+ * Displays job matches, gaps, and development recommendations
4
+ */
5
+
6
+ import {
7
+ render,
8
+ div,
9
+ h1,
10
+ h2,
11
+ h3,
12
+ h4,
13
+ p,
14
+ span,
15
+ button,
16
+ a,
17
+ } from "../lib/render.js";
18
+ import { getState } from "../lib/state.js";
19
+ import { createBadge } from "../components/card.js";
20
+ import { formatLevel } from "../lib/render.js";
21
+ import { getAssessmentState, resetAssessment } from "./self-assessment.js";
22
+ import { findRealisticMatches } from "../model/matching.js";
23
+
24
+ /**
25
+ * Render the assessment results page
26
+ */
27
+ export function renderAssessmentResults() {
28
+ const { data } = getState();
29
+ const assessmentState = getAssessmentState();
30
+
31
+ // Check if there's any assessment data
32
+ const hasSkills = Object.keys(assessmentState.skills).length > 0;
33
+ const hasBehaviours = Object.keys(assessmentState.behaviours).length > 0;
34
+
35
+ if (!hasSkills && !hasBehaviours) {
36
+ renderNoAssessment();
37
+ return;
38
+ }
39
+
40
+ // Create self-assessment object in the expected format
41
+ const selfAssessment = {
42
+ id: "current-assessment",
43
+ skills: assessmentState.skills,
44
+ behaviours: assessmentState.behaviours,
45
+ };
46
+
47
+ // Find matching jobs with realistic scoring
48
+ const { matches, matchesByTier, estimatedGrade } = findRealisticMatches({
49
+ selfAssessment,
50
+ disciplines: data.disciplines,
51
+ grades: data.grades,
52
+ tracks: data.tracks,
53
+ skills: data.skills,
54
+ behaviours: data.behaviours,
55
+ filterByGrade: false, // Show all grades but group by tier
56
+ topN: 20,
57
+ });
58
+
59
+ const page = div(
60
+ { className: "assessment-results-page" },
61
+ // Header
62
+ div(
63
+ { className: "page-header" },
64
+ a(
65
+ { href: "#/self-assessment", className: "back-link" },
66
+ "← Back to Assessment",
67
+ ),
68
+ h1({ className: "page-title" }, "Your Job Matches"),
69
+ p(
70
+ { className: "page-description" },
71
+ "Based on your self-assessment, here are the roles that best match your current skills and behaviours.",
72
+ ),
73
+ ),
74
+
75
+ // Summary stats
76
+ createSummaryStats(assessmentState, data, estimatedGrade),
77
+
78
+ // Top matches grouped by tier
79
+ createMatchesSection(matches, matchesByTier, selfAssessment, data),
80
+
81
+ // Actions
82
+ div(
83
+ { className: "results-actions-footer" },
84
+ button(
85
+ {
86
+ className: "btn btn-secondary",
87
+ onClick: () => {
88
+ window.location.hash = "/self-assessment";
89
+ },
90
+ },
91
+ "← Edit Assessment",
92
+ ),
93
+ button(
94
+ {
95
+ className: "btn btn-secondary",
96
+ onClick: () => {
97
+ if (
98
+ confirm(
99
+ "Are you sure you want to start over? This will clear your assessment.",
100
+ )
101
+ ) {
102
+ resetAssessment();
103
+ window.location.hash = "/self-assessment";
104
+ }
105
+ },
106
+ },
107
+ "Start Over",
108
+ ),
109
+ ),
110
+ );
111
+
112
+ render(page);
113
+ }
114
+
115
+ /**
116
+ * Render message when no assessment data exists
117
+ */
118
+ function renderNoAssessment() {
119
+ render(
120
+ div(
121
+ { className: "assessment-results-page" },
122
+ div(
123
+ { className: "no-assessment-message" },
124
+ h1({}, "No Assessment Data"),
125
+ p(
126
+ {},
127
+ "You haven't completed a self-assessment yet. Complete the assessment to see your job matches.",
128
+ ),
129
+ a(
130
+ { href: "#/self-assessment", className: "btn btn-primary btn-lg" },
131
+ "Start Self-Assessment →",
132
+ ),
133
+ ),
134
+ ),
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Create summary statistics section
140
+ * @param {Object} assessmentState - Current assessment state
141
+ * @param {Object} data - App data
142
+ * @param {{grade: Object, confidence: number}} estimatedGrade - Estimated best-fit grade
143
+ * @returns {HTMLElement}
144
+ */
145
+ function createSummaryStats(assessmentState, data, estimatedGrade) {
146
+ const skillCount = Object.keys(assessmentState.skills).length;
147
+ const behaviourCount = Object.keys(assessmentState.behaviours).length;
148
+
149
+ // Calculate average levels
150
+ const avgSkillLevel = calculateAverageLevel(
151
+ Object.values(assessmentState.skills),
152
+ ["awareness", "foundational", "working", "practitioner", "expert"],
153
+ );
154
+
155
+ // Get grade name based on track (default to professional)
156
+ const gradeName =
157
+ estimatedGrade.grade.professionalTitle ||
158
+ estimatedGrade.grade.name ||
159
+ estimatedGrade.grade.id;
160
+ const confidenceLabel =
161
+ estimatedGrade.confidence >= 0.7
162
+ ? "High"
163
+ : estimatedGrade.confidence >= 0.4
164
+ ? "Medium"
165
+ : "Low";
166
+
167
+ return div(
168
+ { className: "results-summary-section" },
169
+ h2({}, "Assessment Summary"),
170
+ div(
171
+ { className: "auto-grid-xs" },
172
+ createStatBox(
173
+ String(skillCount),
174
+ `of ${data.skills.length} Skills`,
175
+ "📊",
176
+ ),
177
+ createStatBox(
178
+ String(behaviourCount),
179
+ `of ${data.behaviours.length} Behaviours`,
180
+ "🧠",
181
+ ),
182
+ createStatBox(formatLevel(avgSkillLevel), "Avg Skill Level", "💡"),
183
+ createStatBox(gradeName, `Estimated Level (${confidenceLabel})`, "🎯"),
184
+ ),
185
+ );
186
+ }
187
+
188
+ /**
189
+ * Create a stat box
190
+ * @param {string} value
191
+ * @param {string} label
192
+ * @param {string} icon
193
+ * @returns {HTMLElement}
194
+ */
195
+ function createStatBox(value, label, icon) {
196
+ return div(
197
+ { className: "result-stat-box" },
198
+ span({ className: "stat-icon" }, icon),
199
+ span({ className: "stat-value" }, value),
200
+ span({ className: "stat-label" }, label),
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Calculate average level from array of levels
206
+ * @param {string[]} levels
207
+ * @param {string[]} levelOrder
208
+ * @returns {string}
209
+ */
210
+ function calculateAverageLevel(levels, levelOrder) {
211
+ if (levels.length === 0) return levelOrder[0];
212
+
213
+ const sum = levels.reduce((acc, level) => {
214
+ const index = levelOrder.indexOf(level);
215
+ return acc + (index >= 0 ? index : 0);
216
+ }, 0);
217
+
218
+ const avgIndex = Math.round(sum / levels.length);
219
+ return levelOrder[Math.min(avgIndex, levelOrder.length - 1)];
220
+ }
221
+
222
+ /**
223
+ * Create the matches section grouped by tier
224
+ * @param {Array} matches - Job matches from findRealisticMatches
225
+ * @param {Object} matchesByTier - Matches grouped by tier
226
+ * @param {Object} selfAssessment - Self-assessment data
227
+ * @param {Object} data - App data
228
+ * @returns {HTMLElement}
229
+ */
230
+ function createMatchesSection(matches, matchesByTier, selfAssessment, data) {
231
+ if (matches.length === 0) {
232
+ return div(
233
+ { className: "no-matches" },
234
+ h2({}, "No Matches Found"),
235
+ p(
236
+ {},
237
+ "We couldn't find any suitable job matches. Try completing more of the assessment.",
238
+ ),
239
+ );
240
+ }
241
+
242
+ // Create tier sections
243
+ const tierSections = [];
244
+
245
+ // Tier 1: Strong Matches
246
+ if (matchesByTier[1].length > 0) {
247
+ tierSections.push(
248
+ createTierSection(
249
+ 1,
250
+ "Strong Matches",
251
+ "green",
252
+ "Ready for these roles now",
253
+ matchesByTier[1],
254
+ selfAssessment,
255
+ data,
256
+ ),
257
+ );
258
+ }
259
+
260
+ // Tier 2: Good Matches
261
+ if (matchesByTier[2].length > 0) {
262
+ tierSections.push(
263
+ createTierSection(
264
+ 2,
265
+ "Good Matches",
266
+ "blue",
267
+ "Ready within 6-12 months of focused growth",
268
+ matchesByTier[2],
269
+ selfAssessment,
270
+ data,
271
+ ),
272
+ );
273
+ }
274
+
275
+ // Tier 3: Stretch Roles
276
+ if (matchesByTier[3].length > 0) {
277
+ tierSections.push(
278
+ createTierSection(
279
+ 3,
280
+ "Stretch Roles",
281
+ "amber",
282
+ "Ambitious but achievable with dedicated development",
283
+ matchesByTier[3],
284
+ selfAssessment,
285
+ data,
286
+ ),
287
+ );
288
+ }
289
+
290
+ // Tier 4: Aspirational (show fewer)
291
+ if (matchesByTier[4].length > 0) {
292
+ tierSections.push(
293
+ createTierSection(
294
+ 4,
295
+ "Aspirational",
296
+ "gray",
297
+ "Long-term career goals requiring significant growth",
298
+ matchesByTier[4].slice(0, 3),
299
+ selfAssessment,
300
+ data,
301
+ ),
302
+ );
303
+ }
304
+
305
+ return div(
306
+ { className: "matches-section" },
307
+ h2({}, "Job Matches by Readiness"),
308
+ p(
309
+ { className: "text-muted" },
310
+ "Jobs are grouped by how ready you are for them based on your current skills and behaviours.",
311
+ ),
312
+ ...tierSections,
313
+ );
314
+ }
315
+
316
+ /**
317
+ * Create a tier section
318
+ * @param {number} tierNum - Tier number (1-4)
319
+ * @param {string} title - Section title
320
+ * @param {string} color - Color class
321
+ * @param {string} description - Tier description
322
+ * @param {Array} matches - Matches in this tier
323
+ * @param {Object} selfAssessment - Self-assessment data
324
+ * @param {Object} data - App data
325
+ * @returns {HTMLElement}
326
+ */
327
+ function createTierSection(
328
+ tierNum,
329
+ title,
330
+ color,
331
+ description,
332
+ matches,
333
+ selfAssessment,
334
+ data,
335
+ ) {
336
+ return div(
337
+ { className: `tier-section tier-${tierNum} tier-color-${color}` },
338
+ div(
339
+ { className: "tier-header" },
340
+ h3({ className: "tier-title" }, title),
341
+ span(
342
+ { className: "tier-count" },
343
+ `${matches.length} role${matches.length !== 1 ? "s" : ""}`,
344
+ ),
345
+ ),
346
+ p({ className: "tier-description" }, description),
347
+ div(
348
+ { className: "matches-list" },
349
+ ...matches.map((match, index) =>
350
+ createMatchCard(match, index, selfAssessment, data),
351
+ ),
352
+ ),
353
+ );
354
+ }
355
+
356
+ /**
357
+ * Create a match card
358
+ * @param {Object} match - Job match object
359
+ * @param {number} index - Match index
360
+ * @param {Object} selfAssessment - Self-assessment data
361
+ * @param {Object} data - App data
362
+ * @returns {HTMLElement}
363
+ */
364
+ function createMatchCard(match, _index, _selfAssessment, _data) {
365
+ const { job, analysis } = match;
366
+ const matchPercent = Math.round(analysis.overallScore * 100);
367
+
368
+ // Use tier from model for consistent classification
369
+ const tierColor = analysis.tier.color;
370
+
371
+ return div(
372
+ { className: `match-card match-tier-${tierColor}` },
373
+ // Header
374
+ div(
375
+ { className: "match-card-header" },
376
+ div(
377
+ { className: "match-title-area" },
378
+ h3(
379
+ { className: "match-job-title" },
380
+ a(
381
+ {
382
+ href: `#/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
383
+ },
384
+ job.title,
385
+ ),
386
+ ),
387
+ div(
388
+ { className: "match-badges" },
389
+ createBadge(job.discipline.name, "default"),
390
+ createBadge(job.grade.name, "secondary"),
391
+ createBadge(job.track.name, "broad"),
392
+ ),
393
+ ),
394
+ div(
395
+ { className: "match-score-area" },
396
+ div(
397
+ { className: `match-score match-score-${tierColor}` },
398
+ span({ className: "score-value" }, `${matchPercent}%`),
399
+ span({ className: "score-label" }, "Match"),
400
+ ),
401
+ ),
402
+ ),
403
+
404
+ // Score breakdown
405
+ div(
406
+ { className: "auto-grid-sm" },
407
+ createScoreBar(
408
+ "Skills",
409
+ analysis.skillScore,
410
+ analysis.weightsUsed.skills,
411
+ ),
412
+ createScoreBar(
413
+ "Behaviours",
414
+ analysis.behaviourScore,
415
+ analysis.weightsUsed.behaviours,
416
+ ),
417
+ ),
418
+
419
+ // Priority gaps section (use priorityGaps from model - top 3)
420
+ analysis.priorityGaps.length > 0 &&
421
+ div(
422
+ { className: "match-gaps" },
423
+ h4({}, "Priority Development Areas"),
424
+ div(
425
+ { className: "gaps-list" },
426
+ ...analysis.priorityGaps.map((gap) => createGapItem(gap)),
427
+ analysis.gaps.length > 3 &&
428
+ span(
429
+ { className: "more-gaps" },
430
+ `+${analysis.gaps.length - 3} more areas`,
431
+ ),
432
+ ),
433
+ ),
434
+
435
+ // Actions
436
+ div(
437
+ { className: "match-card-actions" },
438
+ a(
439
+ {
440
+ href: `#/job/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
441
+ className: "btn btn-secondary btn-sm",
442
+ },
443
+ "View Job Details",
444
+ ),
445
+ a(
446
+ {
447
+ href: `#/interview/${job.discipline.id}/${job.track.id}/${job.grade.id}`,
448
+ className: "btn btn-secondary btn-sm",
449
+ },
450
+ "Interview Prep",
451
+ ),
452
+ ),
453
+ );
454
+ }
455
+
456
+ /**
457
+ * Create a score bar
458
+ * @param {string} label
459
+ * @param {number} score - Score from 0 to 1
460
+ * @param {number} weight - Weight factor
461
+ * @returns {HTMLElement}
462
+ */
463
+ function createScoreBar(label, score, weight) {
464
+ const percent = Math.round(score * 100);
465
+ const weightPercent = Math.round(weight * 100);
466
+
467
+ return div(
468
+ { className: "score-bar-item" },
469
+ div(
470
+ { className: "score-bar-header" },
471
+ span({ className: "score-bar-label" }, label),
472
+ span(
473
+ { className: "score-bar-values" },
474
+ `${percent}% (${weightPercent}% weight)`,
475
+ ),
476
+ ),
477
+ div(
478
+ { className: "progress-bar" },
479
+ div({ className: "progress-bar-fill", style: `width: ${percent}%` }),
480
+ ),
481
+ );
482
+ }
483
+
484
+ /**
485
+ * Create a gap item
486
+ * @param {Object} gap - Gap information
487
+ * @returns {HTMLElement}
488
+ */
489
+ function createGapItem(gap) {
490
+ const isSkill = gap.type === "skill";
491
+
492
+ return div(
493
+ { className: "gap-item" },
494
+ span({ className: "gap-type-icon" }, isSkill ? "💡" : "🧠"),
495
+ span({ className: "gap-name" }, gap.name),
496
+ span(
497
+ { className: "gap-levels" },
498
+ span({ className: "gap-current" }, formatLevel(gap.current)),
499
+ span({ className: "gap-arrow" }, "→"),
500
+ span({ className: "gap-required" }, formatLevel(gap.required)),
501
+ ),
502
+ span(
503
+ { className: `gap-size gap-size-${gap.gap}` },
504
+ `+${gap.gap} level${gap.gap > 1 ? "s" : ""}`,
505
+ ),
506
+ );
507
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Behaviours pages
3
+ */
4
+
5
+ import { render, div, h1, p } from "../lib/render.js";
6
+ import { getState } from "../lib/state.js";
7
+ import { createCardList } from "../components/list.js";
8
+ import { renderNotFound } from "../components/error-page.js";
9
+ import { prepareBehavioursList } from "../formatters/behaviour/shared.js";
10
+ import { behaviourToDOM } from "../formatters/behaviour/dom.js";
11
+ import { behaviourToCardConfig } from "../lib/card-mappers.js";
12
+
13
+ /**
14
+ * Render behaviours list page
15
+ */
16
+ export function renderBehavioursList() {
17
+ const { data } = getState();
18
+ const { framework } = data;
19
+
20
+ // Transform data for list view
21
+ const { items } = prepareBehavioursList(data.behaviours);
22
+
23
+ const page = div(
24
+ { className: "behaviours-page" },
25
+ // Header
26
+ div(
27
+ { className: "page-header" },
28
+ h1(
29
+ { className: "page-title" },
30
+ framework.entityDefinitions.behaviour.title,
31
+ ),
32
+ p(
33
+ { className: "page-description" },
34
+ framework.entityDefinitions.behaviour.description.trim(),
35
+ ),
36
+ ),
37
+
38
+ // Behaviours list
39
+ createCardList(items, behaviourToCardConfig, "No behaviours found."),
40
+ );
41
+
42
+ render(page);
43
+ }
44
+
45
+ /**
46
+ * Render behaviour detail page
47
+ * @param {Object} params - Route params
48
+ */
49
+ export function renderBehaviourDetail(params) {
50
+ const { data } = getState();
51
+ const behaviour = data.behaviours.find((b) => b.id === params.id);
52
+
53
+ if (!behaviour) {
54
+ renderNotFound({
55
+ entityType: "Behaviour",
56
+ entityId: params.id,
57
+ backPath: "/behaviour",
58
+ backText: "← Back to Behaviours",
59
+ });
60
+ return;
61
+ }
62
+
63
+ // Use DOM formatter - it handles transformation internally
64
+ render(
65
+ behaviourToDOM(behaviour, {
66
+ drivers: data.drivers,
67
+ framework: data.framework,
68
+ }),
69
+ );
70
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Disciplines pages
3
+ */
4
+
5
+ import { render, div, h1, p } from "../lib/render.js";
6
+ import { getState } from "../lib/state.js";
7
+ import { createCardList } from "../components/list.js";
8
+ import { renderNotFound } from "../components/error-page.js";
9
+ import { prepareDisciplinesList } from "../formatters/discipline/shared.js";
10
+ import { disciplineToDOM } from "../formatters/discipline/dom.js";
11
+ import { disciplineToCardConfig } from "../lib/card-mappers.js";
12
+
13
+ /**
14
+ * Render disciplines list page
15
+ */
16
+ export function renderDisciplinesList() {
17
+ const { data } = getState();
18
+ const { framework } = data;
19
+
20
+ // Transform data for list view
21
+ const { items } = prepareDisciplinesList(data.disciplines);
22
+
23
+ const page = div(
24
+ { className: "disciplines-page" },
25
+ // Header
26
+ div(
27
+ { className: "page-header" },
28
+ h1(
29
+ { className: "page-title" },
30
+ framework.entityDefinitions.discipline.title,
31
+ ),
32
+ p(
33
+ { className: "page-description" },
34
+ framework.entityDefinitions.discipline.description.trim(),
35
+ ),
36
+ ),
37
+
38
+ // Disciplines list
39
+ createCardList(items, disciplineToCardConfig, "No disciplines found."),
40
+ );
41
+
42
+ render(page);
43
+ }
44
+
45
+ /**
46
+ * Render discipline detail page
47
+ * @param {Object} params - Route params
48
+ */
49
+ export function renderDisciplineDetail(params) {
50
+ const { data } = getState();
51
+ const discipline = data.disciplines.find((d) => d.id === params.id);
52
+
53
+ if (!discipline) {
54
+ renderNotFound({
55
+ entityType: "Discipline",
56
+ entityId: params.id,
57
+ backPath: "/discipline",
58
+ backText: "← Back to Disciplines",
59
+ });
60
+ return;
61
+ }
62
+
63
+ // Use DOM formatter - it handles transformation internally
64
+ render(
65
+ disciplineToDOM(discipline, {
66
+ skills: data.skills,
67
+ behaviours: data.behaviours,
68
+ framework: data.framework,
69
+ }),
70
+ );
71
+ }