@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,97 @@
1
+ /**
2
+ * Interview formatting for DOM output
3
+ */
4
+
5
+ import { div, heading1, heading2, p, span } from "../../lib/render.js";
6
+ import { createBackLink } from "../../components/nav.js";
7
+ import { createLevelDots } from "../../components/detail.js";
8
+ import { getConceptEmoji } from "../../model/levels.js";
9
+
10
+ /**
11
+ * Format interview detail as DOM elements
12
+ * @param {Object} view - Interview detail view from presenter
13
+ * @param {Object} typeConfig - Interview type configuration
14
+ * @param {Object} options - Formatting options
15
+ * @param {Object} [options.framework] - Framework data for emoji lookup
16
+ * @param {boolean} [options.showBackLink] - Whether to show back navigation link
17
+ * @returns {HTMLElement}
18
+ */
19
+ export function interviewToDOM(
20
+ view,
21
+ typeConfig,
22
+ { framework, showBackLink = true } = {},
23
+ ) {
24
+ const skillEmoji = getConceptEmoji(framework, "skill");
25
+ const behaviourEmoji = getConceptEmoji(framework, "behaviour");
26
+ return div(
27
+ { className: "detail-page interview-detail" },
28
+ // Header
29
+ div(
30
+ { className: "page-header" },
31
+ showBackLink
32
+ ? createBackLink("/interview", "← Back to Interview Builder")
33
+ : null,
34
+ heading1({ className: "page-title" }, "💬 Interview: ", view.name),
35
+ p({ className: "page-description" }, view.description),
36
+ typeConfig
37
+ ? p({ className: "text-muted" }, `Type: ${typeConfig.label}`)
38
+ : null,
39
+ ),
40
+
41
+ // Questions by skill
42
+ view.questionsBySkill && view.questionsBySkill.length > 0
43
+ ? div(
44
+ { className: "detail-section" },
45
+ heading2(
46
+ { className: "section-title" },
47
+ `${skillEmoji} Skill Questions`,
48
+ ),
49
+ ...view.questionsBySkill.map((group) =>
50
+ div(
51
+ { className: "question-group" },
52
+ p(
53
+ { className: "question-group-title" },
54
+ span({ className: "skill-name" }, group.skillName),
55
+ " ",
56
+ createLevelDots(group.levelIndex, group.totalLevels),
57
+ ),
58
+ div(
59
+ { className: "question-list" },
60
+ ...group.questions.map((q) =>
61
+ p({ className: "question-text" }, q.text),
62
+ ),
63
+ ),
64
+ ),
65
+ ),
66
+ )
67
+ : null,
68
+
69
+ // Questions by behaviour
70
+ view.questionsByBehaviour && view.questionsByBehaviour.length > 0
71
+ ? div(
72
+ { className: "detail-section" },
73
+ heading2(
74
+ { className: "section-title" },
75
+ `${behaviourEmoji} Behaviour Questions`,
76
+ ),
77
+ ...view.questionsByBehaviour.map((group) =>
78
+ div(
79
+ { className: "question-group" },
80
+ p(
81
+ { className: "question-group-title" },
82
+ span({ className: "behaviour-name" }, group.behaviourName),
83
+ " ",
84
+ createLevelDots(group.levelIndex, group.totalLevels),
85
+ ),
86
+ div(
87
+ { className: "question-list" },
88
+ ...group.questions.map((q) =>
89
+ p({ className: "question-text" }, q.text),
90
+ ),
91
+ ),
92
+ ),
93
+ ),
94
+ )
95
+ : null,
96
+ );
97
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Interview formatting for markdown/CLI output
3
+ */
4
+
5
+ import { formatLevel } from "../../lib/render.js";
6
+ import { getConceptEmoji } from "../../model/levels.js";
7
+
8
+ /**
9
+ * Format interview detail as markdown
10
+ * @param {Object} view - Interview detail view from presenter
11
+ * @param {Object} options - Options (e.g., type, framework)
12
+ * @param {Object} [options.framework] - Framework data for emoji lookup
13
+ * @returns {string}
14
+ */
15
+ export function interviewToMarkdown(view, { framework } = {}) {
16
+ const skillEmoji = getConceptEmoji(framework, "skill");
17
+ const behaviourEmoji = getConceptEmoji(framework, "behaviour");
18
+ const lines = [
19
+ `# ${view.typeInfo.icon} Interview: ${view.title}`,
20
+ "",
21
+ `**Type**: ${view.typeInfo.name} (${view.typeInfo.description})`,
22
+ `**Expected duration**: ${view.expectedDurationMinutes} minutes`,
23
+ `**Total questions**: ${view.totalQuestions}`,
24
+ "",
25
+ ];
26
+
27
+ // Group sections by type
28
+ const skillSections = view.sections.filter((s) => s.type === "skill");
29
+ const behaviourSections = view.sections.filter((s) => s.type === "behaviour");
30
+
31
+ // Skill questions
32
+ if (skillSections.length > 0) {
33
+ lines.push(`## ${skillEmoji} Skill Questions`, "");
34
+ for (const section of skillSections) {
35
+ lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
36
+ for (const q of section.questions) {
37
+ lines.push(`**Q**: ${q.question}`);
38
+ if (q.followUps.length > 0) {
39
+ for (const followUp of q.followUps) {
40
+ lines.push(` → ${followUp}`);
41
+ }
42
+ }
43
+ lines.push("");
44
+ }
45
+ }
46
+ }
47
+
48
+ // Behaviour questions
49
+ if (behaviourSections.length > 0) {
50
+ lines.push(`## ${behaviourEmoji} Behaviour Questions`, "");
51
+ for (const section of behaviourSections) {
52
+ lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
53
+ for (const q of section.questions) {
54
+ lines.push(`**Q**: ${q.question}`);
55
+ if (q.followUps.length > 0) {
56
+ for (const followUp of q.followUps) {
57
+ lines.push(` → ${followUp}`);
58
+ }
59
+ }
60
+ lines.push("");
61
+ }
62
+ }
63
+ }
64
+
65
+ return lines.join("\n");
66
+ }
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Interview presentation helpers
3
+ *
4
+ * Shared utilities for formatting interview data across DOM and markdown outputs.
5
+ */
6
+
7
+ import {
8
+ isValidJobCombination,
9
+ generateJobTitle,
10
+ getDisciplineSkillIds,
11
+ } from "../../model/derivation.js";
12
+ import {
13
+ deriveInterviewQuestions,
14
+ deriveShortInterview,
15
+ deriveBehaviourQuestions,
16
+ } from "../../model/interview.js";
17
+ import { getOrCreateJob } from "../../lib/job-cache.js";
18
+
19
+ /**
20
+ * Interview type configurations
21
+ */
22
+ export const INTERVIEW_TYPES = {
23
+ short: {
24
+ id: "short",
25
+ name: "Screening",
26
+ description: "20-minute screening interview",
27
+ icon: "⏱️",
28
+ expectedDurationMinutes: 20,
29
+ },
30
+ behaviour: {
31
+ id: "behaviour",
32
+ name: "Behavioural",
33
+ description: "Focus on behaviours and mindsets",
34
+ icon: "🧠",
35
+ expectedDurationMinutes: 45,
36
+ },
37
+ full: {
38
+ id: "full",
39
+ name: "Full Interview",
40
+ description: "Comprehensive interview covering all skills and behaviours",
41
+ icon: "📋",
42
+ expectedDurationMinutes: 90,
43
+ },
44
+ };
45
+
46
+ /**
47
+ * Transform raw interview questions into sections
48
+ * @param {Array} questions - Raw questions from interview module
49
+ * @returns {Array}
50
+ */
51
+ function groupQuestionsIntoSections(questions) {
52
+ const sections = {};
53
+
54
+ for (const q of questions) {
55
+ const id = q.targetId;
56
+ const name = q.targetName;
57
+ const type = q.targetType;
58
+ const level = q.targetLevel;
59
+
60
+ if (!sections[id]) {
61
+ sections[id] = {
62
+ id,
63
+ name,
64
+ type,
65
+ level,
66
+ questions: [],
67
+ };
68
+ }
69
+
70
+ sections[id].questions.push({
71
+ skillOrBehaviourId: id,
72
+ skillOrBehaviourName: name,
73
+ type,
74
+ level,
75
+ question: q.question.text,
76
+ followUps: q.question.followUps || [],
77
+ });
78
+ }
79
+
80
+ return Object.values(sections);
81
+ }
82
+
83
+ /**
84
+ * @typedef {Object} InterviewDetailView
85
+ * @property {string} title
86
+ * @property {string} interviewType - 'full', 'short', or 'behaviour'
87
+ * @property {string} disciplineId
88
+ * @property {string} disciplineName
89
+ * @property {string} gradeId
90
+ * @property {string} trackId
91
+ * @property {string} trackName
92
+ * @property {Array} sections
93
+ * @property {number} totalQuestions
94
+ * @property {number} expectedDurationMinutes
95
+ * @property {Object} typeInfo
96
+ */
97
+
98
+ /**
99
+ * Prepare interview questions for a job
100
+ * @param {Object} params
101
+ * @param {Object} params.discipline
102
+ * @param {Object} params.grade
103
+ * @param {Object} params.track
104
+ * @param {Array} params.skills
105
+ * @param {Array} params.behaviours
106
+ * @param {Array} params.questions
107
+ * @param {string} [params.interviewType='full']
108
+ * @returns {InterviewDetailView|null}
109
+ */
110
+ export function prepareInterviewDetail({
111
+ discipline,
112
+ grade,
113
+ track,
114
+ skills,
115
+ behaviours,
116
+ questions,
117
+ interviewType = "full",
118
+ }) {
119
+ if (!discipline || !grade || !track) return null;
120
+
121
+ const job = getOrCreateJob({
122
+ discipline,
123
+ grade,
124
+ track,
125
+ skills,
126
+ behaviours,
127
+ });
128
+
129
+ if (!job) return null;
130
+
131
+ let interviewGuide;
132
+ switch (interviewType) {
133
+ case "short":
134
+ interviewGuide = deriveShortInterview({ job, questionBank: questions });
135
+ break;
136
+ case "behaviour":
137
+ interviewGuide = deriveBehaviourQuestions({
138
+ job,
139
+ questionBank: questions,
140
+ });
141
+ break;
142
+ case "full":
143
+ default:
144
+ interviewGuide = deriveInterviewQuestions({
145
+ job,
146
+ questionBank: questions,
147
+ });
148
+ break;
149
+ }
150
+
151
+ // Extract the questions array from the interview guide
152
+ const rawQuestions = interviewGuide.questions || [];
153
+
154
+ // Separate skill and behaviour questions based on targetType
155
+ const skillQuestions = rawQuestions.filter((q) => q.targetType === "skill");
156
+ const behaviourQuestions = rawQuestions.filter(
157
+ (q) => q.targetType === "behaviour",
158
+ );
159
+
160
+ const skillSections = groupQuestionsIntoSections(skillQuestions);
161
+ const behaviourSections = groupQuestionsIntoSections(behaviourQuestions);
162
+
163
+ const allSections = [...skillSections, ...behaviourSections];
164
+ const totalQuestions = rawQuestions.length;
165
+
166
+ const typeConfig = INTERVIEW_TYPES[interviewType] || INTERVIEW_TYPES.full;
167
+
168
+ return {
169
+ title: job.title,
170
+ interviewType,
171
+ disciplineId: discipline.id,
172
+ disciplineName: discipline.specialization || discipline.name,
173
+ gradeId: grade.id,
174
+ trackId: track.id,
175
+ trackName: track.name,
176
+ sections: allSections,
177
+ totalQuestions,
178
+ expectedDurationMinutes: typeConfig.expectedDurationMinutes,
179
+ typeInfo: typeConfig,
180
+ };
181
+ }
182
+
183
+ /**
184
+ * @typedef {Object} InterviewBuilderPreview
185
+ * @property {boolean} isValid
186
+ * @property {string|null} title
187
+ * @property {number} totalSkills
188
+ * @property {number} totalBehaviours
189
+ * @property {string|null} invalidReason
190
+ */
191
+
192
+ /**
193
+ * Prepare interview builder preview for form validation
194
+ * @param {Object} params
195
+ * @param {Object|null} params.discipline
196
+ * @param {Object|null} params.grade
197
+ * @param {Object|null} params.track
198
+ * @param {number} params.behaviourCount - Total behaviours in the system
199
+ * @param {Array} [params.grades] - All grades for validation
200
+ * @returns {InterviewBuilderPreview}
201
+ */
202
+ export function prepareInterviewBuilderPreview({
203
+ discipline,
204
+ grade,
205
+ track,
206
+ behaviourCount,
207
+ grades,
208
+ }) {
209
+ if (!discipline || !grade || !track) {
210
+ return {
211
+ isValid: false,
212
+ title: null,
213
+ totalSkills: 0,
214
+ totalBehaviours: 0,
215
+ invalidReason: null,
216
+ };
217
+ }
218
+
219
+ const validCombination = isValidJobCombination({
220
+ discipline,
221
+ grade,
222
+ track,
223
+ grades,
224
+ });
225
+
226
+ if (!validCombination) {
227
+ return {
228
+ isValid: false,
229
+ title: null,
230
+ totalSkills: 0,
231
+ totalBehaviours: 0,
232
+ invalidReason: `The ${track.name} track is not available for ${discipline.specialization}.`,
233
+ };
234
+ }
235
+
236
+ const title = generateJobTitle(discipline, grade, track);
237
+ const totalSkills = getDisciplineSkillIds(discipline).length;
238
+
239
+ return {
240
+ isValid: true,
241
+ title: `Interview for: ${title}`,
242
+ totalSkills,
243
+ totalBehaviours: behaviourCount,
244
+ invalidReason: null,
245
+ };
246
+ }
247
+
248
+ /**
249
+ * @typedef {Object} AllInterviewsView
250
+ * @property {string} title
251
+ * @property {string} disciplineId
252
+ * @property {string} disciplineName
253
+ * @property {string} gradeId
254
+ * @property {string} trackId
255
+ * @property {string} trackName
256
+ * @property {Object.<string, Object>} interviews - Keyed by type
257
+ */
258
+
259
+ /**
260
+ * Prepare all interview types for a job (for toggle UI)
261
+ * @param {Object} params
262
+ * @param {Object} params.discipline
263
+ * @param {Object} params.grade
264
+ * @param {Object} params.track
265
+ * @param {Array} params.skills
266
+ * @param {Array} params.behaviours
267
+ * @param {Array} params.questions
268
+ * @returns {AllInterviewsView|null}
269
+ */
270
+ export function prepareAllInterviews({
271
+ discipline,
272
+ grade,
273
+ track,
274
+ skills,
275
+ behaviours,
276
+ questions,
277
+ }) {
278
+ if (!discipline || !grade || !track) return null;
279
+
280
+ const job = getOrCreateJob({
281
+ discipline,
282
+ grade,
283
+ track,
284
+ skills,
285
+ behaviours,
286
+ });
287
+
288
+ if (!job) return null;
289
+
290
+ // Generate all interview types
291
+ const shortInterview = deriveShortInterview({
292
+ job,
293
+ questionBank: questions,
294
+ targetMinutes: 20,
295
+ });
296
+
297
+ const behaviourInterview = deriveBehaviourQuestions({
298
+ job,
299
+ questionBank: questions,
300
+ });
301
+
302
+ const fullInterview = deriveInterviewQuestions({
303
+ job,
304
+ questionBank: questions,
305
+ options: {
306
+ targetMinutes: 60,
307
+ skillBehaviourRatio: 0.6,
308
+ },
309
+ });
310
+
311
+ return {
312
+ title: job.title,
313
+ disciplineId: discipline.id,
314
+ disciplineName: discipline.specialization || discipline.name,
315
+ gradeId: grade.id,
316
+ trackId: track.id,
317
+ trackName: track.name,
318
+ interviews: {
319
+ short: {
320
+ ...shortInterview,
321
+ type: "short",
322
+ typeInfo: INTERVIEW_TYPES.short,
323
+ },
324
+ behaviour: {
325
+ ...behaviourInterview,
326
+ type: "behaviour",
327
+ typeInfo: INTERVIEW_TYPES.behaviour,
328
+ },
329
+ full: { ...fullInterview, type: "full", typeInfo: INTERVIEW_TYPES.full },
330
+ },
331
+ };
332
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Job Description Formatter
3
+ *
4
+ * Formats job data into markdown job description content.
5
+ * Parallels formatters/agent/profile.js in structure.
6
+ */
7
+
8
+ import {
9
+ SKILL_LEVEL_ORDER,
10
+ BEHAVIOUR_MATURITY_ORDER,
11
+ } from "../../model/levels.js";
12
+
13
+ /**
14
+ * Format job as a markdown job description
15
+ * @param {Object} params
16
+ * @param {Object} params.job - The job definition
17
+ * @param {Object} params.discipline - The discipline
18
+ * @param {Object} params.grade - The grade
19
+ * @param {Object} params.track - The track
20
+ * @returns {string} Markdown formatted job description
21
+ */
22
+ export function formatJobDescription({ job, discipline, grade, track }) {
23
+ const lines = [];
24
+
25
+ // Title
26
+ lines.push(`# ${job.title}`);
27
+ lines.push("");
28
+
29
+ // Meta information
30
+ lines.push(`- **Level:** ${grade.id}`);
31
+ lines.push(`- **Experience:** ${grade.typicalExperienceRange}`);
32
+ lines.push(`- **Track:** ${track.name}`);
33
+ lines.push("");
34
+
35
+ // Role Summary
36
+ lines.push("## ROLE SUMMARY");
37
+ lines.push("");
38
+
39
+ // Build role summary from discipline - use manager version if applicable
40
+ const isManagement = track.isManagement === true;
41
+ let roleSummary =
42
+ isManagement && discipline.managementRoleSummary
43
+ ? discipline.managementRoleSummary
44
+ : discipline.professionalRoleSummary || discipline.description;
45
+ // Replace placeholders
46
+ const { roleTitle, specialization } = discipline;
47
+ roleSummary = roleSummary.replace(/\{roleTitle\}/g, roleTitle);
48
+ roleSummary = roleSummary.replace(/\{specialization\}/g, specialization);
49
+ lines.push(roleSummary);
50
+ lines.push("");
51
+
52
+ // Add track context
53
+ if (track.roleContext) {
54
+ lines.push(track.roleContext);
55
+ lines.push("");
56
+ }
57
+
58
+ // Add grade expectations as natural paragraphs
59
+ if (job.expectations) {
60
+ const exp = job.expectations;
61
+ const expectationSentences = [];
62
+
63
+ if (exp.impactScope) {
64
+ expectationSentences.push(
65
+ `This role encompasses ${exp.impactScope.toLowerCase()}.`,
66
+ );
67
+ }
68
+ if (exp.autonomyExpectation) {
69
+ let autonomySentence = `You will ${exp.autonomyExpectation.toLowerCase()}`;
70
+ if (exp.influenceScope) {
71
+ autonomySentence +=
72
+ `, ${exp.influenceScope.toLowerCase()}` +
73
+ (exp.influenceScope.endsWith(".") ? "" : ".");
74
+ } else {
75
+ autonomySentence += exp.autonomyExpectation.endsWith(".") ? "" : ".";
76
+ }
77
+ expectationSentences.push(autonomySentence);
78
+ } else if (exp.influenceScope) {
79
+ expectationSentences.push(
80
+ exp.influenceScope + (exp.influenceScope.endsWith(".") ? "" : "."),
81
+ );
82
+ }
83
+ if (exp.complexityHandled) {
84
+ expectationSentences.push(
85
+ `You will handle ${exp.complexityHandled.toLowerCase()}.`,
86
+ );
87
+ }
88
+
89
+ if (expectationSentences.length > 0) {
90
+ lines.push(expectationSentences.join(" "));
91
+ lines.push("");
92
+ }
93
+ }
94
+
95
+ // Key Responsibilities
96
+ lines.push("## ROLE RESPONSIBILITIES");
97
+ lines.push("");
98
+
99
+ // Use derived responsibilities (already sorted by level descending)
100
+ const derivedResponsibilities = job.derivedResponsibilities || [];
101
+
102
+ for (const r of derivedResponsibilities) {
103
+ lines.push(`- **${r.capabilityName}:** ${r.responsibility}`);
104
+ }
105
+ lines.push("");
106
+
107
+ // Key Behaviours
108
+ lines.push("## ROLE BEHAVIOURS");
109
+ lines.push("");
110
+
111
+ // Sort behaviours by maturity level (highest first)
112
+ const sortedBehaviours = [...job.behaviourProfile].sort((a, b) => {
113
+ const indexA = BEHAVIOUR_MATURITY_ORDER.indexOf(a.maturity);
114
+ const indexB = BEHAVIOUR_MATURITY_ORDER.indexOf(b.maturity);
115
+ // Sort in reverse order (exemplifying first, emerging last)
116
+ if (indexA === -1 && indexB === -1) return 0;
117
+ if (indexA === -1) return 1;
118
+ if (indexB === -1) return -1;
119
+ return indexB - indexA;
120
+ });
121
+
122
+ for (const behaviour of sortedBehaviours) {
123
+ lines.push(
124
+ `- **${behaviour.behaviourName}:** ${behaviour.maturityDescription || ""}`,
125
+ );
126
+ }
127
+ lines.push("");
128
+
129
+ // Group skills by level
130
+ const skillsByLevel = {};
131
+ for (const skill of job.skillMatrix) {
132
+ const level = skill.level || "Other";
133
+ if (!skillsByLevel[level]) {
134
+ skillsByLevel[level] = [];
135
+ }
136
+ skillsByLevel[level].push(skill);
137
+ }
138
+
139
+ // Sort levels in a logical order using SKILL_LEVEL_ORDER from types.js
140
+ const sortedLevels = Object.keys(skillsByLevel).sort((a, b) => {
141
+ const indexA = SKILL_LEVEL_ORDER.indexOf(a.toLowerCase());
142
+ const indexB = SKILL_LEVEL_ORDER.indexOf(b.toLowerCase());
143
+ // Sort in reverse order (expert first, awareness last)
144
+ if (indexA === -1 && indexB === -1) return a.localeCompare(b);
145
+ if (indexA === -1) return 1;
146
+ if (indexB === -1) return -1;
147
+ return indexB - indexA;
148
+ });
149
+
150
+ for (const level of sortedLevels) {
151
+ const skills = skillsByLevel[level];
152
+ if (skills.length > 0) {
153
+ lines.push(`## ${level.toUpperCase()}-LEVEL SKILLS`);
154
+ lines.push("");
155
+ // Sort skills alphabetically by name
156
+ const sortedSkills = [...skills].sort((a, b) =>
157
+ (a.skillName || "").localeCompare(b.skillName || ""),
158
+ );
159
+ for (const skill of sortedSkills) {
160
+ lines.push(`- **${skill.skillName}:** ${skill.levelDescription || ""}`);
161
+ }
162
+ lines.push("");
163
+ }
164
+ }
165
+
166
+ // Qualifications
167
+ lines.push("## QUALIFICATIONS");
168
+ lines.push("");
169
+
170
+ if (grade.qualificationSummary) {
171
+ lines.push(grade.qualificationSummary);
172
+ lines.push("");
173
+ }
174
+
175
+ return lines.join("\n");
176
+ }