@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.
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/app/commands/agent.js +430 -0
- package/app/commands/behaviour.js +61 -0
- package/app/commands/command-factory.js +211 -0
- package/app/commands/discipline.js +58 -0
- package/app/commands/driver.js +94 -0
- package/app/commands/grade.js +60 -0
- package/app/commands/index.js +20 -0
- package/app/commands/init.js +67 -0
- package/app/commands/interview.js +68 -0
- package/app/commands/job.js +157 -0
- package/app/commands/progress.js +77 -0
- package/app/commands/questions.js +179 -0
- package/app/commands/serve.js +143 -0
- package/app/commands/site.js +121 -0
- package/app/commands/skill.js +76 -0
- package/app/commands/stage.js +129 -0
- package/app/commands/track.js +70 -0
- package/app/components/action-buttons.js +66 -0
- package/app/components/behaviour-profile.js +53 -0
- package/app/components/builder.js +341 -0
- package/app/components/card.js +98 -0
- package/app/components/checklist.js +145 -0
- package/app/components/comparison-radar.js +237 -0
- package/app/components/detail.js +230 -0
- package/app/components/error-page.js +72 -0
- package/app/components/grid.js +109 -0
- package/app/components/list.js +120 -0
- package/app/components/modifier-table.js +142 -0
- package/app/components/nav.js +64 -0
- package/app/components/progression-table.js +320 -0
- package/app/components/radar-chart.js +102 -0
- package/app/components/skill-matrix.js +97 -0
- package/app/css/base.css +56 -0
- package/app/css/bundles/app.css +40 -0
- package/app/css/bundles/handout.css +43 -0
- package/app/css/bundles/slides.css +40 -0
- package/app/css/components/badges.css +215 -0
- package/app/css/components/buttons.css +101 -0
- package/app/css/components/forms.css +105 -0
- package/app/css/components/layout.css +209 -0
- package/app/css/components/nav.css +166 -0
- package/app/css/components/progress.css +166 -0
- package/app/css/components/states.css +82 -0
- package/app/css/components/surfaces.css +243 -0
- package/app/css/components/tables.css +362 -0
- package/app/css/components/typography.css +122 -0
- package/app/css/components/utilities.css +41 -0
- package/app/css/pages/agent-builder.css +391 -0
- package/app/css/pages/assessment-results.css +453 -0
- package/app/css/pages/detail.css +59 -0
- package/app/css/pages/interview-builder.css +148 -0
- package/app/css/pages/job-builder.css +134 -0
- package/app/css/pages/landing.css +92 -0
- package/app/css/pages/lifecycle.css +118 -0
- package/app/css/pages/progress-builder.css +274 -0
- package/app/css/pages/self-assessment.css +502 -0
- package/app/css/reset.css +50 -0
- package/app/css/tokens.css +153 -0
- package/app/css/views/handout.css +30 -0
- package/app/css/views/print.css +608 -0
- package/app/css/views/slide-animations.css +113 -0
- package/app/css/views/slide-base.css +330 -0
- package/app/css/views/slide-sections.css +597 -0
- package/app/css/views/slide-tables.css +275 -0
- package/app/formatters/agent/dom.js +540 -0
- package/app/formatters/agent/profile.js +133 -0
- package/app/formatters/agent/skill.js +58 -0
- package/app/formatters/behaviour/dom.js +91 -0
- package/app/formatters/behaviour/markdown.js +54 -0
- package/app/formatters/behaviour/shared.js +64 -0
- package/app/formatters/discipline/dom.js +187 -0
- package/app/formatters/discipline/markdown.js +87 -0
- package/app/formatters/discipline/shared.js +131 -0
- package/app/formatters/driver/dom.js +103 -0
- package/app/formatters/driver/shared.js +92 -0
- package/app/formatters/grade/dom.js +208 -0
- package/app/formatters/grade/markdown.js +94 -0
- package/app/formatters/grade/shared.js +86 -0
- package/app/formatters/index.js +50 -0
- package/app/formatters/interview/dom.js +97 -0
- package/app/formatters/interview/markdown.js +66 -0
- package/app/formatters/interview/shared.js +332 -0
- package/app/formatters/job/description.js +176 -0
- package/app/formatters/job/dom.js +411 -0
- package/app/formatters/job/markdown.js +102 -0
- package/app/formatters/progress/dom.js +135 -0
- package/app/formatters/progress/markdown.js +86 -0
- package/app/formatters/progress/shared.js +339 -0
- package/app/formatters/questions/json.js +43 -0
- package/app/formatters/questions/markdown.js +303 -0
- package/app/formatters/questions/shared.js +274 -0
- package/app/formatters/questions/yaml.js +76 -0
- package/app/formatters/shared.js +71 -0
- package/app/formatters/skill/dom.js +168 -0
- package/app/formatters/skill/markdown.js +109 -0
- package/app/formatters/skill/shared.js +125 -0
- package/app/formatters/stage/dom.js +135 -0
- package/app/formatters/stage/index.js +12 -0
- package/app/formatters/stage/shared.js +111 -0
- package/app/formatters/track/dom.js +128 -0
- package/app/formatters/track/markdown.js +105 -0
- package/app/formatters/track/shared.js +181 -0
- package/app/handout-main.js +421 -0
- package/app/handout.html +21 -0
- package/app/index.html +59 -0
- package/app/lib/card-mappers.js +173 -0
- package/app/lib/cli-output.js +270 -0
- package/app/lib/error-boundary.js +70 -0
- package/app/lib/errors.js +49 -0
- package/app/lib/form-controls.js +47 -0
- package/app/lib/job-cache.js +86 -0
- package/app/lib/markdown.js +114 -0
- package/app/lib/radar.js +866 -0
- package/app/lib/reactive.js +77 -0
- package/app/lib/render.js +212 -0
- package/app/lib/router-core.js +160 -0
- package/app/lib/router-pages.js +16 -0
- package/app/lib/router-slides.js +202 -0
- package/app/lib/state.js +148 -0
- package/app/lib/utils.js +14 -0
- package/app/lib/yaml-loader.js +327 -0
- package/app/main.js +213 -0
- package/app/model/agent.js +702 -0
- package/app/model/checklist.js +137 -0
- package/app/model/derivation.js +699 -0
- package/app/model/index-generator.js +71 -0
- package/app/model/interview.js +539 -0
- package/app/model/job.js +222 -0
- package/app/model/levels.js +591 -0
- package/app/model/loader.js +564 -0
- package/app/model/matching.js +858 -0
- package/app/model/modifiers.js +158 -0
- package/app/model/profile.js +266 -0
- package/app/model/progression.js +507 -0
- package/app/model/validation.js +1385 -0
- package/app/pages/agent-builder.js +823 -0
- package/app/pages/assessment-results.js +507 -0
- package/app/pages/behaviour.js +70 -0
- package/app/pages/discipline.js +71 -0
- package/app/pages/driver.js +106 -0
- package/app/pages/grade.js +117 -0
- package/app/pages/interview-builder.js +50 -0
- package/app/pages/interview.js +304 -0
- package/app/pages/job-builder.js +50 -0
- package/app/pages/job.js +58 -0
- package/app/pages/landing.js +305 -0
- package/app/pages/progress-builder.js +58 -0
- package/app/pages/progress.js +495 -0
- package/app/pages/self-assessment.js +729 -0
- package/app/pages/skill.js +113 -0
- package/app/pages/stage.js +231 -0
- package/app/pages/track.js +69 -0
- package/app/slide-main.js +360 -0
- package/app/slides/behaviour.js +38 -0
- package/app/slides/chapter.js +82 -0
- package/app/slides/discipline.js +40 -0
- package/app/slides/driver.js +39 -0
- package/app/slides/grade.js +32 -0
- package/app/slides/index.js +198 -0
- package/app/slides/interview.js +58 -0
- package/app/slides/job.js +55 -0
- package/app/slides/overview.js +126 -0
- package/app/slides/progress.js +83 -0
- package/app/slides/skill.js +40 -0
- package/app/slides/track.js +39 -0
- package/app/slides.html +56 -0
- package/app/types.js +147 -0
- package/bin/pathway.js +489 -0
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
- package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
- package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
- package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
- package/examples/agents/.vscode/settings.json +8 -0
- package/examples/behaviours/_index.yaml +8 -0
- package/examples/behaviours/outcome_ownership.yaml +44 -0
- package/examples/behaviours/polymathic_knowledge.yaml +42 -0
- package/examples/behaviours/precise_communication.yaml +40 -0
- package/examples/behaviours/relentless_curiosity.yaml +38 -0
- package/examples/behaviours/systems_thinking.yaml +41 -0
- package/examples/capabilities/_index.yaml +8 -0
- package/examples/capabilities/business.yaml +251 -0
- package/examples/capabilities/delivery.yaml +352 -0
- package/examples/capabilities/people.yaml +100 -0
- package/examples/capabilities/reliability.yaml +318 -0
- package/examples/capabilities/scale.yaml +394 -0
- package/examples/disciplines/_index.yaml +5 -0
- package/examples/disciplines/data_engineering.yaml +76 -0
- package/examples/disciplines/software_engineering.yaml +76 -0
- package/examples/drivers.yaml +205 -0
- package/examples/framework.yaml +58 -0
- package/examples/grades.yaml +118 -0
- package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
- package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
- package/examples/questions/behaviours/precise_communication.yaml +55 -0
- package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
- package/examples/questions/behaviours/systems_thinking.yaml +53 -0
- package/examples/questions/skills/architecture_design.yaml +54 -0
- package/examples/questions/skills/cloud_platforms.yaml +48 -0
- package/examples/questions/skills/code_quality.yaml +49 -0
- package/examples/questions/skills/data_modeling.yaml +46 -0
- package/examples/questions/skills/devops.yaml +47 -0
- package/examples/questions/skills/full_stack_development.yaml +48 -0
- package/examples/questions/skills/sre_practices.yaml +44 -0
- package/examples/questions/skills/stakeholder_management.yaml +49 -0
- package/examples/questions/skills/team_collaboration.yaml +43 -0
- package/examples/questions/skills/technical_writing.yaml +43 -0
- package/examples/self-assessments.yaml +66 -0
- package/examples/stages.yaml +76 -0
- package/examples/tracks/_index.yaml +6 -0
- package/examples/tracks/manager.yaml +53 -0
- package/examples/tracks/platform.yaml +54 -0
- package/examples/tracks/sre.yaml +58 -0
- package/examples/vscode-settings.yaml +22 -0
- 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
|
+
}
|