@forwardimpact/pathway 0.2.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.
- package/app/commands/agent.js +19 -19
- package/app/commands/job.js +9 -4
- package/app/components/builder.js +6 -3
- package/app/formatters/agent/profile.js +0 -111
- package/app/formatters/agent/skill.js +0 -54
- package/app/formatters/grade/dom.js +2 -4
- package/app/formatters/job/description.js +69 -82
- package/app/formatters/job/dom.js +40 -113
- package/app/formatters/job/markdown.js +17 -13
- package/app/formatters/shared.js +7 -2
- package/app/handout.html +7 -0
- package/app/lib/form-controls.js +64 -1
- package/app/lib/render.js +8 -1
- package/app/lib/template-loader.js +9 -0
- package/app/pages/agent-builder.js +6 -3
- package/app/pages/job.js +28 -4
- package/app/pages/progress.js +6 -5
- package/app/pages/self-assessment.js +3 -3
- package/app/slides.html +7 -0
- package/bin/pathway.js +10 -11
- package/examples/grades.yaml +13 -12
- package/package.json +1 -1
- package/templates/job.template.md +47 -0
package/app/commands/agent.js
CHANGED
|
@@ -7,16 +7,18 @@
|
|
|
7
7
|
* All agents are stage-specific. Use --stage for a single stage
|
|
8
8
|
* or --all-stages (default) for all stages.
|
|
9
9
|
*
|
|
10
|
+
* By default, outputs to console. Use --output to write files.
|
|
11
|
+
*
|
|
10
12
|
* Usage:
|
|
11
|
-
* npx pathway agent <discipline> [--track=<track>]
|
|
13
|
+
* npx pathway agent <discipline> [--track=<track>]
|
|
12
14
|
* npx pathway agent <discipline> --track=<track> --stage=plan
|
|
13
|
-
* npx pathway agent <discipline> --track=<track> --
|
|
15
|
+
* npx pathway agent <discipline> --track=<track> --output=./agents
|
|
14
16
|
* npx pathway agent --list
|
|
15
17
|
*
|
|
16
18
|
* Examples:
|
|
17
19
|
* npx pathway agent software_engineering --track=platform
|
|
18
20
|
* npx pathway agent software_engineering --track=platform --stage=plan
|
|
19
|
-
* npx pathway agent software_engineering --track=platform --
|
|
21
|
+
* npx pathway agent software_engineering --track=platform --output=./agents
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { writeFile, mkdir, readFile } from "fs/promises";
|
|
@@ -32,10 +34,7 @@ import {
|
|
|
32
34
|
deriveAgentSkills,
|
|
33
35
|
generateSkillMd,
|
|
34
36
|
} from "../model/agent.js";
|
|
35
|
-
import {
|
|
36
|
-
formatAgentProfile,
|
|
37
|
-
formatAgentProfileForCli,
|
|
38
|
-
} from "../formatters/agent/profile.js";
|
|
37
|
+
import { formatAgentProfile } from "../formatters/agent/profile.js";
|
|
39
38
|
import { formatAgentSkill } from "../formatters/agent/skill.js";
|
|
40
39
|
import { formatError, formatSuccess } from "../lib/cli-output.js";
|
|
41
40
|
import {
|
|
@@ -410,14 +409,15 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
|
|
|
410
409
|
process.exit(1);
|
|
411
410
|
}
|
|
412
411
|
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
// Load template
|
|
413
|
+
const agentTemplate = await loadAgentTemplate(dataDir);
|
|
414
|
+
|
|
415
|
+
// Output to console (default) or write to files (with --output)
|
|
416
|
+
if (!options.output) {
|
|
417
|
+
console.log(formatAgentProfile(profile, agentTemplate));
|
|
416
418
|
return;
|
|
417
419
|
}
|
|
418
420
|
|
|
419
|
-
// Load templates only when writing files
|
|
420
|
-
const agentTemplate = await loadAgentTemplate(dataDir);
|
|
421
421
|
await writeProfile(profile, baseDir, agentTemplate);
|
|
422
422
|
await generateVSCodeSettings(baseDir, agentData.vscodeSettings);
|
|
423
423
|
await generateDevcontainer(
|
|
@@ -484,19 +484,19 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
|
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
//
|
|
488
|
-
|
|
487
|
+
// Load templates
|
|
488
|
+
const agentTemplate = await loadAgentTemplate(dataDir);
|
|
489
|
+
const skillTemplate = await loadSkillTemplate(dataDir);
|
|
490
|
+
|
|
491
|
+
// Output to console (default) or write to files (with --output)
|
|
492
|
+
if (!options.output) {
|
|
489
493
|
for (const profile of profiles) {
|
|
490
|
-
console.log(
|
|
494
|
+
console.log(formatAgentProfile(profile, agentTemplate));
|
|
491
495
|
console.log("\n---\n");
|
|
492
496
|
}
|
|
493
497
|
return;
|
|
494
498
|
}
|
|
495
499
|
|
|
496
|
-
// Load templates only when writing files
|
|
497
|
-
const agentTemplate = await loadAgentTemplate(dataDir);
|
|
498
|
-
const skillTemplate = await loadSkillTemplate(dataDir);
|
|
499
|
-
|
|
500
500
|
for (const profile of profiles) {
|
|
501
501
|
await writeProfile(profile, baseDir, agentTemplate);
|
|
502
502
|
}
|
package/app/commands/job.js
CHANGED
|
@@ -20,15 +20,17 @@ import {
|
|
|
20
20
|
deriveChecklist,
|
|
21
21
|
formatChecklistMarkdown,
|
|
22
22
|
} from "../model/checklist.js";
|
|
23
|
+
import { loadJobTemplate } from "../lib/template-loader.js";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Format job output
|
|
26
27
|
* @param {Object} view - Presenter view
|
|
27
28
|
* @param {Object} _options - Command options
|
|
28
29
|
* @param {Object} entities - Original entities
|
|
30
|
+
* @param {string} jobTemplate - Mustache template for job description
|
|
29
31
|
*/
|
|
30
|
-
function formatJob(view, _options, entities) {
|
|
31
|
-
console.log(jobToMarkdown(view, entities));
|
|
32
|
+
function formatJob(view, _options, entities, jobTemplate) {
|
|
33
|
+
console.log(jobToMarkdown(view, entities, jobTemplate));
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -37,8 +39,9 @@ function formatJob(view, _options, entities) {
|
|
|
37
39
|
* @param {Object} params.data - All loaded data
|
|
38
40
|
* @param {string[]} params.args - Command arguments
|
|
39
41
|
* @param {Object} params.options - Command options
|
|
42
|
+
* @param {string} params.dataDir - Path to data directory
|
|
40
43
|
*/
|
|
41
|
-
export async function runJobCommand({ data, args, options }) {
|
|
44
|
+
export async function runJobCommand({ data, args, options, dataDir }) {
|
|
42
45
|
const jobs = generateAllJobs({
|
|
43
46
|
disciplines: data.disciplines,
|
|
44
47
|
grades: data.grades,
|
|
@@ -167,5 +170,7 @@ export async function runJobCommand({ data, args, options }) {
|
|
|
167
170
|
return;
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
|
|
173
|
+
// Load job template for description formatting
|
|
174
|
+
const jobTemplate = await loadJobTemplate(dataDir);
|
|
175
|
+
formatJob(view, options, { discipline, grade, track }, jobTemplate);
|
|
171
176
|
}
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
} from "../lib/render.js";
|
|
18
18
|
import { getState } from "../lib/state.js";
|
|
19
19
|
import { createBadge } from "./card.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createSelectWithValue,
|
|
22
|
+
createDisciplineSelect,
|
|
23
|
+
} from "../lib/form-controls.js";
|
|
21
24
|
import { createReactive } from "../lib/reactive.js";
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -213,9 +216,9 @@ export function createBuilder({
|
|
|
213
216
|
div(
|
|
214
217
|
{ className: "form-group" },
|
|
215
218
|
label({ className: "form-label" }, labels.discipline || "Discipline"),
|
|
216
|
-
|
|
219
|
+
createDisciplineSelect({
|
|
217
220
|
id: "discipline-select",
|
|
218
|
-
|
|
221
|
+
disciplines: data.disciplines,
|
|
219
222
|
initialValue: selection.get().discipline,
|
|
220
223
|
placeholder: "Select a discipline...",
|
|
221
224
|
onChange: (value) => {
|
|
@@ -46,114 +46,3 @@ export function formatAgentProfile({ frontmatter, bodyData }, template) {
|
|
|
46
46
|
};
|
|
47
47
|
return Mustache.render(template, data);
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Format agent profile for CLI output (markdown)
|
|
52
|
-
* @param {Object} profile - Profile with frontmatter and bodyData
|
|
53
|
-
* @returns {string} Markdown formatted for CLI display
|
|
54
|
-
*/
|
|
55
|
-
export function formatAgentProfileForCli({ frontmatter, bodyData }) {
|
|
56
|
-
const lines = [];
|
|
57
|
-
|
|
58
|
-
lines.push(`# Agent Profile: ${frontmatter.name}`);
|
|
59
|
-
lines.push("");
|
|
60
|
-
lines.push(`**Description:** ${frontmatter.description}`);
|
|
61
|
-
lines.push("");
|
|
62
|
-
lines.push(`**Infer:** ${frontmatter.infer}`);
|
|
63
|
-
|
|
64
|
-
if (frontmatter.handoffs && frontmatter.handoffs.length > 0) {
|
|
65
|
-
lines.push("");
|
|
66
|
-
lines.push("**Handoffs:**");
|
|
67
|
-
for (const handoff of frontmatter.handoffs) {
|
|
68
|
-
const target = handoff.agent ? ` → ${handoff.agent}` : " (self)";
|
|
69
|
-
lines.push(` - ${handoff.label}${target}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
lines.push("");
|
|
74
|
-
lines.push("---");
|
|
75
|
-
lines.push("");
|
|
76
|
-
|
|
77
|
-
// Render structured body data
|
|
78
|
-
lines.push(`# ${bodyData.title}`);
|
|
79
|
-
lines.push("");
|
|
80
|
-
lines.push(bodyData.stageDescription);
|
|
81
|
-
lines.push("");
|
|
82
|
-
|
|
83
|
-
lines.push("## Core Identity");
|
|
84
|
-
lines.push("");
|
|
85
|
-
lines.push(bodyData.identity);
|
|
86
|
-
lines.push("");
|
|
87
|
-
|
|
88
|
-
if (bodyData.priority) {
|
|
89
|
-
lines.push(bodyData.priority);
|
|
90
|
-
lines.push("");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (bodyData.capabilities && bodyData.capabilities.length > 0) {
|
|
94
|
-
lines.push("Your primary capabilities:");
|
|
95
|
-
for (const cap of bodyData.capabilities) {
|
|
96
|
-
lines.push(`- ${cap}`);
|
|
97
|
-
}
|
|
98
|
-
lines.push("");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (bodyData.beforeMakingChanges && bodyData.beforeMakingChanges.length > 0) {
|
|
102
|
-
lines.push("Before making changes:");
|
|
103
|
-
for (const step of bodyData.beforeMakingChanges) {
|
|
104
|
-
lines.push(`${step.index}. ${step.text}`);
|
|
105
|
-
}
|
|
106
|
-
lines.push("");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (bodyData.delegation) {
|
|
110
|
-
lines.push("## Delegation");
|
|
111
|
-
lines.push("");
|
|
112
|
-
lines.push(bodyData.delegation);
|
|
113
|
-
lines.push("");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
lines.push("## Operational Context");
|
|
117
|
-
lines.push("");
|
|
118
|
-
lines.push(bodyData.operationalContext);
|
|
119
|
-
lines.push("");
|
|
120
|
-
|
|
121
|
-
lines.push(bodyData.workingStyle);
|
|
122
|
-
|
|
123
|
-
if (bodyData.beforeHandoff) {
|
|
124
|
-
lines.push("## Before Handoff");
|
|
125
|
-
lines.push("");
|
|
126
|
-
lines.push(
|
|
127
|
-
"Before offering a handoff, verify and summarize completion of these items:",
|
|
128
|
-
);
|
|
129
|
-
lines.push("");
|
|
130
|
-
lines.push(bodyData.beforeHandoff);
|
|
131
|
-
lines.push("");
|
|
132
|
-
lines.push(
|
|
133
|
-
"When verified, summarize what was accomplished then offer the handoff.",
|
|
134
|
-
);
|
|
135
|
-
lines.push("If items are incomplete, explain what remains.");
|
|
136
|
-
lines.push("");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
lines.push("## Return Format");
|
|
140
|
-
lines.push("");
|
|
141
|
-
lines.push("When completing work (for handoff or as a subagent), provide:");
|
|
142
|
-
lines.push("");
|
|
143
|
-
lines.push("1. **Work completed**: What was accomplished");
|
|
144
|
-
lines.push(
|
|
145
|
-
"2. **Checklist status**: Items verified from Before Handoff section",
|
|
146
|
-
);
|
|
147
|
-
lines.push("3. **Recommendation**: Ready for next stage, or needs more work");
|
|
148
|
-
lines.push("");
|
|
149
|
-
|
|
150
|
-
if (bodyData.constraints && bodyData.constraints.length > 0) {
|
|
151
|
-
lines.push("## Constraints");
|
|
152
|
-
lines.push("");
|
|
153
|
-
for (const constraint of bodyData.constraints) {
|
|
154
|
-
lines.push(`- ${constraint}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return lines.join("\n");
|
|
159
|
-
}
|
|
@@ -35,57 +35,3 @@ export function formatAgentSkill(
|
|
|
35
35
|
};
|
|
36
36
|
return Mustache.render(template, data);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Format agent skill for CLI output (markdown)
|
|
41
|
-
* @param {Object} skill - Skill with frontmatter, title, stages, reference
|
|
42
|
-
* @returns {string} Markdown formatted for CLI display
|
|
43
|
-
*/
|
|
44
|
-
export function formatAgentSkillForCli({
|
|
45
|
-
frontmatter,
|
|
46
|
-
title,
|
|
47
|
-
stages,
|
|
48
|
-
reference,
|
|
49
|
-
}) {
|
|
50
|
-
const lines = [];
|
|
51
|
-
|
|
52
|
-
lines.push(`# ${title}`);
|
|
53
|
-
lines.push("");
|
|
54
|
-
lines.push(`**Name:** ${frontmatter.name}`);
|
|
55
|
-
lines.push("");
|
|
56
|
-
lines.push(`**Description:** ${frontmatter.description.trim()}`);
|
|
57
|
-
lines.push("");
|
|
58
|
-
|
|
59
|
-
if (stages && stages.length > 0) {
|
|
60
|
-
lines.push("## Stage Guidance");
|
|
61
|
-
lines.push("");
|
|
62
|
-
for (const stage of stages) {
|
|
63
|
-
lines.push(`### ${stage.stageName} Stage`);
|
|
64
|
-
lines.push("");
|
|
65
|
-
lines.push(`**Focus:** ${stage.focus.trim()}`);
|
|
66
|
-
lines.push("");
|
|
67
|
-
if (stage.activities && stage.activities.length > 0) {
|
|
68
|
-
lines.push("**Activities:**");
|
|
69
|
-
for (const item of stage.activities) {
|
|
70
|
-
lines.push(`- ${item}`);
|
|
71
|
-
}
|
|
72
|
-
lines.push("");
|
|
73
|
-
}
|
|
74
|
-
if (stage.ready && stage.ready.length > 0) {
|
|
75
|
-
lines.push(`**Ready for ${stage.nextStageName} when:**`);
|
|
76
|
-
for (const item of stage.ready) {
|
|
77
|
-
lines.push(`- [ ] ${item}`);
|
|
78
|
-
}
|
|
79
|
-
lines.push("");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (reference) {
|
|
85
|
-
lines.push("## Reference");
|
|
86
|
-
lines.push("");
|
|
87
|
-
lines.push(reference.trim());
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return lines.join("\n");
|
|
91
|
-
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
tr,
|
|
15
15
|
th,
|
|
16
16
|
td,
|
|
17
|
+
formatLevel,
|
|
17
18
|
} from "../../lib/render.js";
|
|
18
19
|
import { createBackLink } from "../../components/nav.js";
|
|
19
20
|
import { createLevelDots } from "../../components/detail.js";
|
|
@@ -102,10 +103,7 @@ export function gradeToDOM(grade, { framework, showBackLink = true } = {}) {
|
|
|
102
103
|
...Object.entries(view.expectations).map(([key, value]) =>
|
|
103
104
|
div(
|
|
104
105
|
{ className: "list-item" },
|
|
105
|
-
p(
|
|
106
|
-
{ className: "label" },
|
|
107
|
-
key.charAt(0).toUpperCase() + key.slice(1),
|
|
108
|
-
),
|
|
106
|
+
p({ className: "label" }, formatLevel(key)),
|
|
109
107
|
p({}, value),
|
|
110
108
|
),
|
|
111
109
|
),
|
|
@@ -3,41 +3,28 @@
|
|
|
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
|
-
*
|
|
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 {
|
|
24
|
+
* @param {Object} [params.track] - The track (optional)
|
|
25
|
+
* @returns {Object} Data object ready for Mustache template
|
|
21
26
|
*/
|
|
22
|
-
|
|
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
|
-
if (track) {
|
|
33
|
-
lines.push(`- **Track:** ${track.name}`);
|
|
34
|
-
}
|
|
35
|
-
lines.push("");
|
|
36
|
-
|
|
37
|
-
// Role Summary
|
|
38
|
-
lines.push("## ROLE SUMMARY");
|
|
39
|
-
lines.push("");
|
|
40
|
-
|
|
27
|
+
function prepareJobDescriptionData({ job, discipline, grade, track }) {
|
|
41
28
|
// Build role summary from discipline - use manager version if applicable
|
|
42
29
|
const isManagement = discipline.isManagement === true;
|
|
43
30
|
let roleSummary =
|
|
@@ -48,16 +35,9 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
48
35
|
const { roleTitle, specialization } = discipline;
|
|
49
36
|
roleSummary = roleSummary.replace(/\{roleTitle\}/g, roleTitle);
|
|
50
37
|
roleSummary = roleSummary.replace(/\{specialization\}/g, specialization);
|
|
51
|
-
lines.push(roleSummary);
|
|
52
|
-
lines.push("");
|
|
53
|
-
|
|
54
|
-
// Add track context
|
|
55
|
-
if (track?.roleContext) {
|
|
56
|
-
lines.push(track.roleContext);
|
|
57
|
-
lines.push("");
|
|
58
|
-
}
|
|
59
38
|
|
|
60
|
-
//
|
|
39
|
+
// Build expectations paragraph
|
|
40
|
+
let expectationsParagraph = "";
|
|
61
41
|
if (job.expectations) {
|
|
62
42
|
const exp = job.expectations;
|
|
63
43
|
const expectationSentences = [];
|
|
@@ -89,45 +69,20 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
89
69
|
}
|
|
90
70
|
|
|
91
71
|
if (expectationSentences.length > 0) {
|
|
92
|
-
|
|
93
|
-
lines.push("");
|
|
72
|
+
expectationsParagraph = expectationSentences.join(" ");
|
|
94
73
|
}
|
|
95
74
|
}
|
|
96
75
|
|
|
97
|
-
// Key Responsibilities
|
|
98
|
-
lines.push("## ROLE RESPONSIBILITIES");
|
|
99
|
-
lines.push("");
|
|
100
|
-
|
|
101
|
-
// Use derived responsibilities (already sorted by level descending)
|
|
102
|
-
const derivedResponsibilities = job.derivedResponsibilities || [];
|
|
103
|
-
|
|
104
|
-
for (const r of derivedResponsibilities) {
|
|
105
|
-
lines.push(`- **${r.capabilityName}:** ${r.responsibility}`);
|
|
106
|
-
}
|
|
107
|
-
lines.push("");
|
|
108
|
-
|
|
109
|
-
// Key Behaviours
|
|
110
|
-
lines.push("## ROLE BEHAVIOURS");
|
|
111
|
-
lines.push("");
|
|
112
|
-
|
|
113
76
|
// Sort behaviours by maturity level (highest first)
|
|
114
77
|
const sortedBehaviours = [...job.behaviourProfile].sort((a, b) => {
|
|
115
78
|
const indexA = BEHAVIOUR_MATURITY_ORDER.indexOf(a.maturity);
|
|
116
79
|
const indexB = BEHAVIOUR_MATURITY_ORDER.indexOf(b.maturity);
|
|
117
|
-
// Sort in reverse order (exemplifying first, emerging last)
|
|
118
80
|
if (indexA === -1 && indexB === -1) return 0;
|
|
119
81
|
if (indexA === -1) return 1;
|
|
120
82
|
if (indexB === -1) return -1;
|
|
121
83
|
return indexB - indexA;
|
|
122
84
|
});
|
|
123
85
|
|
|
124
|
-
for (const behaviour of sortedBehaviours) {
|
|
125
|
-
lines.push(
|
|
126
|
-
`- **${behaviour.behaviourName}:** ${behaviour.maturityDescription || ""}`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
lines.push("");
|
|
130
|
-
|
|
131
86
|
// Group skills by level
|
|
132
87
|
const skillsByLevel = {};
|
|
133
88
|
for (const skill of job.skillMatrix) {
|
|
@@ -138,41 +93,73 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
138
93
|
skillsByLevel[level].push(skill);
|
|
139
94
|
}
|
|
140
95
|
|
|
141
|
-
// Sort levels in
|
|
96
|
+
// Sort levels in reverse order (expert first, awareness last)
|
|
142
97
|
const sortedLevels = Object.keys(skillsByLevel).sort((a, b) => {
|
|
143
98
|
const indexA = SKILL_LEVEL_ORDER.indexOf(a.toLowerCase());
|
|
144
99
|
const indexB = SKILL_LEVEL_ORDER.indexOf(b.toLowerCase());
|
|
145
|
-
// Sort in reverse order (expert first, awareness last)
|
|
146
100
|
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
|
|
147
101
|
if (indexA === -1) return 1;
|
|
148
102
|
if (indexB === -1) return -1;
|
|
149
103
|
return indexB - indexA;
|
|
150
104
|
});
|
|
151
105
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (skills.length > 0) {
|
|
155
|
-
lines.push(`## ${level.toUpperCase()}-LEVEL SKILLS`);
|
|
156
|
-
lines.push("");
|
|
157
|
-
// Sort skills alphabetically by name
|
|
158
|
-
const sortedSkills = [...skills].sort((a, b) =>
|
|
159
|
-
(a.skillName || "").localeCompare(b.skillName || ""),
|
|
160
|
-
);
|
|
161
|
-
for (const skill of sortedSkills) {
|
|
162
|
-
lines.push(`- **${skill.skillName}:** ${skill.levelDescription || ""}`);
|
|
163
|
-
}
|
|
164
|
-
lines.push("");
|
|
165
|
-
}
|
|
166
|
-
}
|
|
106
|
+
// Keep only the top 2 skill levels for job descriptions
|
|
107
|
+
const topLevels = sortedLevels.slice(0, 2);
|
|
167
108
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
});
|
|
171
123
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
176
148
|
|
|
177
|
-
|
|
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);
|
|
178
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 &&
|
|
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({
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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({
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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 &&
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
track: entities.track,
|
|
96
|
-
}),
|
|
99
|
+
jobTemplate,
|
|
100
|
+
),
|
|
97
101
|
);
|
|
98
102
|
lines.push("```");
|
|
99
103
|
}
|
package/app/formatters/shared.js
CHANGED
|
@@ -36,7 +36,7 @@ export function tableToMarkdown(headers, rows) {
|
|
|
36
36
|
export function objectToMarkdownList(obj, indent = 0) {
|
|
37
37
|
const prefix = " ".repeat(indent);
|
|
38
38
|
return Object.entries(obj)
|
|
39
|
-
.map(([key, value]) => `${prefix}- **${key}**: ${value}`)
|
|
39
|
+
.map(([key, value]) => `${prefix}- **${capitalize(key)}**: ${value}`)
|
|
40
40
|
.join("\n");
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -51,12 +51,17 @@ export function formatPercent(value) {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Capitalize first letter of each word
|
|
54
|
+
* Handles both snake_case and camelCase
|
|
54
55
|
* @param {string} str
|
|
55
56
|
* @returns {string}
|
|
56
57
|
*/
|
|
57
58
|
export function capitalize(str) {
|
|
58
59
|
if (!str) return "";
|
|
59
|
-
|
|
60
|
+
// Insert space before uppercase letters (for camelCase), then handle snake_case
|
|
61
|
+
return str
|
|
62
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
63
|
+
.replace(/_/g, " ")
|
|
64
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
/**
|
package/app/handout.html
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Engineering Pathway - Handout View</title>
|
|
7
7
|
<link rel="stylesheet" href="css/bundles/handout.css" />
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"mustache": "https://esm.sh/mustache@4.2.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
8
15
|
</head>
|
|
9
16
|
<body class="slide-view handout-view">
|
|
10
17
|
<header
|
package/app/lib/form-controls.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Reusable form control components
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { select, option } from "./render.js";
|
|
5
|
+
import { select, option, optgroup } from "./render.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Create a select element with initial value and change handler
|
|
@@ -45,3 +45,66 @@ export function createSelectWithValue({
|
|
|
45
45
|
|
|
46
46
|
return selectEl;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a discipline select with optgroups for Professional and Management
|
|
51
|
+
* @param {Object} options - Configuration options
|
|
52
|
+
* @param {string} options.id - Element ID
|
|
53
|
+
* @param {Array} options.disciplines - Array of discipline objects
|
|
54
|
+
* @param {string} options.initialValue - Initial selected value
|
|
55
|
+
* @param {string} options.placeholder - Placeholder text for empty option
|
|
56
|
+
* @param {Function} options.onChange - Callback when selection changes
|
|
57
|
+
* @param {Function} [options.getDisplayName] - Optional function to get display name from item
|
|
58
|
+
* @returns {HTMLElement}
|
|
59
|
+
*/
|
|
60
|
+
export function createDisciplineSelect({
|
|
61
|
+
id,
|
|
62
|
+
disciplines,
|
|
63
|
+
initialValue,
|
|
64
|
+
placeholder,
|
|
65
|
+
onChange,
|
|
66
|
+
getDisplayName,
|
|
67
|
+
}) {
|
|
68
|
+
const displayFn = getDisplayName || ((d) => d.specialization || d.name);
|
|
69
|
+
|
|
70
|
+
// Separate disciplines by type
|
|
71
|
+
const professional = disciplines.filter((d) => d.isProfessional);
|
|
72
|
+
const management = disciplines.filter((d) => d.isManagement);
|
|
73
|
+
|
|
74
|
+
// Sort each group alphabetically by display name
|
|
75
|
+
professional.sort((a, b) => displayFn(a).localeCompare(displayFn(b)));
|
|
76
|
+
management.sort((a, b) => displayFn(a).localeCompare(displayFn(b)));
|
|
77
|
+
|
|
78
|
+
// Build options for a group
|
|
79
|
+
const buildOptions = (items) =>
|
|
80
|
+
items.map((item) => {
|
|
81
|
+
const opt = option({ value: item.id }, displayFn(item));
|
|
82
|
+
if (item.id === initialValue) {
|
|
83
|
+
opt.selected = true;
|
|
84
|
+
}
|
|
85
|
+
return opt;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Build optgroups - Professional first, then Management
|
|
89
|
+
const groups = [];
|
|
90
|
+
if (professional.length > 0) {
|
|
91
|
+
groups.push(
|
|
92
|
+
optgroup({ label: "Professional" }, ...buildOptions(professional)),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (management.length > 0) {
|
|
96
|
+
groups.push(optgroup({ label: "Management" }, ...buildOptions(management)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const selectEl = select(
|
|
100
|
+
{ className: "form-select", id },
|
|
101
|
+
option({ value: "" }, placeholder),
|
|
102
|
+
...groups,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
selectEl.addEventListener("change", (e) => {
|
|
106
|
+
onChange(e.target.value);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return selectEl;
|
|
110
|
+
}
|
package/app/lib/render.js
CHANGED
|
@@ -118,6 +118,8 @@ export const select = (attrs, ...children) =>
|
|
|
118
118
|
createElement("select", attrs, ...children);
|
|
119
119
|
export const option = (attrs, ...children) =>
|
|
120
120
|
createElement("option", attrs, ...children);
|
|
121
|
+
export const optgroup = (attrs, ...children) =>
|
|
122
|
+
createElement("optgroup", attrs, ...children);
|
|
121
123
|
export const label = (attrs, ...children) =>
|
|
122
124
|
createElement("label", attrs, ...children);
|
|
123
125
|
export const form = (attrs, ...children) =>
|
|
@@ -185,12 +187,17 @@ export function showError(message) {
|
|
|
185
187
|
|
|
186
188
|
/**
|
|
187
189
|
* Format a skill level or behaviour maturity for display
|
|
190
|
+
* Handles both snake_case and camelCase
|
|
188
191
|
* @param {string} value - The level/maturity value
|
|
189
192
|
* @returns {string}
|
|
190
193
|
*/
|
|
191
194
|
export function formatLevel(value) {
|
|
192
195
|
if (!value) return "";
|
|
193
|
-
|
|
196
|
+
// Insert space before uppercase letters (for camelCase), then handle snake_case
|
|
197
|
+
return value
|
|
198
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
199
|
+
.replace(/_/g, " ")
|
|
200
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
/**
|
|
@@ -64,3 +64,12 @@ export async function loadAgentTemplate(dataDir) {
|
|
|
64
64
|
export async function loadSkillTemplate(dataDir) {
|
|
65
65
|
return loadTemplate("skill.template.md", dataDir);
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Load job description template
|
|
70
|
+
* @param {string} dataDir - Path to data directory
|
|
71
|
+
* @returns {Promise<string>} Job template content
|
|
72
|
+
*/
|
|
73
|
+
export async function loadJobTemplate(dataDir) {
|
|
74
|
+
return loadTemplate("job.template.md", dataDir);
|
|
75
|
+
}
|
|
@@ -26,7 +26,10 @@ import {
|
|
|
26
26
|
deriveAgentSkills,
|
|
27
27
|
deriveReferenceGrade,
|
|
28
28
|
} from "../model/agent.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createSelectWithValue,
|
|
31
|
+
createDisciplineSelect,
|
|
32
|
+
} from "../lib/form-controls.js";
|
|
30
33
|
import { createReactive } from "../lib/reactive.js";
|
|
31
34
|
import { getStageEmoji } from "../formatters/stage/shared.js";
|
|
32
35
|
import { formatAgentProfile } from "../formatters/agent/profile.js";
|
|
@@ -242,9 +245,9 @@ export async function renderAgentBuilder() {
|
|
|
242
245
|
{ className: "form-group" },
|
|
243
246
|
label({ className: "form-label" }, "Discipline"),
|
|
244
247
|
availableDisciplines.length > 0
|
|
245
|
-
?
|
|
248
|
+
? createDisciplineSelect({
|
|
246
249
|
id: "agent-discipline-select",
|
|
247
|
-
|
|
250
|
+
disciplines: availableDisciplines,
|
|
248
251
|
initialValue: selection.get().discipline,
|
|
249
252
|
placeholder: "Select a discipline...",
|
|
250
253
|
onChange: (value) => {
|
package/app/pages/job.js
CHANGED
|
@@ -2,17 +2,32 @@
|
|
|
2
2
|
* Job detail page with visualizations
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { render } from "../lib/render.js";
|
|
5
|
+
import { render, div, p } from "../lib/render.js";
|
|
6
6
|
import { getState } from "../lib/state.js";
|
|
7
7
|
import { renderError } from "../components/error-page.js";
|
|
8
8
|
import { prepareJobDetail } from "../model/job.js";
|
|
9
9
|
import { jobToDOM } from "../formatters/job/dom.js";
|
|
10
10
|
|
|
11
|
+
/** @type {string|null} Cached job template */
|
|
12
|
+
let jobTemplateCache = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Load job template with caching
|
|
16
|
+
* @returns {Promise<string>}
|
|
17
|
+
*/
|
|
18
|
+
async function getJobTemplate() {
|
|
19
|
+
if (!jobTemplateCache) {
|
|
20
|
+
const response = await fetch("./templates/job.template.md");
|
|
21
|
+
jobTemplateCache = await response.text();
|
|
22
|
+
}
|
|
23
|
+
return jobTemplateCache;
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
/**
|
|
12
27
|
* Render job detail page
|
|
13
28
|
* @param {Object} params - Route params
|
|
14
29
|
*/
|
|
15
|
-
export function renderJobDetail(params) {
|
|
30
|
+
export async function renderJobDetail(params) {
|
|
16
31
|
const { discipline: disciplineId, grade: gradeId, track: trackId } = params;
|
|
17
32
|
const { data } = getState();
|
|
18
33
|
|
|
@@ -63,7 +78,16 @@ export function renderJobDetail(params) {
|
|
|
63
78
|
return;
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
//
|
|
67
|
-
|
|
81
|
+
// Show loading while fetching template
|
|
82
|
+
render(
|
|
83
|
+
div(
|
|
84
|
+
{ className: "job-detail-page" },
|
|
85
|
+
div({ className: "loading" }, p({}, "Loading...")),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Load template and format
|
|
90
|
+
const jobTemplate = await getJobTemplate();
|
|
91
|
+
const page = jobToDOM(jobView, { discipline, grade, track, jobTemplate });
|
|
68
92
|
render(page);
|
|
69
93
|
}
|
package/app/pages/progress.js
CHANGED
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
} from "../components/comparison-radar.js";
|
|
14
14
|
import { createProgressionTable } from "../components/progression-table.js";
|
|
15
15
|
import { renderError } from "../components/error-page.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
createSelectWithValue,
|
|
18
|
+
createDisciplineSelect,
|
|
19
|
+
} from "../lib/form-controls.js";
|
|
17
20
|
import {
|
|
18
21
|
prepareCurrentJob,
|
|
19
22
|
prepareCustomProgression,
|
|
@@ -513,11 +516,9 @@ function createComparisonSelectorsSection({
|
|
|
513
516
|
div(
|
|
514
517
|
{ className: "form-group" },
|
|
515
518
|
label({ for: "compare-discipline-select" }, "Target Discipline"),
|
|
516
|
-
|
|
519
|
+
createDisciplineSelect({
|
|
517
520
|
id: "compare-discipline-select",
|
|
518
|
-
|
|
519
|
-
a.specialization.localeCompare(b.specialization),
|
|
520
|
-
),
|
|
521
|
+
disciplines: data.disciplines,
|
|
521
522
|
initialValue: selectedDisciplineId,
|
|
522
523
|
placeholder: "Select discipline...",
|
|
523
524
|
getDisplayName: (d) => d.specialization,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "../lib/render.js";
|
|
18
18
|
import { getState } from "../lib/state.js";
|
|
19
19
|
import { createBadge } from "../components/card.js";
|
|
20
|
-
import {
|
|
20
|
+
import { createDisciplineSelect } from "../lib/form-controls.js";
|
|
21
21
|
import {
|
|
22
22
|
SKILL_LEVEL_ORDER,
|
|
23
23
|
BEHAVIOUR_MATURITY_ORDER,
|
|
@@ -306,9 +306,9 @@ function renderIntroStep(data) {
|
|
|
306
306
|
"Select a discipline to highlight which skills are most relevant for that role. " +
|
|
307
307
|
"You can still assess all skills.",
|
|
308
308
|
),
|
|
309
|
-
|
|
309
|
+
createDisciplineSelect({
|
|
310
310
|
id: "discipline-filter-select",
|
|
311
|
-
|
|
311
|
+
disciplines: data.disciplines,
|
|
312
312
|
initialValue: assessmentState.discipline || "",
|
|
313
313
|
placeholder: "Select discipline",
|
|
314
314
|
onChange: (value) => {
|
package/app/slides.html
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Engineering Pathway - Slide View</title>
|
|
7
7
|
<link rel="stylesheet" href="css/bundles/slides.css" />
|
|
8
|
+
<script type="importmap">
|
|
9
|
+
{
|
|
10
|
+
"imports": {
|
|
11
|
+
"mustache": "https://esm.sh/mustache@4.2.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
8
15
|
</head>
|
|
9
16
|
<body class="slide-view">
|
|
10
17
|
<header
|
package/bin/pathway.js
CHANGED
|
@@ -101,7 +101,7 @@ Composite Commands:
|
|
|
101
101
|
progress <discipline> <track> <grade> [--compare=GRADE]
|
|
102
102
|
Show career progression
|
|
103
103
|
questions [filters] Browse interview questions
|
|
104
|
-
agent
|
|
104
|
+
agent <discipline> [--track=<track>] Generate AI coding agent
|
|
105
105
|
|
|
106
106
|
Global Options:
|
|
107
107
|
--list Output IDs only (for piping to other commands)
|
|
@@ -121,10 +121,10 @@ Questions Filters:
|
|
|
121
121
|
--format=FORMAT Output format: table, yaml, json
|
|
122
122
|
|
|
123
123
|
Agent Options:
|
|
124
|
-
--
|
|
125
|
-
--
|
|
126
|
-
--
|
|
127
|
-
--all-
|
|
124
|
+
--track=TRACK Track for the agent (e.g., platform, forward_deployed)
|
|
125
|
+
--output=PATH Write files to directory (without this, outputs to console)
|
|
126
|
+
--stage=STAGE Generate specific stage agent (plan, code, review)
|
|
127
|
+
--all-stages Generate all stage agents (default)
|
|
128
128
|
|
|
129
129
|
Examples:
|
|
130
130
|
npx pathway skill # Summary of all skills
|
|
@@ -139,7 +139,7 @@ Examples:
|
|
|
139
139
|
npx pathway questions --level=practitioner
|
|
140
140
|
npx pathway questions --stats
|
|
141
141
|
|
|
142
|
-
npx pathway agent software_engineering platform --output=./agents
|
|
142
|
+
npx pathway agent software_engineering --track=platform --output=./agents
|
|
143
143
|
npx pathway --validate # Validate all data
|
|
144
144
|
`;
|
|
145
145
|
|
|
@@ -160,6 +160,8 @@ function parseArgs(args) {
|
|
|
160
160
|
type: "full",
|
|
161
161
|
compare: null,
|
|
162
162
|
data: null,
|
|
163
|
+
// Shared command options
|
|
164
|
+
track: null,
|
|
163
165
|
// Questions command options
|
|
164
166
|
level: null,
|
|
165
167
|
maturity: null,
|
|
@@ -172,9 +174,6 @@ function parseArgs(args) {
|
|
|
172
174
|
checklist: null,
|
|
173
175
|
// Agent command options
|
|
174
176
|
output: null,
|
|
175
|
-
preview: false,
|
|
176
|
-
role: null,
|
|
177
|
-
"all-roles": false,
|
|
178
177
|
stage: null,
|
|
179
178
|
"all-stages": false,
|
|
180
179
|
// Serve command options
|
|
@@ -196,14 +195,14 @@ function parseArgs(args) {
|
|
|
196
195
|
result.validate = true;
|
|
197
196
|
} else if (arg === "--generate-index") {
|
|
198
197
|
result.generateIndex = true;
|
|
199
|
-
} else if (arg === "--preview") {
|
|
200
|
-
result.preview = true;
|
|
201
198
|
} else if (arg.startsWith("--type=")) {
|
|
202
199
|
result.type = arg.slice(7);
|
|
203
200
|
} else if (arg.startsWith("--compare=")) {
|
|
204
201
|
result.compare = arg.slice(10);
|
|
205
202
|
} else if (arg.startsWith("--data=")) {
|
|
206
203
|
result.data = arg.slice(7);
|
|
204
|
+
} else if (arg.startsWith("--track=")) {
|
|
205
|
+
result.track = arg.slice(8);
|
|
207
206
|
} else if (arg.startsWith("--output=")) {
|
|
208
207
|
result.output = arg.slice(9);
|
|
209
208
|
} else if (arg.startsWith("--level=")) {
|
package/examples/grades.yaml
CHANGED
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
managementTitle: Manager
|
|
29
29
|
typicalExperienceRange: "2-5"
|
|
30
30
|
ordinalRank: 2
|
|
31
|
-
qualificationSummary:
|
|
32
|
-
|
|
33
|
-
independently and contribute effectively to team projects.
|
|
31
|
+
qualificationSummary: >
|
|
32
|
+
{typicalExperienceRange} years of relevant experience. Demonstrated ability
|
|
33
|
+
to complete tasks independently and contribute effectively to team projects.
|
|
34
34
|
baseSkillLevels:
|
|
35
35
|
primary: foundational
|
|
36
36
|
secondary: foundational
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
managementTitle: Senior Manager
|
|
51
51
|
typicalExperienceRange: "5-8"
|
|
52
52
|
ordinalRank: 3
|
|
53
|
-
qualificationSummary:
|
|
54
|
-
|
|
55
|
-
initiatives and mentoring team members.
|
|
53
|
+
qualificationSummary: >
|
|
54
|
+
{typicalExperienceRange} years of relevant experience. Proven track record
|
|
55
|
+
of leading technical initiatives and mentoring team members.
|
|
56
56
|
baseSkillLevels:
|
|
57
57
|
primary: practitioner
|
|
58
58
|
secondary: working
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
managementTitle: Director
|
|
72
72
|
typicalExperienceRange: "8-12"
|
|
73
73
|
ordinalRank: 4
|
|
74
|
-
qualificationSummary:
|
|
75
|
-
|
|
76
|
-
technical projects with multi-team impact.
|
|
74
|
+
qualificationSummary: >
|
|
75
|
+
{typicalExperienceRange} years of relevant experience. Extensive experience
|
|
76
|
+
leading complex technical projects with multi-team impact.
|
|
77
77
|
baseSkillLevels:
|
|
78
78
|
primary: expert
|
|
79
79
|
secondary: practitioner
|
|
@@ -94,9 +94,10 @@
|
|
|
94
94
|
managementTitle: VP of Engineering
|
|
95
95
|
typicalExperienceRange: "12+"
|
|
96
96
|
ordinalRank: 5
|
|
97
|
-
qualificationSummary:
|
|
98
|
-
|
|
99
|
-
track record of organization-wide technical
|
|
97
|
+
qualificationSummary: >
|
|
98
|
+
{typicalExperienceRange} years of relevant experience. Deep technical
|
|
99
|
+
expertise with a proven track record of organization-wide technical
|
|
100
|
+
leadership.
|
|
100
101
|
baseSkillLevels:
|
|
101
102
|
primary: expert
|
|
102
103
|
secondary: expert
|
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# {{title}}
|
|
2
|
+
|
|
3
|
+
- **Level:** {{gradeId}}
|
|
4
|
+
- **Experience:** {{typicalExperienceRange}}
|
|
5
|
+
{{#trackName}}- **Track:** {{trackName}}
|
|
6
|
+
{{/trackName}}
|
|
7
|
+
|
|
8
|
+
## ROLE SUMMARY
|
|
9
|
+
|
|
10
|
+
{{{roleSummary}}}
|
|
11
|
+
|
|
12
|
+
{{#trackRoleContext}}
|
|
13
|
+
{{{trackRoleContext}}}
|
|
14
|
+
|
|
15
|
+
{{/trackRoleContext}}
|
|
16
|
+
{{#expectationsParagraph}}
|
|
17
|
+
{{{expectationsParagraph}}}
|
|
18
|
+
|
|
19
|
+
{{/expectationsParagraph}}
|
|
20
|
+
|
|
21
|
+
## ROLE RESPONSIBILITIES
|
|
22
|
+
|
|
23
|
+
{{#responsibilities}}
|
|
24
|
+
- **{{capabilityName}}:** {{responsibility}}
|
|
25
|
+
{{/responsibilities}}
|
|
26
|
+
|
|
27
|
+
## ROLE BEHAVIOURS
|
|
28
|
+
|
|
29
|
+
{{#behaviours}}
|
|
30
|
+
- **{{behaviourName}}:** {{maturityDescription}}
|
|
31
|
+
{{/behaviours}}
|
|
32
|
+
|
|
33
|
+
{{#skillLevels}}
|
|
34
|
+
|
|
35
|
+
## {{levelHeading}}
|
|
36
|
+
|
|
37
|
+
{{#skills}}
|
|
38
|
+
- **{{skillName}}:** {{levelDescription}}
|
|
39
|
+
{{/skills}}
|
|
40
|
+
|
|
41
|
+
{{/skillLevels}}
|
|
42
|
+
|
|
43
|
+
## QUALIFICATIONS
|
|
44
|
+
|
|
45
|
+
{{#qualificationSummary}}
|
|
46
|
+
{{{qualificationSummary}}}
|
|
47
|
+
{{/qualificationSummary}}
|