@forwardimpact/pathway 0.25.15 → 0.25.21

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 (34) hide show
  1. package/bin/fit-pathway.js +62 -54
  2. package/package.json +1 -3
  3. package/src/commands/agent-io.js +120 -0
  4. package/src/commands/agent.js +266 -349
  5. package/src/commands/init.js +2 -2
  6. package/src/commands/job.js +237 -183
  7. package/src/components/comparison-radar.js +118 -103
  8. package/src/components/progression-table.js +244 -208
  9. package/src/formatters/index.js +0 -19
  10. package/src/formatters/interview/markdown.js +100 -88
  11. package/src/formatters/job/description.js +76 -75
  12. package/src/formatters/job/dom.js +113 -97
  13. package/src/formatters/level/dom.js +87 -102
  14. package/src/formatters/questions/markdown.js +37 -33
  15. package/src/formatters/questions/shared.js +142 -75
  16. package/src/formatters/skill/dom.js +102 -93
  17. package/src/lib/comparison-radar-chart.js +256 -0
  18. package/src/lib/radar-utils.js +199 -0
  19. package/src/lib/radar.js +25 -662
  20. package/src/pages/agent-builder-download.js +170 -0
  21. package/src/pages/agent-builder-preview.js +344 -0
  22. package/src/pages/agent-builder.js +6 -550
  23. package/src/pages/progress-comparison.js +110 -0
  24. package/src/pages/progress.js +11 -111
  25. package/src/pages/self-assessment-steps.js +494 -0
  26. package/src/pages/self-assessment.js +54 -504
  27. package/src/formatters/behaviour/microdata.js +0 -106
  28. package/src/formatters/discipline/microdata.js +0 -117
  29. package/src/formatters/driver/microdata.js +0 -91
  30. package/src/formatters/level/microdata.js +0 -141
  31. package/src/formatters/microdata-shared.js +0 -184
  32. package/src/formatters/skill/microdata.js +0 -151
  33. package/src/formatters/stage/microdata.js +0 -116
  34. package/src/formatters/track/microdata.js +0 -111
@@ -5,6 +5,91 @@
5
5
  import { formatLevel } from "../../lib/render.js";
6
6
  import { getConceptEmoji } from "@forwardimpact/map/levels";
7
7
 
8
+ /**
9
+ * Append follow-ups to lines
10
+ * @param {string[]} lines
11
+ * @param {Object} q - Question object
12
+ */
13
+ function appendFollowUps(lines, q) {
14
+ if (q.followUps.length > 0) {
15
+ lines.push("", "**Follow-ups:**");
16
+ for (const followUp of q.followUps) {
17
+ lines.push(` → ${followUp}`);
18
+ }
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Append looking-for items to lines
24
+ * @param {string[]} lines
25
+ * @param {Object} q - Question object
26
+ */
27
+ function appendLookingFor(lines, q) {
28
+ if (q.lookingFor && q.lookingFor.length > 0) {
29
+ lines.push("", "**What to look for:**");
30
+ for (const item of q.lookingFor) {
31
+ lines.push(`- ${item}`);
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Format skill question sections
38
+ * @param {string[]} lines
39
+ * @param {Array} sections
40
+ * @param {string} emoji
41
+ */
42
+ function formatSkillSections(lines, sections, emoji) {
43
+ if (sections.length === 0) return;
44
+ lines.push(`## ${emoji} Skill Questions`, "");
45
+ for (const section of sections) {
46
+ lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
47
+ for (const q of section.questions) {
48
+ lines.push(`**Q**: ${q.question}`);
49
+ appendFollowUps(lines, q);
50
+ appendLookingFor(lines, q);
51
+ lines.push("");
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Format scenario-based question sections (capability or behaviour)
58
+ * @param {string[]} lines
59
+ * @param {Array} sections
60
+ * @param {string} heading
61
+ * @param {string} promptsKey - Key for guided prompts
62
+ * @param {string} promptsLabel - Display label for prompts
63
+ */
64
+ function formatScenarioSections(
65
+ lines,
66
+ sections,
67
+ heading,
68
+ promptsKey,
69
+ promptsLabel,
70
+ ) {
71
+ if (sections.length === 0) return;
72
+ lines.push(`## ${heading}`, "");
73
+ for (const section of sections) {
74
+ lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
75
+ for (const q of section.questions) {
76
+ lines.push(`**Scenario**: ${q.question}`);
77
+ if (q.context) {
78
+ lines.push(`> ${q.context}`);
79
+ }
80
+ if (q[promptsKey] && q[promptsKey].length > 0) {
81
+ lines.push("", `**${promptsLabel}:**`);
82
+ for (const prompt of q[promptsKey]) {
83
+ lines.push(`- ${prompt}`);
84
+ }
85
+ }
86
+ appendFollowUps(lines, q);
87
+ appendLookingFor(lines, q);
88
+ lines.push("");
89
+ }
90
+ }
91
+ }
92
+
8
93
  /**
9
94
  * Format interview detail as markdown
10
95
  * @param {Object} view - Interview detail view from presenter
@@ -24,102 +109,29 @@ export function interviewToMarkdown(view, { framework } = {}) {
24
109
  "",
25
110
  ];
26
111
 
27
- // Group sections by type
28
112
  const skillSections = view.sections.filter((s) => s.type === "skill");
29
113
  const behaviourSections = view.sections.filter((s) => s.type === "behaviour");
30
114
  const capabilitySections = view.sections.filter(
31
115
  (s) => s.type === "capability",
32
116
  );
33
117
 
34
- // Skill questions
35
- if (skillSections.length > 0) {
36
- lines.push(`## ${skillEmoji} Skill Questions`, "");
37
- for (const section of skillSections) {
38
- lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
39
- for (const q of section.questions) {
40
- lines.push(`**Q**: ${q.question}`);
41
- if (q.followUps.length > 0) {
42
- lines.push("", "**Follow-ups:**");
43
- for (const followUp of q.followUps) {
44
- lines.push(` → ${followUp}`);
45
- }
46
- }
47
- if (q.lookingFor && q.lookingFor.length > 0) {
48
- lines.push("", "**What to look for:**");
49
- for (const item of q.lookingFor) {
50
- lines.push(`- ${item}`);
51
- }
52
- }
53
- lines.push("");
54
- }
55
- }
56
- }
118
+ formatSkillSections(lines, skillSections, skillEmoji);
57
119
 
58
- // Capability decomposition questions
59
- if (capabilitySections.length > 0) {
60
- lines.push(`## 🧩 Decomposition Questions`, "");
61
- for (const section of capabilitySections) {
62
- lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
63
- for (const q of section.questions) {
64
- lines.push(`**Scenario**: ${q.question}`);
65
- if (q.context) {
66
- lines.push(`> ${q.context}`);
67
- }
68
- if (q.decompositionPrompts && q.decompositionPrompts.length > 0) {
69
- lines.push("", "**Guide the candidate through:**");
70
- for (const prompt of q.decompositionPrompts) {
71
- lines.push(`- ${prompt}`);
72
- }
73
- }
74
- if (q.followUps.length > 0) {
75
- lines.push("", "**Follow-ups:**");
76
- for (const followUp of q.followUps) {
77
- lines.push(` → ${followUp}`);
78
- }
79
- }
80
- if (q.lookingFor && q.lookingFor.length > 0) {
81
- lines.push("", "**What to look for:**");
82
- for (const item of q.lookingFor) {
83
- lines.push(`- ${item}`);
84
- }
85
- }
86
- lines.push("");
87
- }
88
- }
89
- }
120
+ formatScenarioSections(
121
+ lines,
122
+ capabilitySections,
123
+ "🧩 Decomposition Questions",
124
+ "decompositionPrompts",
125
+ "Guide the candidate through",
126
+ );
90
127
 
91
- // Behaviour stakeholder simulation questions
92
- if (behaviourSections.length > 0) {
93
- lines.push(`## ${behaviourEmoji} Stakeholder Simulation`, "");
94
- for (const section of behaviourSections) {
95
- lines.push(`### ${section.name} (${formatLevel(section.level)})`, "");
96
- for (const q of section.questions) {
97
- lines.push(`**Scenario**: ${q.question}`);
98
- if (q.context) {
99
- lines.push(`> ${q.context}`);
100
- }
101
- if (q.simulationPrompts && q.simulationPrompts.length > 0) {
102
- lines.push("", "**Steer the simulation:**");
103
- for (const prompt of q.simulationPrompts) {
104
- lines.push(`- ${prompt}`);
105
- }
106
- }
107
- if (q.followUps.length > 0) {
108
- lines.push("", "**Follow-ups:**");
109
- for (const followUp of q.followUps) {
110
- lines.push(` → ${followUp}`);
111
- }
112
- }
113
- if (q.lookingFor && q.lookingFor.length > 0) {
114
- lines.push("", "**What to look for:**");
115
- for (const item of q.lookingFor) {
116
- lines.push(`- ${item}`);
117
- }
118
- }
119
- lines.push("");
120
- }
121
- }
122
- }
128
+ formatScenarioSections(
129
+ lines,
130
+ behaviourSections,
131
+ `${behaviourEmoji} Stakeholder Simulation`,
132
+ "simulationPrompts",
133
+ "Steer the simulation",
134
+ );
123
135
 
124
136
  return lines.join("\n");
125
137
  }
@@ -13,6 +13,80 @@ import Mustache from "mustache";
13
13
  import { BEHAVIOUR_MATURITY_ORDER } from "@forwardimpact/map/levels";
14
14
  import { trimValue, trimFields } from "../shared.js";
15
15
 
16
+ /**
17
+ * Build expectations paragraph from job expectations
18
+ * @param {Object|undefined} expectations
19
+ * @returns {string}
20
+ */
21
+ function buildExpectationsParagraph(expectations) {
22
+ if (!expectations) return "";
23
+ const exp = expectations;
24
+ const sentences = [];
25
+
26
+ if (exp.impactScope) {
27
+ sentences.push(`This role encompasses ${exp.impactScope.toLowerCase()}.`);
28
+ }
29
+ if (exp.autonomyExpectation) {
30
+ let autonomySentence = `You will ${exp.autonomyExpectation.toLowerCase()}`;
31
+ if (exp.influenceScope) {
32
+ autonomySentence +=
33
+ `, ${exp.influenceScope.toLowerCase()}` +
34
+ (exp.influenceScope.endsWith(".") ? "" : ".");
35
+ } else {
36
+ autonomySentence += exp.autonomyExpectation.endsWith(".") ? "" : ".";
37
+ }
38
+ sentences.push(autonomySentence);
39
+ } else if (exp.influenceScope) {
40
+ sentences.push(
41
+ exp.influenceScope + (exp.influenceScope.endsWith(".") ? "" : "."),
42
+ );
43
+ }
44
+ if (exp.complexityHandled) {
45
+ sentences.push(`You will handle ${exp.complexityHandled.toLowerCase()}.`);
46
+ }
47
+ return sentences.length > 0 ? sentences.join(" ") : "";
48
+ }
49
+
50
+ /**
51
+ * Build capability skill sections at the highest proficiency
52
+ * @param {Object} job
53
+ * @returns {Array}
54
+ */
55
+ function buildCapabilitySkills(job) {
56
+ const derivedResponsibilities = job.derivedResponsibilities || [];
57
+ if (derivedResponsibilities.length === 0) return [];
58
+
59
+ const highestProficiency = derivedResponsibilities[0].proficiency;
60
+ const topResponsibilities = derivedResponsibilities.filter(
61
+ (r) => r.proficiency === highestProficiency,
62
+ );
63
+
64
+ const skillsByCapability = {};
65
+ for (const skill of job.skillMatrix) {
66
+ if (skill.proficiency !== highestProficiency) continue;
67
+ if (!skillsByCapability[skill.capability]) {
68
+ skillsByCapability[skill.capability] = [];
69
+ }
70
+ skillsByCapability[skill.capability].push(skill);
71
+ }
72
+
73
+ return topResponsibilities
74
+ .filter((r) => skillsByCapability[r.capability]?.length > 0)
75
+ .map((r) => {
76
+ const skills = [...skillsByCapability[r.capability]].sort((a, b) =>
77
+ (a.skillName || "").localeCompare(b.skillName || ""),
78
+ );
79
+ return {
80
+ capabilityHeading: r.capabilityName.toUpperCase(),
81
+ responsibilityDescription: r.responsibility,
82
+ skills: skills.map((s) => ({
83
+ skillName: s.skillName,
84
+ proficiencyDescription: s.proficiencyDescription || "",
85
+ })),
86
+ };
87
+ });
88
+ }
89
+
16
90
  /**
17
91
  * Prepare job data for template rendering
18
92
  * @param {Object} params
@@ -29,42 +103,7 @@ function prepareJobDescriptionData({ job, discipline, level, track }) {
29
103
  roleSummary = roleSummary.replace(/\{roleTitle\}/g, roleTitle);
30
104
  roleSummary = roleSummary.replace(/\{specialization\}/g, specialization);
31
105
 
32
- // Build expectations paragraph
33
- let expectationsParagraph = "";
34
- if (job.expectations) {
35
- const exp = job.expectations;
36
- const expectationSentences = [];
37
-
38
- if (exp.impactScope) {
39
- expectationSentences.push(
40
- `This role encompasses ${exp.impactScope.toLowerCase()}.`,
41
- );
42
- }
43
- if (exp.autonomyExpectation) {
44
- let autonomySentence = `You will ${exp.autonomyExpectation.toLowerCase()}`;
45
- if (exp.influenceScope) {
46
- autonomySentence +=
47
- `, ${exp.influenceScope.toLowerCase()}` +
48
- (exp.influenceScope.endsWith(".") ? "" : ".");
49
- } else {
50
- autonomySentence += exp.autonomyExpectation.endsWith(".") ? "" : ".";
51
- }
52
- expectationSentences.push(autonomySentence);
53
- } else if (exp.influenceScope) {
54
- expectationSentences.push(
55
- exp.influenceScope + (exp.influenceScope.endsWith(".") ? "" : "."),
56
- );
57
- }
58
- if (exp.complexityHandled) {
59
- expectationSentences.push(
60
- `You will handle ${exp.complexityHandled.toLowerCase()}.`,
61
- );
62
- }
63
-
64
- if (expectationSentences.length > 0) {
65
- expectationsParagraph = expectationSentences.join(" ");
66
- }
67
- }
106
+ const expectationsParagraph = buildExpectationsParagraph(job.expectations);
68
107
 
69
108
  // Sort behaviours by maturity level (highest first)
70
109
  const sortedBehaviours = [...job.behaviourProfile].sort((a, b) => {
@@ -76,45 +115,7 @@ function prepareJobDescriptionData({ job, discipline, level, track }) {
76
115
  return indexB - indexA;
77
116
  });
78
117
 
79
- // Build capability skill sections at the highest skill proficiency
80
- let capabilitySkills = [];
81
- const derivedResponsibilities = job.derivedResponsibilities || [];
82
- if (derivedResponsibilities.length > 0) {
83
- // derivedResponsibilities is sorted: highest proficiency first, then by skill count
84
- const highestProficiency = derivedResponsibilities[0].proficiency;
85
-
86
- // Filter responsibilities to only the highest proficiency
87
- const topResponsibilities = derivedResponsibilities.filter(
88
- (r) => r.proficiency === highestProficiency,
89
- );
90
-
91
- // Group skill matrix entries by capability at the highest level
92
- const skillsByCapability = {};
93
- for (const skill of job.skillMatrix) {
94
- if (skill.proficiency !== highestProficiency) continue;
95
- if (!skillsByCapability[skill.capability]) {
96
- skillsByCapability[skill.capability] = [];
97
- }
98
- skillsByCapability[skill.capability].push(skill);
99
- }
100
-
101
- // Build capability sections in skill count order
102
- capabilitySkills = topResponsibilities
103
- .filter((r) => skillsByCapability[r.capability]?.length > 0)
104
- .map((r) => {
105
- const skills = [...skillsByCapability[r.capability]].sort((a, b) =>
106
- (a.skillName || "").localeCompare(b.skillName || ""),
107
- );
108
- return {
109
- capabilityHeading: r.capabilityName.toUpperCase(),
110
- responsibilityDescription: r.responsibility,
111
- skills: skills.map((s) => ({
112
- skillName: s.skillName,
113
- proficiencyDescription: s.proficiencyDescription || "",
114
- })),
115
- };
116
- });
117
- }
118
+ const capabilitySkills = buildCapabilitySkills(job);
118
119
 
119
120
  // Build qualification summary with placeholder replacement
120
121
  const qualificationSummary =
@@ -33,6 +33,107 @@ import { createToolkitTable } from "../toolkit/dom.js";
33
33
  * @param {string} [options.jobTemplate] - Mustache template for job description
34
34
  * @returns {HTMLElement}
35
35
  */
36
+ /**
37
+ * Build a job data object from a view for template rendering
38
+ * @param {Object} view
39
+ * @returns {Object}
40
+ */
41
+ function buildJobFromView(view) {
42
+ return {
43
+ title: view.title,
44
+ skillMatrix: view.skillMatrix,
45
+ behaviourProfile: view.behaviourProfile,
46
+ expectations: view.expectations,
47
+ derivedResponsibilities: view.derivedResponsibilities,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Create the tables section for a job detail view
53
+ * @param {Object} view
54
+ * @returns {HTMLElement}
55
+ */
56
+ function createJobTablesSection(view) {
57
+ return div(
58
+ { className: "job-tables-section" },
59
+ createDetailSection({
60
+ title: "Behaviour Profile",
61
+ content: createBehaviourProfile(view.behaviourProfile),
62
+ }),
63
+ createDetailSection({
64
+ title: "Skill Matrix",
65
+ content: createSkillMatrix(view.skillMatrix, {
66
+ capabilityOrder: view.capabilityOrder,
67
+ }),
68
+ }),
69
+ view.toolkit && view.toolkit.length > 0
70
+ ? createDetailSection({
71
+ title: "Tool Kit",
72
+ content: createToolkitTable(view.toolkit),
73
+ })
74
+ : null,
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Create the page header with breadcrumb links
80
+ * @param {Object} view
81
+ * @param {boolean} showBackLink
82
+ * @returns {HTMLElement}
83
+ */
84
+ function createJobHeader(view, showBackLink) {
85
+ return div(
86
+ { className: "page-header" },
87
+ showBackLink
88
+ ? createBackLink("/job-builder", "← Back to Job Builder")
89
+ : null,
90
+ h1({ className: "page-title" }, view.title),
91
+ div(
92
+ { className: "page-description" },
93
+ "Generated from: ",
94
+ a({ href: `#/discipline/${view.disciplineId}` }, view.disciplineName),
95
+ " × ",
96
+ a({ href: `#/level/${view.levelId}` }, view.levelId),
97
+ " × ",
98
+ a({ href: `#/track/${view.trackId}` }, view.trackName),
99
+ ),
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Create the expectations section if expectations exist
105
+ * @param {Object} view
106
+ * @returns {HTMLElement|null}
107
+ */
108
+ function createExpectationsSection(view) {
109
+ if (!view.expectations || Object.keys(view.expectations).length === 0) {
110
+ return null;
111
+ }
112
+ return createDetailSection({
113
+ title: "Expectations",
114
+ content: createExpectationsCard(view.expectations),
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Create the radar charts section
120
+ * @param {Object} view
121
+ * @returns {HTMLElement}
122
+ */
123
+ function createRadarSection(view) {
124
+ return div(
125
+ { className: "section auto-grid-lg" },
126
+ createBehaviourRadar(view.behaviourProfile, {
127
+ title: "Behaviours Radar",
128
+ size: 420,
129
+ }),
130
+ createSkillRadar(view.skillMatrix, {
131
+ title: "Skills Radar",
132
+ size: 420,
133
+ }),
134
+ );
135
+ }
136
+
36
137
  export function jobToDOM(view, options = {}) {
37
138
  const {
38
139
  showBackLink = true,
@@ -46,107 +147,22 @@ export function jobToDOM(view, options = {}) {
46
147
  } = options;
47
148
 
48
149
  const hasEntities = discipline && level && jobTemplate;
150
+ const job = hasEntities ? buildJobFromView(view) : null;
151
+ const descParams = hasEntities
152
+ ? { job, discipline, level, track, template: jobTemplate }
153
+ : null;
49
154
 
50
155
  return div(
51
156
  { className: "job-detail-page" },
52
- // Header
53
- div(
54
- { className: "page-header" },
55
- showBackLink
56
- ? createBackLink("/job-builder", "← Back to Job Builder")
57
- : null,
58
- h1({ className: "page-title" }, view.title),
59
- div(
60
- { className: "page-description" },
61
- "Generated from: ",
62
- a({ href: `#/discipline/${view.disciplineId}` }, view.disciplineName),
63
- " × ",
64
- a({ href: `#/level/${view.levelId}` }, view.levelId),
65
- " × ",
66
- a({ href: `#/track/${view.trackId}` }, view.trackName),
67
- ),
68
- ),
69
-
70
- // Expectations card
71
- view.expectations && Object.keys(view.expectations).length > 0
72
- ? createDetailSection({
73
- title: "Expectations",
74
- content: createExpectationsCard(view.expectations),
75
- })
157
+ createJobHeader(view, showBackLink),
158
+ createExpectationsSection(view),
159
+ createRadarSection(view),
160
+ showJobDescriptionHtml && descParams
161
+ ? createJobDescriptionHtml(descParams)
76
162
  : null,
77
-
78
- // Radar charts
79
- div(
80
- { className: "section auto-grid-lg" },
81
- createBehaviourRadar(view.behaviourProfile, {
82
- title: "Behaviours Radar",
83
- size: 420,
84
- }),
85
- createSkillRadar(view.skillMatrix, {
86
- title: "Skills Radar",
87
- size: 420,
88
- }),
89
- ),
90
-
91
- // Job Description HTML (for print view)
92
- showJobDescriptionHtml && hasEntities
93
- ? createJobDescriptionHtml({
94
- job: {
95
- title: view.title,
96
- skillMatrix: view.skillMatrix,
97
- behaviourProfile: view.behaviourProfile,
98
- expectations: view.expectations,
99
- derivedResponsibilities: view.derivedResponsibilities,
100
- },
101
- discipline,
102
- level,
103
- track,
104
- template: jobTemplate,
105
- })
106
- : null,
107
-
108
- // Behaviour profile, Skill matrix, Toolkit, Driver coverage tables
109
- showTables
110
- ? div(
111
- { className: "job-tables-section" },
112
- // Behaviour profile table
113
- createDetailSection({
114
- title: "Behaviour Profile",
115
- content: createBehaviourProfile(view.behaviourProfile),
116
- }),
117
-
118
- createDetailSection({
119
- title: "Skill Matrix",
120
- content: createSkillMatrix(view.skillMatrix, {
121
- capabilityOrder: view.capabilityOrder,
122
- }),
123
- }),
124
-
125
- // Toolkit (after skill matrix)
126
- view.toolkit && view.toolkit.length > 0
127
- ? createDetailSection({
128
- title: "Tool Kit",
129
- content: createToolkitTable(view.toolkit),
130
- })
131
- : null,
132
- )
133
- : null,
134
-
135
- // Job Description (copyable markdown)
136
- showJobDescriptionMarkdown && hasEntities
137
- ? createJobDescriptionSection({
138
- job: {
139
- title: view.title,
140
- skillMatrix: view.skillMatrix,
141
- behaviourProfile: view.behaviourProfile,
142
- expectations: view.expectations,
143
- derivedResponsibilities: view.derivedResponsibilities,
144
- },
145
- discipline,
146
- level,
147
- track,
148
- template: jobTemplate,
149
- })
163
+ showTables ? createJobTablesSection(view) : null,
164
+ showJobDescriptionMarkdown && descParams
165
+ ? createJobDescriptionSection(descParams)
150
166
  : null,
151
167
  );
152
168
  }