@forwardimpact/pathway 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/app/commands/agent.js +119 -31
  2. package/app/commands/command-factory.js +3 -3
  3. package/app/commands/interview.js +14 -7
  4. package/app/commands/job.js +52 -33
  5. package/app/commands/progress.js +14 -7
  6. package/app/commands/serve.js +5 -0
  7. package/app/commands/stage.js +0 -10
  8. package/app/commands/track.js +5 -8
  9. package/app/components/builder.js +117 -30
  10. package/app/css/components/surfaces.css +16 -0
  11. package/app/formatters/agent/profile.js +30 -115
  12. package/app/formatters/agent/skill.js +23 -44
  13. package/app/formatters/behaviour/dom.js +3 -0
  14. package/app/formatters/behaviour/microdata.js +106 -0
  15. package/app/formatters/discipline/dom.js +28 -1
  16. package/app/formatters/discipline/microdata.js +117 -0
  17. package/app/formatters/discipline/shared.js +49 -8
  18. package/app/formatters/driver/dom.js +3 -0
  19. package/app/formatters/driver/microdata.js +91 -0
  20. package/app/formatters/grade/dom.js +5 -4
  21. package/app/formatters/grade/microdata.js +151 -0
  22. package/app/formatters/index.js +32 -1
  23. package/app/formatters/interview/shared.js +13 -8
  24. package/app/formatters/job/description.js +70 -81
  25. package/app/formatters/job/dom.js +40 -113
  26. package/app/formatters/job/markdown.js +17 -13
  27. package/app/formatters/json-ld.js +242 -0
  28. package/app/formatters/microdata-shared.js +184 -0
  29. package/app/formatters/progress/shared.js +14 -11
  30. package/app/formatters/shared.js +7 -2
  31. package/app/formatters/skill/dom.js +3 -0
  32. package/app/formatters/skill/microdata.js +151 -0
  33. package/app/formatters/stage/dom.js +3 -18
  34. package/app/formatters/stage/microdata.js +110 -0
  35. package/app/formatters/stage/shared.js +0 -27
  36. package/app/formatters/track/dom.js +5 -30
  37. package/app/formatters/track/markdown.js +2 -25
  38. package/app/formatters/track/microdata.js +111 -0
  39. package/app/formatters/track/shared.js +6 -58
  40. package/app/handout-main.js +26 -12
  41. package/app/handout.html +7 -0
  42. package/app/index.html +11 -0
  43. package/app/lib/card-mappers.js +17 -12
  44. package/app/lib/form-controls.js +64 -1
  45. package/app/lib/job-cache.js +12 -9
  46. package/app/lib/render.js +8 -1
  47. package/app/lib/template-loader.js +75 -0
  48. package/app/lib/yaml-loader.js +25 -8
  49. package/app/main.js +8 -4
  50. package/app/model/agent.js +158 -130
  51. package/app/model/checklist.js +57 -91
  52. package/app/model/derivation.js +135 -68
  53. package/app/model/index-generator.js +1 -7
  54. package/app/model/job.js +19 -13
  55. package/app/model/levels.js +20 -12
  56. package/app/model/loader.js +41 -17
  57. package/app/model/matching.js +33 -3
  58. package/app/model/profile.js +38 -45
  59. package/app/model/schema-validation.js +438 -0
  60. package/app/model/validation.js +747 -68
  61. package/app/pages/agent-builder.js +125 -28
  62. package/app/pages/assessment-results.js +10 -4
  63. package/app/pages/discipline.js +36 -6
  64. package/app/pages/driver.js +9 -47
  65. package/app/pages/interview-builder.js +3 -1
  66. package/app/pages/interview.js +15 -4
  67. package/app/pages/job-builder.js +4 -1
  68. package/app/pages/job.js +43 -8
  69. package/app/pages/landing.js +10 -10
  70. package/app/pages/progress-builder.js +3 -1
  71. package/app/pages/progress.js +78 -26
  72. package/app/pages/self-assessment.js +3 -3
  73. package/app/pages/stage.js +3 -126
  74. package/app/slide-main.js +45 -17
  75. package/app/slides/index.js +3 -1
  76. package/app/slides/overview.js +40 -4
  77. package/app/slides/progress.js +4 -2
  78. package/app/slides.html +7 -0
  79. package/bin/pathway.js +28 -75
  80. package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
  81. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
  82. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
  83. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
  84. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
  85. package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
  86. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
  87. package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
  88. package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
  89. package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
  90. package/examples/agents/.vscode/settings.json +1 -1
  91. package/examples/behaviours/outcome_ownership.yaml +1 -2
  92. package/examples/behaviours/polymathic_knowledge.yaml +1 -2
  93. package/examples/behaviours/precise_communication.yaml +1 -2
  94. package/examples/behaviours/relentless_curiosity.yaml +1 -2
  95. package/examples/behaviours/systems_thinking.yaml +1 -2
  96. package/examples/capabilities/business.yaml +80 -142
  97. package/examples/capabilities/delivery.yaml +155 -219
  98. package/examples/capabilities/people.yaml +2 -34
  99. package/examples/capabilities/reliability.yaml +161 -80
  100. package/examples/capabilities/scale.yaml +234 -252
  101. package/examples/copilot-setup-steps.yaml +25 -0
  102. package/examples/devcontainer.yaml +21 -0
  103. package/examples/disciplines/_index.yaml +1 -0
  104. package/examples/disciplines/data_engineering.yaml +14 -12
  105. package/examples/disciplines/engineering_management.yaml +63 -0
  106. package/examples/disciplines/software_engineering.yaml +14 -12
  107. package/examples/drivers.yaml +1 -4
  108. package/examples/framework.yaml +1 -2
  109. package/examples/grades.yaml +14 -15
  110. package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
  111. package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
  112. package/examples/questions/behaviours/precise_communication.yaml +1 -2
  113. package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
  114. package/examples/questions/behaviours/systems_thinking.yaml +1 -2
  115. package/examples/questions/skills/architecture_design.yaml +1 -2
  116. package/examples/questions/skills/cloud_platforms.yaml +1 -2
  117. package/examples/questions/skills/code_quality.yaml +1 -2
  118. package/examples/questions/skills/data_modeling.yaml +1 -2
  119. package/examples/questions/skills/devops.yaml +1 -2
  120. package/examples/questions/skills/full_stack_development.yaml +1 -2
  121. package/examples/questions/skills/sre_practices.yaml +1 -2
  122. package/examples/questions/skills/stakeholder_management.yaml +1 -2
  123. package/examples/questions/skills/team_collaboration.yaml +1 -2
  124. package/examples/questions/skills/technical_writing.yaml +1 -2
  125. package/examples/self-assessments.yaml +1 -3
  126. package/examples/stages.yaml +101 -46
  127. package/examples/tracks/_index.yaml +0 -1
  128. package/examples/tracks/platform.yaml +8 -13
  129. package/examples/tracks/sre.yaml +8 -18
  130. package/examples/vscode-settings.yaml +2 -7
  131. package/package.json +9 -3
  132. package/templates/agent.template.md +65 -0
  133. package/templates/job.template.md +47 -0
  134. package/templates/skill.template.md +28 -0
  135. package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
  136. package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
  137. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
  138. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
  139. package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
  140. package/examples/tracks/manager.yaml +0 -53
@@ -3,41 +3,30 @@
3
3
  *
4
4
  * Formats job data into markdown job description content.
5
5
  * Parallels formatters/agent/profile.js in structure.
6
+ *
7
+ * Uses Mustache templates for flexible output formatting.
8
+ * Templates are loaded from data/ directory with fallback to templates/ directory.
6
9
  */
7
10
 
11
+ import Mustache from "mustache";
12
+
8
13
  import {
9
14
  SKILL_LEVEL_ORDER,
10
15
  BEHAVIOUR_MATURITY_ORDER,
11
16
  } from "../../model/levels.js";
12
17
 
13
18
  /**
14
- * Format job as a markdown job description
19
+ * Prepare job data for template rendering
15
20
  * @param {Object} params
16
21
  * @param {Object} params.job - The job definition
17
22
  * @param {Object} params.discipline - The discipline
18
23
  * @param {Object} params.grade - The grade
19
- * @param {Object} params.track - The track
20
- * @returns {string} Markdown formatted job description
24
+ * @param {Object} [params.track] - The track (optional)
25
+ * @returns {Object} Data object ready for Mustache template
21
26
  */
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
-
27
+ function prepareJobDescriptionData({ job, discipline, grade, track }) {
39
28
  // Build role summary from discipline - use manager version if applicable
40
- const isManagement = track.isManagement === true;
29
+ const isManagement = discipline.isManagement === true;
41
30
  let roleSummary =
42
31
  isManagement && discipline.managementRoleSummary
43
32
  ? discipline.managementRoleSummary
@@ -46,16 +35,9 @@ export function formatJobDescription({ job, discipline, grade, track }) {
46
35
  const { roleTitle, specialization } = discipline;
47
36
  roleSummary = roleSummary.replace(/\{roleTitle\}/g, roleTitle);
48
37
  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
38
 
58
- // Add grade expectations as natural paragraphs
39
+ // Build expectations paragraph
40
+ let expectationsParagraph = "";
59
41
  if (job.expectations) {
60
42
  const exp = job.expectations;
61
43
  const expectationSentences = [];
@@ -87,45 +69,20 @@ export function formatJobDescription({ job, discipline, grade, track }) {
87
69
  }
88
70
 
89
71
  if (expectationSentences.length > 0) {
90
- lines.push(expectationSentences.join(" "));
91
- lines.push("");
72
+ expectationsParagraph = expectationSentences.join(" ");
92
73
  }
93
74
  }
94
75
 
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
76
  // Sort behaviours by maturity level (highest first)
112
77
  const sortedBehaviours = [...job.behaviourProfile].sort((a, b) => {
113
78
  const indexA = BEHAVIOUR_MATURITY_ORDER.indexOf(a.maturity);
114
79
  const indexB = BEHAVIOUR_MATURITY_ORDER.indexOf(b.maturity);
115
- // Sort in reverse order (exemplifying first, emerging last)
116
80
  if (indexA === -1 && indexB === -1) return 0;
117
81
  if (indexA === -1) return 1;
118
82
  if (indexB === -1) return -1;
119
83
  return indexB - indexA;
120
84
  });
121
85
 
122
- for (const behaviour of sortedBehaviours) {
123
- lines.push(
124
- `- **${behaviour.behaviourName}:** ${behaviour.maturityDescription || ""}`,
125
- );
126
- }
127
- lines.push("");
128
-
129
86
  // Group skills by level
130
87
  const skillsByLevel = {};
131
88
  for (const skill of job.skillMatrix) {
@@ -136,41 +93,73 @@ export function formatJobDescription({ job, discipline, grade, track }) {
136
93
  skillsByLevel[level].push(skill);
137
94
  }
138
95
 
139
- // Sort levels in a logical order using SKILL_LEVEL_ORDER from types.js
96
+ // Sort levels in reverse order (expert first, awareness last)
140
97
  const sortedLevels = Object.keys(skillsByLevel).sort((a, b) => {
141
98
  const indexA = SKILL_LEVEL_ORDER.indexOf(a.toLowerCase());
142
99
  const indexB = SKILL_LEVEL_ORDER.indexOf(b.toLowerCase());
143
- // Sort in reverse order (expert first, awareness last)
144
100
  if (indexA === -1 && indexB === -1) return a.localeCompare(b);
145
101
  if (indexA === -1) return 1;
146
102
  if (indexB === -1) return -1;
147
103
  return indexB - indexA;
148
104
  });
149
105
 
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
- }
106
+ // Keep only the top 2 skill levels for job descriptions
107
+ const topLevels = sortedLevels.slice(0, 2);
165
108
 
166
- // Qualifications
167
- lines.push("## QUALIFICATIONS");
168
- lines.push("");
109
+ // Build skill levels array for template
110
+ const skillLevels = topLevels.map((level) => {
111
+ const skills = skillsByLevel[level];
112
+ const sortedSkills = [...skills].sort((a, b) =>
113
+ (a.skillName || "").localeCompare(b.skillName || ""),
114
+ );
115
+ return {
116
+ levelHeading: `${level.toUpperCase()}-LEVEL SKILLS`,
117
+ skills: sortedSkills.map((s) => ({
118
+ skillName: s.skillName,
119
+ levelDescription: s.levelDescription || "",
120
+ })),
121
+ };
122
+ });
169
123
 
170
- if (grade.qualificationSummary) {
171
- lines.push(grade.qualificationSummary);
172
- lines.push("");
173
- }
124
+ return {
125
+ title: job.title,
126
+ gradeId: grade.id,
127
+ typicalExperienceRange: grade.typicalExperienceRange,
128
+ trackName: track?.name || null,
129
+ roleSummary,
130
+ trackRoleContext: track?.roleContext || null,
131
+ expectationsParagraph: expectationsParagraph || null,
132
+ responsibilities: (job.derivedResponsibilities || []).map((r) => ({
133
+ capabilityName: r.capabilityName,
134
+ responsibility: r.responsibility,
135
+ })),
136
+ behaviours: sortedBehaviours.map((b) => ({
137
+ behaviourName: b.behaviourName,
138
+ maturityDescription: b.maturityDescription || "",
139
+ })),
140
+ skillLevels,
141
+ qualificationSummary:
142
+ (grade.qualificationSummary || "").replace(
143
+ /\{typicalExperienceRange\}/g,
144
+ grade.typicalExperienceRange || "",
145
+ ) || null,
146
+ };
147
+ }
174
148
 
175
- return lines.join("\n");
149
+ /**
150
+ * Format job as a markdown job description using Mustache template
151
+ * @param {Object} params
152
+ * @param {Object} params.job - The job definition
153
+ * @param {Object} params.discipline - The discipline
154
+ * @param {Object} params.grade - The grade
155
+ * @param {Object} [params.track] - The track (optional)
156
+ * @param {string} template - Mustache template string
157
+ * @returns {string} Markdown formatted job description
158
+ */
159
+ export function formatJobDescription(
160
+ { job, discipline, grade, track },
161
+ template,
162
+ ) {
163
+ const data = prepareJobDescriptionData({ job, discipline, grade, track });
164
+ return Mustache.render(template, data);
176
165
  }
@@ -2,18 +2,7 @@
2
2
  * Job formatting for DOM/web output
3
3
  */
4
4
 
5
- import {
6
- div,
7
- h1,
8
- h2,
9
- p,
10
- a,
11
- span,
12
- button,
13
- section,
14
- details,
15
- summary,
16
- } from "../../lib/render.js";
5
+ import { div, h1, h2, p, a, span, button, section } from "../../lib/render.js";
17
6
  import { createBackLink } from "../../components/nav.js";
18
7
  import {
19
8
  createDetailSection,
@@ -39,6 +28,7 @@ import { formatJobDescription } from "./description.js";
39
28
  * @param {Object} [options.discipline] - Discipline entity for job description
40
29
  * @param {Object} [options.grade] - Grade entity for job description
41
30
  * @param {Object} [options.track] - Track entity for job description
31
+ * @param {string} [options.jobTemplate] - Mustache template for job description
42
32
  * @returns {HTMLElement}
43
33
  */
44
34
  export function jobToDOM(view, options = {}) {
@@ -50,9 +40,10 @@ export function jobToDOM(view, options = {}) {
50
40
  discipline,
51
41
  grade,
52
42
  track,
43
+ jobTemplate,
53
44
  } = options;
54
45
 
55
- const hasEntities = discipline && grade && track;
46
+ const hasEntities = discipline && grade && jobTemplate;
56
47
 
57
48
  return div(
58
49
  { className: "job-detail-page" },
@@ -108,6 +99,7 @@ export function jobToDOM(view, options = {}) {
108
99
  discipline,
109
100
  grade,
110
101
  track,
102
+ template: jobTemplate,
111
103
  })
112
104
  : null,
113
105
 
@@ -140,14 +132,6 @@ export function jobToDOM(view, options = {}) {
140
132
  ),
141
133
  })
142
134
  : null,
143
-
144
- // Handoff Checklists
145
- view.checklists && hasChecklistItems(view.checklists)
146
- ? createDetailSection({
147
- title: "📋 Handoff Checklists",
148
- content: createChecklistSections(view.checklists),
149
- })
150
- : null,
151
135
  )
152
136
  : null,
153
137
 
@@ -164,6 +148,7 @@ export function jobToDOM(view, options = {}) {
164
148
  discipline,
165
149
  grade,
166
150
  track,
151
+ template: jobTemplate,
167
152
  })
168
153
  : null,
169
154
  );
@@ -211,84 +196,6 @@ function getScoreColor(score) {
211
196
  return "#ef4444"; // Red
212
197
  }
213
198
 
214
- /**
215
- * Check if any checklist has items
216
- * @param {Object} checklists - Checklists object keyed by handoff type
217
- * @returns {boolean}
218
- */
219
- function hasChecklistItems(checklists) {
220
- for (const items of Object.values(checklists)) {
221
- if (items && items.length > 0) {
222
- return true;
223
- }
224
- }
225
- return false;
226
- }
227
-
228
- /**
229
- * Create collapsible checklist sections for all handoffs
230
- * @param {Object} checklists - Checklists object keyed by handoff type
231
- * @returns {HTMLElement}
232
- */
233
- function createChecklistSections(checklists) {
234
- const handoffLabels = {
235
- plan_to_code: "📋 → 💻 Plan → Code",
236
- code_to_review: "💻 → ✅ Code → Review",
237
- };
238
-
239
- const sections = Object.entries(checklists)
240
- .filter(([_, items]) => items && items.length > 0)
241
- .map(([handoff, groups]) => {
242
- const label = handoffLabels[handoff] || handoff;
243
- const totalItems = groups.reduce((sum, g) => sum + g.items.length, 0);
244
-
245
- return details(
246
- { className: "checklist-section" },
247
- summary(
248
- { className: "checklist-section-header" },
249
- span({ className: "checklist-section-label" }, label),
250
- span({ className: "badge badge-default" }, `${totalItems} items`),
251
- ),
252
- div(
253
- { className: "checklist-section-content" },
254
- ...groups.map((group) => createChecklistGroup(group)),
255
- ),
256
- );
257
- });
258
-
259
- return div({ className: "checklist-sections" }, ...sections);
260
- }
261
-
262
- /**
263
- * Create a checklist group for a capability
264
- * @param {Object} group - Group with capability, level, and items
265
- * @returns {HTMLElement}
266
- */
267
- function createChecklistGroup(group) {
268
- const emoji = group.capability.emoji || "📌";
269
- const capabilityName = group.capability.name || group.capability.id;
270
-
271
- return div(
272
- { className: "checklist-group" },
273
- div(
274
- { className: "checklist-group-header" },
275
- span({ className: "checklist-emoji" }, emoji),
276
- span({ className: "checklist-capability" }, capabilityName),
277
- span({ className: "badge badge-secondary" }, group.level),
278
- ),
279
- div(
280
- { className: "checklist-items" },
281
- ...group.items.map((item) =>
282
- div(
283
- { className: "checklist-item" },
284
- span({ className: "checklist-checkbox" }, "☐"),
285
- span({}, item),
286
- ),
287
- ),
288
- ),
289
- );
290
- }
291
-
292
199
  /**
293
200
  * Create the job description section with copy button
294
201
  * @param {Object} params
@@ -296,15 +203,25 @@ function createChecklistGroup(group) {
296
203
  * @param {Object} params.discipline - The discipline
297
204
  * @param {Object} params.grade - The grade
298
205
  * @param {Object} params.track - The track
206
+ * @param {string} params.template - Mustache template for job description
299
207
  * @returns {HTMLElement} The job description section element
300
208
  */
301
- export function createJobDescriptionSection({ job, discipline, grade, track }) {
302
- const markdown = formatJobDescription({
303
- job,
304
- discipline,
305
- grade,
306
- track,
307
- });
209
+ export function createJobDescriptionSection({
210
+ job,
211
+ discipline,
212
+ grade,
213
+ track,
214
+ template,
215
+ }) {
216
+ const markdown = formatJobDescription(
217
+ {
218
+ job,
219
+ discipline,
220
+ grade,
221
+ track,
222
+ },
223
+ template,
224
+ );
308
225
 
309
226
  const copyButton = button(
310
227
  {
@@ -388,15 +305,25 @@ export function createJobDescriptionSection({ job, discipline, grade, track }) {
388
305
  * @param {Object} params.discipline - The discipline
389
306
  * @param {Object} params.grade - The grade
390
307
  * @param {Object} params.track - The track
308
+ * @param {string} params.template - Mustache template for job description
391
309
  * @returns {HTMLElement} The job description HTML element (print-only)
392
310
  */
393
- export function createJobDescriptionHtml({ job, discipline, grade, track }) {
394
- const markdown = formatJobDescription({
395
- job,
396
- discipline,
397
- grade,
398
- track,
399
- });
311
+ export function createJobDescriptionHtml({
312
+ job,
313
+ discipline,
314
+ grade,
315
+ track,
316
+ template,
317
+ }) {
318
+ const markdown = formatJobDescription(
319
+ {
320
+ job,
321
+ discipline,
322
+ grade,
323
+ track,
324
+ },
325
+ template,
326
+ );
400
327
 
401
328
  const html = markdownToHtml(markdown);
402
329
 
@@ -15,9 +15,10 @@ import { SKILL_LEVEL_ORDER } from "../../model/levels.js";
15
15
  * Format job detail as markdown
16
16
  * @param {Object} view - Job detail view from presenter
17
17
  * @param {Object} [entities] - Original entities (for job description)
18
+ * @param {string} [jobTemplate] - Mustache template for job description
18
19
  * @returns {string}
19
20
  */
20
- export function jobToMarkdown(view, entities = {}) {
21
+ export function jobToMarkdown(view, entities = {}, jobTemplate) {
21
22
  const lines = [
22
23
  `# ${view.title}`,
23
24
  "",
@@ -77,23 +78,26 @@ export function jobToMarkdown(view, entities = {}) {
77
78
  }
78
79
 
79
80
  // Job Description (copyable markdown)
80
- if (entities.discipline && entities.grade && entities.track) {
81
+ if (entities.discipline && entities.grade && jobTemplate) {
81
82
  lines.push("---", "");
82
83
  lines.push("## Job Description", "");
83
84
  lines.push("```markdown");
84
85
  lines.push(
85
- formatJobDescription({
86
- job: {
87
- title: view.title,
88
- skillMatrix: view.skillMatrix,
89
- behaviourProfile: view.behaviourProfile,
90
- expectations: view.expectations,
91
- derivedResponsibilities: view.derivedResponsibilities,
86
+ formatJobDescription(
87
+ {
88
+ job: {
89
+ title: view.title,
90
+ skillMatrix: view.skillMatrix,
91
+ behaviourProfile: view.behaviourProfile,
92
+ expectations: view.expectations,
93
+ derivedResponsibilities: view.derivedResponsibilities,
94
+ },
95
+ discipline: entities.discipline,
96
+ grade: entities.grade,
97
+ track: entities.track,
92
98
  },
93
- discipline: entities.discipline,
94
- grade: entities.grade,
95
- track: entities.track,
96
- }),
99
+ jobTemplate,
100
+ ),
97
101
  );
98
102
  lines.push("```");
99
103
  }