@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,86 @@
1
+ /**
2
+ * Progress/progression formatting for markdown/CLI output
3
+ */
4
+
5
+ import { tableToMarkdown } from "../shared.js";
6
+ import { formatLevel } from "../../lib/render.js";
7
+
8
+ /**
9
+ * Format progress detail as markdown
10
+ * @param {Object} view - Progress detail view from presenter
11
+ * @returns {string}
12
+ */
13
+ export function progressToMarkdown(view) {
14
+ const lines = [
15
+ `# 📈 Career Progression`,
16
+ "",
17
+ `**From**: ${view.fromTitle}`,
18
+ `**To**: ${view.toTitle}`,
19
+ "",
20
+ ];
21
+
22
+ // Summary
23
+ lines.push("## Summary", "");
24
+ lines.push(`- Skills to improve: ${view.summary.skillsToImprove}`);
25
+ lines.push(`- Behaviours to improve: ${view.summary.behavioursToImprove}`);
26
+ lines.push(`- New skills: ${view.summary.newSkills}`);
27
+ lines.push(`- Total changes: ${view.summary.totalChanges}`);
28
+ lines.push("");
29
+
30
+ // Skill changes
31
+ const skillsWithChanges = view.skillChanges.filter(
32
+ (s) => s.levelChange !== 0,
33
+ );
34
+ if (skillsWithChanges.length > 0) {
35
+ lines.push("## Skill Changes", "");
36
+ const skillRows = skillsWithChanges.map((s) => [
37
+ s.name,
38
+ formatLevel(s.type),
39
+ formatLevel(s.fromLevel || "-"),
40
+ "→",
41
+ formatLevel(s.toLevel),
42
+ formatChange(s.levelChange),
43
+ ]);
44
+ lines.push(
45
+ tableToMarkdown(["Skill", "Type", "From", "", "To", "Change"], skillRows),
46
+ );
47
+ lines.push("");
48
+ }
49
+
50
+ // Behaviour changes
51
+ const behavioursWithChanges = view.behaviourChanges.filter(
52
+ (b) => b.maturityChange !== 0,
53
+ );
54
+ if (behavioursWithChanges.length > 0) {
55
+ lines.push("## Behaviour Changes", "");
56
+ const behaviourRows = behavioursWithChanges.map((b) => [
57
+ b.name,
58
+ formatLevel(b.fromMaturity || "-"),
59
+ "→",
60
+ formatLevel(b.toMaturity),
61
+ formatChange(b.maturityChange),
62
+ ]);
63
+ lines.push(
64
+ tableToMarkdown(["Behaviour", "From", "", "To", "Change"], behaviourRows),
65
+ );
66
+ lines.push("");
67
+ }
68
+
69
+ if (skillsWithChanges.length === 0 && behavioursWithChanges.length === 0) {
70
+ lines.push("No changes required for this progression.");
71
+ lines.push("");
72
+ }
73
+
74
+ return lines.join("\n");
75
+ }
76
+
77
+ /**
78
+ * Format change indicator
79
+ * @param {number} change
80
+ * @returns {string}
81
+ */
82
+ function formatChange(change) {
83
+ if (change > 0) return `+${change}`;
84
+ if (change < 0) return `${change}`;
85
+ return "0";
86
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Progress presentation helpers
3
+ *
4
+ * Shared utilities for formatting career progression data across DOM and markdown outputs.
5
+ */
6
+
7
+ import {
8
+ isValidJobCombination,
9
+ generateJobTitle,
10
+ } from "../../model/derivation.js";
11
+ import {
12
+ analyzeProgression,
13
+ analyzeCustomProgression,
14
+ getNextGrade,
15
+ } from "../../model/progression.js";
16
+ import { getOrCreateJob } from "../../lib/job-cache.js";
17
+
18
+ /**
19
+ * Get the next grade for progression
20
+ * @param {Object} currentGrade
21
+ * @param {Array} grades
22
+ * @returns {Object|null}
23
+ */
24
+ export function getDefaultTargetGrade(currentGrade, grades) {
25
+ return getNextGrade(currentGrade, grades);
26
+ }
27
+
28
+ /**
29
+ * Check if a job combination is valid
30
+ * @param {Object} params
31
+ * @param {Object} params.discipline
32
+ * @param {Object} params.grade
33
+ * @param {Object} params.track
34
+ * @param {Array} [params.grades] - All grades for validation
35
+ * @returns {boolean}
36
+ */
37
+ export function isValidCombination({ discipline, grade, track, grades }) {
38
+ return isValidJobCombination({ discipline, grade, track, grades });
39
+ }
40
+
41
+ /**
42
+ * @typedef {Object} CurrentJobView
43
+ * @property {string} title
44
+ * @property {number} skillCount
45
+ * @property {number} behaviourCount
46
+ * @property {number} primarySkillCount
47
+ * @property {Array} skillMatrix - Raw skill matrix for components
48
+ * @property {Array} behaviourProfile - Raw behaviour profile for components
49
+ */
50
+
51
+ /**
52
+ * Prepare current job summary for progress detail page
53
+ * @param {Object} params
54
+ * @param {Object} params.discipline
55
+ * @param {Object} params.grade
56
+ * @param {Object} params.track
57
+ * @param {Array} params.skills
58
+ * @param {Array} params.behaviours
59
+ * @param {Array} [params.capabilities]
60
+ * @returns {CurrentJobView|null}
61
+ */
62
+ export function prepareCurrentJob({
63
+ discipline,
64
+ grade,
65
+ track,
66
+ skills,
67
+ behaviours,
68
+ capabilities,
69
+ }) {
70
+ if (!discipline || !grade || !track) return null;
71
+
72
+ const job = getOrCreateJob({
73
+ discipline,
74
+ grade,
75
+ track,
76
+ skills,
77
+ behaviours,
78
+ capabilities,
79
+ });
80
+
81
+ if (!job) return null;
82
+
83
+ return {
84
+ title: job.title,
85
+ skillCount: job.skillMatrix.length,
86
+ behaviourCount: job.behaviourProfile.length,
87
+ primarySkillCount: job.skillMatrix.filter((s) => s.type === "primary")
88
+ .length,
89
+ skillMatrix: job.skillMatrix,
90
+ behaviourProfile: job.behaviourProfile,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * @typedef {Object} CareerProgressPreview
96
+ * @property {boolean} isValid
97
+ * @property {string|null} title
98
+ * @property {string|null} invalidReason
99
+ * @property {Object|null} nextGrade
100
+ * @property {Array} validTracks - Other valid tracks for comparison
101
+ */
102
+
103
+ /**
104
+ * Prepare career progress builder preview for form validation
105
+ * @param {Object} params
106
+ * @param {Object|null} params.discipline
107
+ * @param {Object|null} params.grade
108
+ * @param {Object|null} params.track
109
+ * @param {Array} params.grades - All grades
110
+ * @param {Array} params.tracks - All tracks
111
+ * @returns {CareerProgressPreview}
112
+ */
113
+ export function prepareCareerProgressPreview({
114
+ discipline,
115
+ grade,
116
+ track,
117
+ grades,
118
+ tracks,
119
+ }) {
120
+ if (!discipline || !grade || !track) {
121
+ return {
122
+ isValid: false,
123
+ title: null,
124
+ invalidReason: null,
125
+ nextGrade: null,
126
+ validTracks: [],
127
+ };
128
+ }
129
+
130
+ const validCombination = isValidJobCombination({
131
+ discipline,
132
+ grade,
133
+ track,
134
+ grades,
135
+ });
136
+
137
+ if (!validCombination) {
138
+ return {
139
+ isValid: false,
140
+ title: null,
141
+ invalidReason: track.validDisciplines
142
+ ? `The ${track.name} track is only available for certain disciplines.`
143
+ : "This combination is not valid.",
144
+ nextGrade: null,
145
+ validTracks: [],
146
+ };
147
+ }
148
+
149
+ const title = generateJobTitle(discipline, grade, track);
150
+ const nextGrade = getNextGrade(grade, grades);
151
+
152
+ // Find other valid tracks for comparison
153
+ const validTracks = tracks.filter(
154
+ (t) =>
155
+ t.id !== track.id &&
156
+ isValidJobCombination({ discipline, grade, track: t, grades }),
157
+ );
158
+
159
+ return {
160
+ isValid: true,
161
+ title,
162
+ invalidReason: null,
163
+ nextGrade: nextGrade
164
+ ? { id: nextGrade.id, name: nextGrade.managementTitle }
165
+ : null,
166
+ validTracks: validTracks.map((t) => ({ id: t.id, name: t.name })),
167
+ };
168
+ }
169
+
170
+ /**
171
+ * @typedef {Object} ProgressDetailView
172
+ * @property {string} fromTitle
173
+ * @property {string} toTitle
174
+ * @property {Object} fromJob
175
+ * @property {Object} toJob
176
+ * @property {Array} skillChanges
177
+ * @property {Array} behaviourChanges
178
+ * @property {Object} summary
179
+ */
180
+
181
+ /**
182
+ * Prepare career progression between two roles
183
+ * @param {Object} params
184
+ * @param {Object} params.fromDiscipline
185
+ * @param {Object} params.fromGrade
186
+ * @param {Object} params.fromTrack
187
+ * @param {Object} params.toDiscipline
188
+ * @param {Object} params.toGrade
189
+ * @param {Object} params.toTrack
190
+ * @param {Array} params.skills
191
+ * @param {Array} params.behaviours
192
+ * @param {Array} [params.capabilities]
193
+ * @returns {ProgressDetailView|null}
194
+ */
195
+ export function prepareProgressDetail({
196
+ fromDiscipline,
197
+ fromGrade,
198
+ fromTrack,
199
+ toDiscipline,
200
+ toGrade,
201
+ toTrack,
202
+ skills,
203
+ behaviours,
204
+ capabilities,
205
+ }) {
206
+ if (!fromDiscipline || !fromGrade || !fromTrack) return null;
207
+ if (!toDiscipline || !toGrade || !toTrack) return null;
208
+
209
+ const fromJob = getOrCreateJob({
210
+ discipline: fromDiscipline,
211
+ grade: fromGrade,
212
+ track: fromTrack,
213
+ skills,
214
+ behaviours,
215
+ capabilities,
216
+ });
217
+
218
+ const toJob = getOrCreateJob({
219
+ discipline: toDiscipline,
220
+ grade: toGrade,
221
+ track: toTrack,
222
+ skills,
223
+ behaviours,
224
+ capabilities,
225
+ });
226
+
227
+ if (!fromJob || !toJob) return null;
228
+
229
+ const progression = analyzeProgression(fromJob, toJob);
230
+
231
+ // Transform skill changes
232
+ const skillChanges = progression.skillChanges.map((s) => ({
233
+ id: s.id,
234
+ name: s.name,
235
+ type: s.type,
236
+ fromLevel: s.currentLevel,
237
+ toLevel: s.targetLevel,
238
+ levelChange: s.change,
239
+ }));
240
+
241
+ // Transform behaviour changes
242
+ const behaviourChanges = progression.behaviourChanges.map((b) => ({
243
+ id: b.id,
244
+ name: b.name,
245
+ fromMaturity: b.currentLevel,
246
+ toMaturity: b.targetLevel,
247
+ maturityChange: b.change,
248
+ }));
249
+
250
+ const summary = {
251
+ skillsToImprove: skillChanges.filter((s) => s.levelChange > 0).length,
252
+ behavioursToImprove: behaviourChanges.filter((b) => b.maturityChange > 0)
253
+ .length,
254
+ newSkills: skillChanges.filter((s) => !s.fromLevel && s.toLevel).length,
255
+ totalChanges:
256
+ skillChanges.filter((s) => s.levelChange !== 0).length +
257
+ behaviourChanges.filter((b) => b.maturityChange !== 0).length,
258
+ };
259
+
260
+ return {
261
+ fromTitle: fromJob.title,
262
+ toTitle: toJob.title,
263
+ fromJob: {
264
+ disciplineId: fromDiscipline.id,
265
+ gradeId: fromGrade.id,
266
+ trackId: fromTrack.id,
267
+ },
268
+ toJob: {
269
+ disciplineId: toDiscipline.id,
270
+ gradeId: toGrade.id,
271
+ trackId: toTrack.id,
272
+ },
273
+ skillChanges,
274
+ behaviourChanges,
275
+ summary,
276
+ };
277
+ }
278
+
279
+ /**
280
+ * @typedef {Object} CustomProgressionView
281
+ * @property {Object} current - Current job info
282
+ * @property {Object} target - Target job info
283
+ * @property {Array} skillChanges
284
+ * @property {Array} behaviourChanges
285
+ * @property {Object} summary
286
+ */
287
+
288
+ /**
289
+ * Prepare custom progression analysis between any two roles
290
+ * @param {Object} params
291
+ * @param {Object} params.discipline - Current discipline
292
+ * @param {Object} params.currentGrade
293
+ * @param {Object} params.currentTrack
294
+ * @param {Object} params.targetDiscipline
295
+ * @param {Object} params.targetGrade
296
+ * @param {Object} params.targetTrack
297
+ * @param {Array} params.skills
298
+ * @param {Array} params.behaviours
299
+ * @returns {CustomProgressionView|null}
300
+ */
301
+ export function prepareCustomProgression({
302
+ discipline,
303
+ currentGrade,
304
+ currentTrack,
305
+ targetDiscipline,
306
+ targetGrade,
307
+ targetTrack,
308
+ skills,
309
+ behaviours,
310
+ }) {
311
+ const analysis = analyzeCustomProgression({
312
+ discipline,
313
+ currentGrade,
314
+ currentTrack,
315
+ targetDiscipline,
316
+ targetGrade,
317
+ targetTrack,
318
+ skills,
319
+ behaviours,
320
+ });
321
+
322
+ if (!analysis) return null;
323
+
324
+ return {
325
+ current: {
326
+ title: analysis.current.title,
327
+ skillMatrix: analysis.current.skillMatrix,
328
+ behaviourProfile: analysis.current.behaviourProfile,
329
+ },
330
+ target: {
331
+ title: analysis.target.title,
332
+ skillMatrix: analysis.target.skillMatrix,
333
+ behaviourProfile: analysis.target.behaviourProfile,
334
+ },
335
+ skillChanges: analysis.skillChanges,
336
+ behaviourChanges: analysis.behaviourChanges,
337
+ summary: analysis.summary,
338
+ };
339
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Questions JSON Formatter
3
+ *
4
+ * Formats questions as JSON for programmatic analysis.
5
+ */
6
+
7
+ /**
8
+ * Format questions as JSON
9
+ * @param {Object} view - Questions view from prepareQuestionsView
10
+ * @param {Object} options - Format options
11
+ * @returns {string}
12
+ */
13
+ export function questionsToJson(view, _options = {}) {
14
+ const { filter, questions, stats } = view;
15
+
16
+ const output = {
17
+ filter: {
18
+ level: filter.level,
19
+ maturity: filter.maturity,
20
+ skills: filter.skills,
21
+ behaviours: filter.behaviours,
22
+ capability: filter.capability,
23
+ },
24
+ questions: questions.map((q) => ({
25
+ source: q.source,
26
+ sourceName: q.sourceName,
27
+ sourceType: q.sourceType,
28
+ level: q.level,
29
+ id: q.id,
30
+ text: q.text,
31
+ lookingFor: q.lookingFor,
32
+ expectedDurationMinutes: q.expectedDurationMinutes,
33
+ followUps: q.followUps.length > 0 ? q.followUps : undefined,
34
+ })),
35
+ stats: {
36
+ totalQuestions: stats.totalQuestions,
37
+ bySource: stats.bySource,
38
+ byLevel: stats.byLevel,
39
+ },
40
+ };
41
+
42
+ return JSON.stringify(output, null, 2);
43
+ }