@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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drivers pages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, div, h1, h2, p, section } from "../lib/render.js";
|
|
6
|
+
import { getState } from "../lib/state.js";
|
|
7
|
+
import { createCardList } from "../components/list.js";
|
|
8
|
+
import { createDetailHeader, createLinksList } from "../components/detail.js";
|
|
9
|
+
import { renderNotFound } from "../components/error-page.js";
|
|
10
|
+
import {
|
|
11
|
+
prepareDriversList,
|
|
12
|
+
prepareDriverDetail,
|
|
13
|
+
} from "../formatters/driver/shared.js";
|
|
14
|
+
import { driverToCardConfig } from "../lib/card-mappers.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render drivers list page
|
|
18
|
+
*/
|
|
19
|
+
export function renderDriversList() {
|
|
20
|
+
const { data } = getState();
|
|
21
|
+
const { framework } = data;
|
|
22
|
+
|
|
23
|
+
// Transform data for list view
|
|
24
|
+
const { items } = prepareDriversList(data.drivers);
|
|
25
|
+
|
|
26
|
+
const page = div(
|
|
27
|
+
{ className: "drivers-page" },
|
|
28
|
+
// Header
|
|
29
|
+
div(
|
|
30
|
+
{ className: "page-header" },
|
|
31
|
+
h1({ className: "page-title" }, framework.entityDefinitions.driver.title),
|
|
32
|
+
p(
|
|
33
|
+
{ className: "page-description" },
|
|
34
|
+
framework.entityDefinitions.driver.description.trim(),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
// Drivers list
|
|
39
|
+
createCardList(items, driverToCardConfig, "No drivers found."),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
render(page);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render driver detail page
|
|
47
|
+
* @param {Object} params - Route params
|
|
48
|
+
*/
|
|
49
|
+
export function renderDriverDetail(params) {
|
|
50
|
+
const { data } = getState();
|
|
51
|
+
const driver = data.drivers.find((d) => d.id === params.id);
|
|
52
|
+
|
|
53
|
+
if (!driver) {
|
|
54
|
+
renderNotFound({
|
|
55
|
+
entityType: "Driver",
|
|
56
|
+
entityId: params.id,
|
|
57
|
+
backPath: "/driver",
|
|
58
|
+
backText: "← Back to Drivers",
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Transform data for detail view
|
|
64
|
+
const view = prepareDriverDetail(driver, {
|
|
65
|
+
skills: data.skills,
|
|
66
|
+
behaviours: data.behaviours,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const page = div(
|
|
70
|
+
{ className: "driver-detail" },
|
|
71
|
+
createDetailHeader({
|
|
72
|
+
title: view.name,
|
|
73
|
+
description: view.description,
|
|
74
|
+
backLink: "/driver",
|
|
75
|
+
backText: "← Back to Drivers",
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
// Contributing Skills and Contributing Behaviours in two columns
|
|
79
|
+
view.contributingSkills.length > 0 || view.contributingBehaviours.length > 0
|
|
80
|
+
? section(
|
|
81
|
+
{ className: "section section-detail" },
|
|
82
|
+
div(
|
|
83
|
+
{ className: "content-columns" },
|
|
84
|
+
// Contributing Skills column
|
|
85
|
+
view.contributingSkills.length > 0
|
|
86
|
+
? div(
|
|
87
|
+
{ className: "column" },
|
|
88
|
+
h2({ className: "section-title" }, "Contributing Skills"),
|
|
89
|
+
createLinksList(view.contributingSkills, "/skill"),
|
|
90
|
+
)
|
|
91
|
+
: null,
|
|
92
|
+
// Contributing Behaviours column
|
|
93
|
+
view.contributingBehaviours.length > 0
|
|
94
|
+
? div(
|
|
95
|
+
{ className: "column" },
|
|
96
|
+
h2({ className: "section-title" }, "Contributing Behaviours"),
|
|
97
|
+
createLinksList(view.contributingBehaviours, "/behaviour"),
|
|
98
|
+
)
|
|
99
|
+
: null,
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
: null,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
render(page);
|
|
106
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grades pages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render, div, h1, h3, p, formatLevel } from "../lib/render.js";
|
|
6
|
+
import { getState } from "../lib/state.js";
|
|
7
|
+
import { createBadge } from "../components/card.js";
|
|
8
|
+
import { renderNotFound } from "../components/error-page.js";
|
|
9
|
+
import { prepareGradesList } from "../formatters/grade/shared.js";
|
|
10
|
+
import { gradeToDOM } from "../formatters/grade/dom.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render grades list page
|
|
14
|
+
*/
|
|
15
|
+
export function renderGradesList() {
|
|
16
|
+
const { data } = getState();
|
|
17
|
+
const { framework } = data;
|
|
18
|
+
|
|
19
|
+
// Transform data for list view
|
|
20
|
+
const { items } = prepareGradesList(data.grades);
|
|
21
|
+
|
|
22
|
+
const page = div(
|
|
23
|
+
{ className: "grades-page" },
|
|
24
|
+
// Header
|
|
25
|
+
div(
|
|
26
|
+
{ className: "page-header" },
|
|
27
|
+
h1({ className: "page-title" }, framework.entityDefinitions.grade.title),
|
|
28
|
+
p(
|
|
29
|
+
{ className: "page-description" },
|
|
30
|
+
framework.entityDefinitions.grade.description.trim(),
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
|
|
34
|
+
// Grades timeline
|
|
35
|
+
div(
|
|
36
|
+
{ className: "grades-timeline" },
|
|
37
|
+
...items.map((grade) => createGradeTimelineItem(grade)),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
render(page);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a grade timeline item
|
|
46
|
+
* @param {Object} grade
|
|
47
|
+
* @returns {HTMLElement}
|
|
48
|
+
*/
|
|
49
|
+
function createGradeTimelineItem(grade) {
|
|
50
|
+
const item = div(
|
|
51
|
+
{ className: "grade-timeline-item" },
|
|
52
|
+
div({ className: "grade-level-marker" }, String(grade.ordinalRank)),
|
|
53
|
+
div(
|
|
54
|
+
{ className: "grade-timeline-content card card-clickable" },
|
|
55
|
+
div(
|
|
56
|
+
{ className: "card-header" },
|
|
57
|
+
h3({ className: "card-title" }, grade.displayName),
|
|
58
|
+
createBadge(grade.id, "default"),
|
|
59
|
+
),
|
|
60
|
+
grade.typicalExperienceRange
|
|
61
|
+
? p(
|
|
62
|
+
{ className: "text-muted", style: "margin: 0.25rem 0" },
|
|
63
|
+
`${grade.typicalExperienceRange} experience`,
|
|
64
|
+
)
|
|
65
|
+
: null,
|
|
66
|
+
div(
|
|
67
|
+
{ className: "card-meta", style: "margin-top: 0.5rem" },
|
|
68
|
+
createBadge(
|
|
69
|
+
`Primary: ${formatLevel(grade.baseSkillLevels?.primary)}`,
|
|
70
|
+
"primary",
|
|
71
|
+
),
|
|
72
|
+
createBadge(
|
|
73
|
+
`Secondary: ${formatLevel(grade.baseSkillLevels?.secondary)}`,
|
|
74
|
+
"secondary",
|
|
75
|
+
),
|
|
76
|
+
createBadge(
|
|
77
|
+
`Broad: ${formatLevel(grade.baseSkillLevels?.broad)}`,
|
|
78
|
+
"broad",
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
grade.scope
|
|
82
|
+
? p(
|
|
83
|
+
{ className: "card-description", style: "margin-top: 0.75rem" },
|
|
84
|
+
`Scope: ${grade.scope}`,
|
|
85
|
+
)
|
|
86
|
+
: null,
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
item.querySelector(".card").addEventListener("click", () => {
|
|
91
|
+
window.location.hash = `/grade/${grade.id}`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return item;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Render grade detail page
|
|
99
|
+
* @param {Object} params - Route params
|
|
100
|
+
*/
|
|
101
|
+
export function renderGradeDetail(params) {
|
|
102
|
+
const { data } = getState();
|
|
103
|
+
const grade = data.grades.find((g) => g.id === params.id);
|
|
104
|
+
|
|
105
|
+
if (!grade) {
|
|
106
|
+
renderNotFound({
|
|
107
|
+
entityType: "Grade",
|
|
108
|
+
entityId: params.id,
|
|
109
|
+
backPath: "/grade",
|
|
110
|
+
backText: "← Back to Grades",
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Use DOM formatter - it handles transformation internally
|
|
116
|
+
render(gradeToDOM(grade, { framework: data.framework }));
|
|
117
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interview prep page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render } from "../lib/render.js";
|
|
6
|
+
import { getState } from "../lib/state.js";
|
|
7
|
+
import { createBuilder, createStandardPreview } from "../components/builder.js";
|
|
8
|
+
import { prepareInterviewBuilderPreview } from "../formatters/interview/shared.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render interview prep page
|
|
12
|
+
*/
|
|
13
|
+
export function renderInterviewPrep() {
|
|
14
|
+
const { data } = getState();
|
|
15
|
+
|
|
16
|
+
render(
|
|
17
|
+
createBuilder({
|
|
18
|
+
title: "Interview Prep",
|
|
19
|
+
description:
|
|
20
|
+
"Select a discipline, track, and grade to generate tailored interview questions " +
|
|
21
|
+
"based on the role's skill requirements and expected behaviours.",
|
|
22
|
+
formTitle: "Select Role",
|
|
23
|
+
emptyPreviewText: "Select all three components to preview the interview.",
|
|
24
|
+
buttonText: "View Interview Questions →",
|
|
25
|
+
previewPresenter: (selection) =>
|
|
26
|
+
prepareInterviewBuilderPreview({
|
|
27
|
+
...selection,
|
|
28
|
+
behaviourCount: data.behaviours.length,
|
|
29
|
+
grades: data.grades,
|
|
30
|
+
}),
|
|
31
|
+
detailPath: (sel) =>
|
|
32
|
+
`/interview/${sel.discipline}/${sel.track}/${sel.grade}`,
|
|
33
|
+
renderPreview: createStandardPreview,
|
|
34
|
+
helpItems: [
|
|
35
|
+
{
|
|
36
|
+
label: "Role Selection",
|
|
37
|
+
text: "Choose a discipline, track, and grade to define the target role for the interview.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "Skill Questions",
|
|
41
|
+
text: "Questions are generated based on the required skill levels for the role.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: "Behaviour Questions",
|
|
45
|
+
text: "Behavioural questions assess mindsets and ways of working at the expected maturity.",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interview detail page with interview questions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
render,
|
|
7
|
+
div,
|
|
8
|
+
h1,
|
|
9
|
+
h2,
|
|
10
|
+
h4,
|
|
11
|
+
p,
|
|
12
|
+
a,
|
|
13
|
+
button,
|
|
14
|
+
span,
|
|
15
|
+
ul,
|
|
16
|
+
li,
|
|
17
|
+
formatLevel,
|
|
18
|
+
} from "../lib/render.js";
|
|
19
|
+
import { getState } from "../lib/state.js";
|
|
20
|
+
import { createBadge } from "../components/card.js";
|
|
21
|
+
import { createBackLink } from "../components/nav.js";
|
|
22
|
+
import { createDetailSection } from "../components/detail.js";
|
|
23
|
+
import { renderError } from "../components/error-page.js";
|
|
24
|
+
import { getConceptEmoji } from "../model/levels.js";
|
|
25
|
+
import {
|
|
26
|
+
prepareAllInterviews,
|
|
27
|
+
INTERVIEW_TYPES,
|
|
28
|
+
} from "../formatters/interview/shared.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render interview detail page
|
|
32
|
+
* @param {Object} params - Route params
|
|
33
|
+
*/
|
|
34
|
+
export function renderInterviewDetail(params) {
|
|
35
|
+
const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
|
|
36
|
+
const { data } = getState();
|
|
37
|
+
|
|
38
|
+
// Find the components
|
|
39
|
+
const discipline = data.disciplines.find((d) => d.id === disciplineId);
|
|
40
|
+
const track = data.tracks.find((t) => t.id === trackId);
|
|
41
|
+
const grade = data.grades.find((g) => g.id === gradeId);
|
|
42
|
+
|
|
43
|
+
if (!discipline || !track || !grade) {
|
|
44
|
+
renderError({
|
|
45
|
+
title: "Interview Not Found",
|
|
46
|
+
message: "Invalid combination. One or more components are missing.",
|
|
47
|
+
backPath: "/interview-prep",
|
|
48
|
+
backText: "← Back to Interview Prep",
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Use formatter shared module to get all interview types
|
|
54
|
+
const interviewsView = prepareAllInterviews({
|
|
55
|
+
discipline,
|
|
56
|
+
grade,
|
|
57
|
+
track,
|
|
58
|
+
skills: data.skills,
|
|
59
|
+
behaviours: data.behaviours,
|
|
60
|
+
questions: data.questions,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!interviewsView) {
|
|
64
|
+
renderError({
|
|
65
|
+
title: "Invalid Combination",
|
|
66
|
+
message: "This discipline, track, and grade combination is not valid.",
|
|
67
|
+
backPath: "/interview-prep",
|
|
68
|
+
backText: "← Back to Interview Prep",
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// State for current interview type (default to first: Screening)
|
|
74
|
+
let currentType = "short";
|
|
75
|
+
|
|
76
|
+
const page = div(
|
|
77
|
+
{ className: "interview-detail-page" },
|
|
78
|
+
// Header
|
|
79
|
+
div(
|
|
80
|
+
{ className: "page-header" },
|
|
81
|
+
createBackLink("/interview-prep", "← Back to Interview Prep"),
|
|
82
|
+
h1({ className: "page-title" }, `Interview: ${interviewsView.title}`),
|
|
83
|
+
div(
|
|
84
|
+
{ className: "page-description" },
|
|
85
|
+
"Interview questions for: ",
|
|
86
|
+
a(
|
|
87
|
+
{ href: `#/discipline/${interviewsView.disciplineId}` },
|
|
88
|
+
interviewsView.disciplineName,
|
|
89
|
+
),
|
|
90
|
+
" × ",
|
|
91
|
+
a(
|
|
92
|
+
{ href: `#/grade/${interviewsView.gradeId}` },
|
|
93
|
+
interviewsView.gradeId,
|
|
94
|
+
),
|
|
95
|
+
" × ",
|
|
96
|
+
a(
|
|
97
|
+
{ href: `#/track/${interviewsView.trackId}` },
|
|
98
|
+
interviewsView.trackName,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
|
|
103
|
+
// Interview type toggle
|
|
104
|
+
div(
|
|
105
|
+
{ className: "interview-type-toggle", id: "interview-type-toggle" },
|
|
106
|
+
...Object.values(INTERVIEW_TYPES).map((type) =>
|
|
107
|
+
createTypeButton(type, type.id === currentType, () => {
|
|
108
|
+
currentType = type.id;
|
|
109
|
+
updateTypeToggle();
|
|
110
|
+
updateQuestions();
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
|
|
115
|
+
// Questions container
|
|
116
|
+
div({ id: "interview-questions-container" }),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
render(page);
|
|
120
|
+
|
|
121
|
+
// Generate and display initial questions
|
|
122
|
+
updateQuestions();
|
|
123
|
+
|
|
124
|
+
function updateTypeToggle() {
|
|
125
|
+
const toggleEl = document.getElementById("interview-type-toggle");
|
|
126
|
+
toggleEl.innerHTML = "";
|
|
127
|
+
Object.values(INTERVIEW_TYPES).forEach((type) => {
|
|
128
|
+
toggleEl.appendChild(
|
|
129
|
+
createTypeButton(type, type.id === currentType, () => {
|
|
130
|
+
currentType = type.id;
|
|
131
|
+
updateTypeToggle();
|
|
132
|
+
updateQuestions();
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function updateQuestions() {
|
|
139
|
+
const container = document.getElementById("interview-questions-container");
|
|
140
|
+
container.innerHTML = "";
|
|
141
|
+
|
|
142
|
+
// Get interview data from presenter
|
|
143
|
+
const interview = interviewsView.interviews[currentType];
|
|
144
|
+
|
|
145
|
+
container.appendChild(
|
|
146
|
+
div(
|
|
147
|
+
{},
|
|
148
|
+
// Interview summary
|
|
149
|
+
createInterviewSummary(interview),
|
|
150
|
+
|
|
151
|
+
// Questions sections
|
|
152
|
+
createQuestionsDisplay(interview, data.framework),
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create type button
|
|
160
|
+
*/
|
|
161
|
+
function createTypeButton(type, isActive, onClick) {
|
|
162
|
+
const btn = button(
|
|
163
|
+
{
|
|
164
|
+
className: `interview-type-btn ${isActive ? "active" : ""}`,
|
|
165
|
+
},
|
|
166
|
+
span({ className: "interview-type-icon" }, type.icon),
|
|
167
|
+
span({ className: "interview-type-name" }, type.name),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
btn.addEventListener("click", onClick);
|
|
171
|
+
return btn;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create interview summary
|
|
176
|
+
*/
|
|
177
|
+
function createInterviewSummary(interview) {
|
|
178
|
+
const typeInfo = interview.typeInfo;
|
|
179
|
+
|
|
180
|
+
return div(
|
|
181
|
+
{ className: "interview-summary card" },
|
|
182
|
+
div(
|
|
183
|
+
{ className: "interview-summary-header" },
|
|
184
|
+
h2({}, `${typeInfo.icon} ${typeInfo.name}`),
|
|
185
|
+
p({ className: "text-muted" }, typeInfo.description),
|
|
186
|
+
),
|
|
187
|
+
div(
|
|
188
|
+
{ className: "interview-summary-stats" },
|
|
189
|
+
createBadge(`${interview.questions.length} questions`, "default"),
|
|
190
|
+
createBadge(`~${interview.expectedDurationMinutes} minutes`, "secondary"),
|
|
191
|
+
interview.coverage.skills.length > 0
|
|
192
|
+
? createBadge(
|
|
193
|
+
`${interview.coverage.skills.length} skills covered`,
|
|
194
|
+
"primary",
|
|
195
|
+
)
|
|
196
|
+
: null,
|
|
197
|
+
interview.coverage.behaviours.length > 0
|
|
198
|
+
? createBadge(
|
|
199
|
+
`${interview.coverage.behaviours.length} behaviours covered`,
|
|
200
|
+
"primary",
|
|
201
|
+
)
|
|
202
|
+
: null,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create questions display
|
|
209
|
+
*/
|
|
210
|
+
function createQuestionsDisplay(interview, framework) {
|
|
211
|
+
// Group questions by type
|
|
212
|
+
const skillQuestions = interview.questions.filter(
|
|
213
|
+
(q) => q.targetType === "skill",
|
|
214
|
+
);
|
|
215
|
+
const behaviourQuestions = interview.questions.filter(
|
|
216
|
+
(q) => q.targetType === "behaviour",
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const sections = [];
|
|
220
|
+
|
|
221
|
+
if (skillQuestions.length > 0) {
|
|
222
|
+
sections.push(
|
|
223
|
+
createDetailSection({
|
|
224
|
+
title: `${getConceptEmoji(framework, "skill")} Skill Questions (${skillQuestions.length})`,
|
|
225
|
+
content: createQuestionsList(skillQuestions),
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (behaviourQuestions.length > 0) {
|
|
231
|
+
sections.push(
|
|
232
|
+
createDetailSection({
|
|
233
|
+
title: `${getConceptEmoji(framework, "behaviour")} Behaviour Questions (${behaviourQuestions.length})`,
|
|
234
|
+
content: createQuestionsList(behaviourQuestions),
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (sections.length === 0) {
|
|
240
|
+
return div(
|
|
241
|
+
{ className: "card" },
|
|
242
|
+
p(
|
|
243
|
+
{ className: "text-muted" },
|
|
244
|
+
"No questions available for this combination. Please check that question data exists for the required skills and behaviours.",
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return div({}, ...sections);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create questions list
|
|
254
|
+
*/
|
|
255
|
+
function createQuestionsList(questions) {
|
|
256
|
+
return div(
|
|
257
|
+
{ className: "questions-list" },
|
|
258
|
+
...questions.map((q, index) => createQuestionCard(q, index + 1)),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create question card
|
|
264
|
+
*/
|
|
265
|
+
function createQuestionCard(questionEntry, number) {
|
|
266
|
+
const { question, targetName, targetLevel } = questionEntry;
|
|
267
|
+
|
|
268
|
+
const followUpsList =
|
|
269
|
+
question.followUps && question.followUps.length > 0
|
|
270
|
+
? div(
|
|
271
|
+
{ className: "question-followups" },
|
|
272
|
+
h4({}, "Follow-up questions:"),
|
|
273
|
+
ul({}, ...question.followUps.map((fu) => li({}, fu))),
|
|
274
|
+
)
|
|
275
|
+
: null;
|
|
276
|
+
|
|
277
|
+
const lookingForList =
|
|
278
|
+
question.lookingFor && question.lookingFor.length > 0
|
|
279
|
+
? div(
|
|
280
|
+
{ className: "question-looking-for" },
|
|
281
|
+
h4({}, "What to look for:"),
|
|
282
|
+
ul({}, ...question.lookingFor.map((lf) => li({}, lf))),
|
|
283
|
+
)
|
|
284
|
+
: null;
|
|
285
|
+
|
|
286
|
+
return div(
|
|
287
|
+
{ className: "question-card" },
|
|
288
|
+
div(
|
|
289
|
+
{ className: "question-header" },
|
|
290
|
+
span({ className: "question-number" }, `Q${number}`),
|
|
291
|
+
div(
|
|
292
|
+
{ className: "question-meta" },
|
|
293
|
+
createBadge(targetName, "default"),
|
|
294
|
+
createBadge(formatLevel(targetLevel), "secondary"),
|
|
295
|
+
question.expectedDurationMinutes
|
|
296
|
+
? createBadge(`~${question.expectedDurationMinutes} min`, "secondary")
|
|
297
|
+
: null,
|
|
298
|
+
),
|
|
299
|
+
),
|
|
300
|
+
div({ className: "question-text" }, question.text),
|
|
301
|
+
followUpsList,
|
|
302
|
+
lookingForList,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job builder page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render } from "../lib/render.js";
|
|
6
|
+
import { getState } from "../lib/state.js";
|
|
7
|
+
import { createBuilder, createStandardPreview } from "../components/builder.js";
|
|
8
|
+
import { prepareJobBuilderPreview } from "../model/job.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render job builder page
|
|
12
|
+
*/
|
|
13
|
+
export function renderJobBuilder() {
|
|
14
|
+
const { data } = getState();
|
|
15
|
+
|
|
16
|
+
render(
|
|
17
|
+
createBuilder({
|
|
18
|
+
title: "Job Builder",
|
|
19
|
+
description:
|
|
20
|
+
"Combine a discipline, track, and grade to generate a complete job definition " +
|
|
21
|
+
"with skill matrix and behaviour profile.",
|
|
22
|
+
formTitle: "Select Components",
|
|
23
|
+
emptyPreviewText:
|
|
24
|
+
"Select all three components to preview the job definition.",
|
|
25
|
+
buttonText: "View Full Job Definition →",
|
|
26
|
+
previewPresenter: (selection) =>
|
|
27
|
+
prepareJobBuilderPreview({
|
|
28
|
+
...selection,
|
|
29
|
+
behaviourCount: data.behaviours.length,
|
|
30
|
+
grades: data.grades,
|
|
31
|
+
}),
|
|
32
|
+
detailPath: (sel) => `/job/${sel.discipline}/${sel.track}/${sel.grade}`,
|
|
33
|
+
renderPreview: createStandardPreview,
|
|
34
|
+
helpItems: [
|
|
35
|
+
{
|
|
36
|
+
label: "Discipline",
|
|
37
|
+
text: "Defines the T-shaped skill profile with primary, secondary, and broad skills.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "Grade",
|
|
41
|
+
text: "Sets base skill levels and behaviour maturity expectations for career level.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: "Track",
|
|
45
|
+
text: "Modifies skill and behaviour expectations based on the nature of work.",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
package/app/pages/job.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job detail page with visualizations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { render } from "../lib/render.js";
|
|
6
|
+
import { getState } from "../lib/state.js";
|
|
7
|
+
import { renderError } from "../components/error-page.js";
|
|
8
|
+
import { prepareJobDetail } from "../model/job.js";
|
|
9
|
+
import { jobToDOM } from "../formatters/job/dom.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Render job detail page
|
|
13
|
+
* @param {Object} params - Route params
|
|
14
|
+
*/
|
|
15
|
+
export function renderJobDetail(params) {
|
|
16
|
+
const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
|
|
17
|
+
const { data } = getState();
|
|
18
|
+
|
|
19
|
+
// Find the components
|
|
20
|
+
const discipline = data.disciplines.find((d) => d.id === disciplineId);
|
|
21
|
+
const track = data.tracks.find((t) => t.id === trackId);
|
|
22
|
+
const grade = data.grades.find((g) => g.id === gradeId);
|
|
23
|
+
|
|
24
|
+
if (!discipline || !track || !grade) {
|
|
25
|
+
renderError({
|
|
26
|
+
title: "Job Not Found",
|
|
27
|
+
message: "Invalid job combination. One or more components are missing.",
|
|
28
|
+
backPath: "/job-builder",
|
|
29
|
+
backText: "← Back to Job Builder",
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Use formatter shared module to get job detail view
|
|
35
|
+
const jobView = prepareJobDetail({
|
|
36
|
+
discipline,
|
|
37
|
+
grade,
|
|
38
|
+
track,
|
|
39
|
+
skills: data.skills,
|
|
40
|
+
behaviours: data.behaviours,
|
|
41
|
+
drivers: data.drivers,
|
|
42
|
+
capabilities: data.capabilities,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!jobView) {
|
|
46
|
+
renderError({
|
|
47
|
+
title: "Invalid Combination",
|
|
48
|
+
message: "This discipline, track, and grade combination is not valid.",
|
|
49
|
+
backPath: "/job-builder",
|
|
50
|
+
backText: "← Back to Job Builder",
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Format using DOM formatter
|
|
56
|
+
const page = jobToDOM(jobView, { discipline, grade, track });
|
|
57
|
+
render(page);
|
|
58
|
+
}
|