@forwardimpact/pathway 0.2.0 → 0.4.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 +20 -20
- package/app/commands/index.js +4 -3
- package/app/commands/job.js +9 -4
- package/app/commands/skill.js +56 -2
- package/app/commands/tool.js +112 -0
- package/app/components/builder.js +6 -3
- package/app/components/checklist.js +6 -4
- package/app/components/markdown-textarea.js +132 -0
- package/app/css/components/forms.css +45 -0
- package/app/css/components/layout.css +12 -0
- package/app/css/components/surfaces.css +22 -0
- package/app/css/pages/detail.css +50 -0
- package/app/css/pages/job-builder.css +0 -42
- package/app/formatters/agent/profile.js +61 -120
- package/app/formatters/agent/skill.js +48 -60
- package/app/formatters/grade/dom.js +2 -4
- package/app/formatters/job/description.js +74 -82
- package/app/formatters/job/dom.js +45 -179
- package/app/formatters/job/markdown.js +17 -13
- package/app/formatters/shared.js +65 -2
- package/app/formatters/skill/dom.js +57 -2
- package/app/formatters/skill/markdown.js +18 -0
- package/app/formatters/skill/shared.js +12 -4
- package/app/formatters/stage/microdata.js +1 -1
- package/app/formatters/stage/shared.js +1 -1
- package/app/formatters/tool/shared.js +72 -0
- package/app/handout-main.js +7 -7
- package/app/handout.html +7 -0
- package/app/index.html +10 -3
- package/app/lib/card-mappers.js +64 -17
- package/app/lib/form-controls.js +64 -1
- package/app/lib/render.js +12 -1
- package/app/lib/template-loader.js +9 -0
- package/app/lib/yaml-loader.js +12 -1
- package/app/main.js +4 -0
- package/app/model/agent.js +26 -18
- package/app/model/derivation.js +3 -3
- package/app/model/levels.js +2 -0
- package/app/model/loader.js +12 -1
- package/app/model/validation.js +74 -8
- package/app/pages/agent-builder.js +8 -5
- package/app/pages/job.js +28 -4
- package/app/pages/landing.js +34 -14
- package/app/pages/progress.js +6 -5
- package/app/pages/self-assessment.js +10 -8
- package/app/pages/skill.js +5 -17
- package/app/pages/stage.js +10 -6
- package/app/pages/tool.js +50 -0
- package/app/slides/index.js +25 -25
- package/app/slides.html +7 -0
- package/bin/pathway.js +41 -27
- package/examples/capabilities/business.yaml +17 -17
- package/examples/capabilities/delivery.yaml +51 -36
- package/examples/capabilities/reliability.yaml +127 -114
- package/examples/capabilities/scale.yaml +38 -36
- package/examples/disciplines/engineering_management.yaml +1 -1
- package/examples/framework.yaml +12 -0
- package/examples/grades.yaml +18 -19
- package/examples/self-assessments.yaml +1 -1
- package/package.json +1 -1
- package/templates/job.template.md +47 -0
- package/templates/skill.template.md +31 -12
|
@@ -89,46 +89,4 @@
|
|
|
89
89
|
font-weight: 600;
|
|
90
90
|
color: var(--color-primary);
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
/* Job description section */
|
|
94
|
-
.job-description-container {
|
|
95
|
-
display: flex;
|
|
96
|
-
flex-direction: column;
|
|
97
|
-
gap: var(--space-md);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.job-description-header {
|
|
101
|
-
display: flex;
|
|
102
|
-
justify-content: space-between;
|
|
103
|
-
align-items: center;
|
|
104
|
-
flex-wrap: wrap;
|
|
105
|
-
gap: var(--space-md);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.job-description-header .text-muted {
|
|
109
|
-
margin: 0;
|
|
110
|
-
flex: 1;
|
|
111
|
-
min-width: 200px;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.job-description-textarea {
|
|
115
|
-
width: 100%;
|
|
116
|
-
min-height: 400px;
|
|
117
|
-
padding: var(--space-md);
|
|
118
|
-
font-family:
|
|
119
|
-
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
120
|
-
font-size: var(--font-size-sm);
|
|
121
|
-
line-height: 1.6;
|
|
122
|
-
background-color: var(--color-bg);
|
|
123
|
-
border: 1px solid var(--color-border);
|
|
124
|
-
border-radius: var(--radius-md);
|
|
125
|
-
resize: vertical;
|
|
126
|
-
color: var(--color-text);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.job-description-textarea:focus {
|
|
130
|
-
outline: none;
|
|
131
|
-
border-color: var(--color-primary);
|
|
132
|
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
133
|
-
}
|
|
134
92
|
}
|
|
@@ -10,6 +10,66 @@
|
|
|
10
10
|
|
|
11
11
|
import Mustache from "mustache";
|
|
12
12
|
|
|
13
|
+
import { trimValue, trimRequired, trimFields } from "../shared.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prepare agent profile data for template rendering
|
|
17
|
+
* Normalizes string values by trimming trailing newlines for consistent template output.
|
|
18
|
+
* @param {Object} params
|
|
19
|
+
* @param {Object} params.frontmatter - YAML frontmatter data
|
|
20
|
+
* @param {string} params.frontmatter.name - Agent name
|
|
21
|
+
* @param {string} params.frontmatter.description - Agent description
|
|
22
|
+
* @param {boolean} params.frontmatter.infer - Whether to auto-select
|
|
23
|
+
* @param {Array} [params.frontmatter.handoffs] - Handoff definitions
|
|
24
|
+
* @param {Object} params.bodyData - Structured body data
|
|
25
|
+
* @param {string} params.bodyData.title - Agent title
|
|
26
|
+
* @param {string} params.bodyData.stageDescription - Stage description text
|
|
27
|
+
* @param {string} params.bodyData.identity - Core identity text
|
|
28
|
+
* @param {string} [params.bodyData.priority] - Priority/philosophy statement
|
|
29
|
+
* @param {string[]} params.bodyData.capabilities - List of capability names
|
|
30
|
+
* @param {Array<{index: number, text: string}>} params.bodyData.beforeMakingChanges - Numbered steps
|
|
31
|
+
* @param {string} [params.bodyData.delegation] - Delegation guidance
|
|
32
|
+
* @param {string} params.bodyData.operationalContext - Operational context text
|
|
33
|
+
* @param {string} params.bodyData.workingStyle - Working style markdown section
|
|
34
|
+
* @param {string} [params.bodyData.beforeHandoff] - Before handoff checklist markdown
|
|
35
|
+
* @param {string[]} params.bodyData.constraints - List of constraints
|
|
36
|
+
* @returns {Object} Data object ready for Mustache template
|
|
37
|
+
*/
|
|
38
|
+
function prepareAgentProfileData({ frontmatter, bodyData }) {
|
|
39
|
+
// Trim array fields using helpers
|
|
40
|
+
const handoffs = trimFields(frontmatter.handoffs, { prompt: "required" });
|
|
41
|
+
const beforeMakingChanges = trimFields(bodyData.beforeMakingChanges, {
|
|
42
|
+
text: "required",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Trim simple string arrays
|
|
46
|
+
const constraints = (bodyData.constraints || []).map((c) => trimRequired(c));
|
|
47
|
+
const capabilities = (bodyData.capabilities || []).map((c) =>
|
|
48
|
+
trimRequired(c),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
// Frontmatter
|
|
53
|
+
name: frontmatter.name,
|
|
54
|
+
description: trimRequired(frontmatter.description),
|
|
55
|
+
infer: frontmatter.infer,
|
|
56
|
+
handoffs,
|
|
57
|
+
|
|
58
|
+
// Body data - trim all string fields
|
|
59
|
+
title: bodyData.title,
|
|
60
|
+
stageDescription: trimValue(bodyData.stageDescription),
|
|
61
|
+
identity: trimValue(bodyData.identity),
|
|
62
|
+
priority: trimValue(bodyData.priority),
|
|
63
|
+
capabilities,
|
|
64
|
+
beforeMakingChanges,
|
|
65
|
+
delegation: trimValue(bodyData.delegation),
|
|
66
|
+
operationalContext: trimValue(bodyData.operationalContext),
|
|
67
|
+
workingStyle: trimValue(bodyData.workingStyle),
|
|
68
|
+
beforeHandoff: trimValue(bodyData.beforeHandoff),
|
|
69
|
+
constraints,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
13
73
|
/**
|
|
14
74
|
* Format agent profile as .agent.md file content using Mustache template
|
|
15
75
|
* @param {Object} profile - Profile with frontmatter and bodyData
|
|
@@ -35,125 +95,6 @@ import Mustache from "mustache";
|
|
|
35
95
|
* @returns {string} Complete .agent.md file content
|
|
36
96
|
*/
|
|
37
97
|
export function formatAgentProfile({ frontmatter, bodyData }, template) {
|
|
38
|
-
const data = {
|
|
39
|
-
// Frontmatter
|
|
40
|
-
name: frontmatter.name,
|
|
41
|
-
description: frontmatter.description,
|
|
42
|
-
infer: frontmatter.infer,
|
|
43
|
-
handoffs: frontmatter.handoffs || [],
|
|
44
|
-
// Body data
|
|
45
|
-
...bodyData,
|
|
46
|
-
};
|
|
98
|
+
const data = prepareAgentProfileData({ frontmatter, bodyData });
|
|
47
99
|
return Mustache.render(template, data);
|
|
48
100
|
}
|
|
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
|
-
}
|
|
@@ -10,6 +10,47 @@
|
|
|
10
10
|
|
|
11
11
|
import Mustache from "mustache";
|
|
12
12
|
|
|
13
|
+
import { trimValue, splitLines, trimFields } from "../shared.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prepare agent skill data for template rendering
|
|
17
|
+
* Normalizes string values by trimming trailing newlines for consistent template output.
|
|
18
|
+
* @param {Object} params
|
|
19
|
+
* @param {Object} params.frontmatter - YAML frontmatter data
|
|
20
|
+
* @param {string} params.frontmatter.name - Skill name (required)
|
|
21
|
+
* @param {string} params.frontmatter.description - Skill description (required)
|
|
22
|
+
* @param {string} [params.frontmatter.useWhen] - When to use this skill
|
|
23
|
+
* @param {string} params.title - Human-readable skill title for heading
|
|
24
|
+
* @param {Array} params.stages - Array of stage objects with stageName, focus, activities, ready
|
|
25
|
+
* @param {string} params.reference - Reference content (markdown)
|
|
26
|
+
* @param {Array} [params.toolReferences] - Array of tool reference objects
|
|
27
|
+
* @returns {Object} Data object ready for Mustache template
|
|
28
|
+
*/
|
|
29
|
+
function prepareAgentSkillData({
|
|
30
|
+
frontmatter,
|
|
31
|
+
title,
|
|
32
|
+
stages,
|
|
33
|
+
reference,
|
|
34
|
+
toolReferences,
|
|
35
|
+
}) {
|
|
36
|
+
// Process stages - trim focus and array values
|
|
37
|
+
const processedStages = trimFields(stages, {
|
|
38
|
+
focus: "required",
|
|
39
|
+
activities: "array",
|
|
40
|
+
ready: "array",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
name: frontmatter.name,
|
|
45
|
+
descriptionLines: splitLines(frontmatter.description),
|
|
46
|
+
useWhenLines: splitLines(frontmatter.useWhen),
|
|
47
|
+
title,
|
|
48
|
+
stages: processedStages,
|
|
49
|
+
reference: trimValue(reference) || "",
|
|
50
|
+
toolReferences: toolReferences || [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
13
54
|
/**
|
|
14
55
|
* Format agent skill as SKILL.md file content using Mustache template
|
|
15
56
|
* @param {Object} skill - Skill with frontmatter, title, stages, reference
|
|
@@ -19,73 +60,20 @@ import Mustache from "mustache";
|
|
|
19
60
|
* @param {string} skill.title - Human-readable skill title for heading
|
|
20
61
|
* @param {Array} skill.stages - Array of stage objects with stageName, focus, activities, ready
|
|
21
62
|
* @param {string} skill.reference - Reference content (markdown)
|
|
63
|
+
* @param {Array} [skill.toolReferences] - Array of tool reference objects
|
|
22
64
|
* @param {string} template - Mustache template string
|
|
23
65
|
* @returns {string} Complete SKILL.md file content
|
|
24
66
|
*/
|
|
25
67
|
export function formatAgentSkill(
|
|
26
|
-
{ frontmatter, title, stages, reference },
|
|
68
|
+
{ frontmatter, title, stages, reference, toolReferences },
|
|
27
69
|
template,
|
|
28
70
|
) {
|
|
29
|
-
const data = {
|
|
30
|
-
|
|
31
|
-
descriptionLines: frontmatter.description.trim().split("\n"),
|
|
71
|
+
const data = prepareAgentSkillData({
|
|
72
|
+
frontmatter,
|
|
32
73
|
title,
|
|
33
74
|
stages,
|
|
34
|
-
reference
|
|
35
|
-
|
|
75
|
+
reference,
|
|
76
|
+
toolReferences,
|
|
77
|
+
});
|
|
36
78
|
return Mustache.render(template, data);
|
|
37
79
|
}
|
|
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,29 @@
|
|
|
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";
|
|
17
|
+
import { trimValue, trimFields } from "../shared.js";
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
20
|
+
* Prepare job data for template rendering
|
|
15
21
|
* @param {Object} params
|
|
16
22
|
* @param {Object} params.job - The job definition
|
|
17
23
|
* @param {Object} params.discipline - The discipline
|
|
18
24
|
* @param {Object} params.grade - The grade
|
|
19
|
-
* @param {Object} params.track - The track
|
|
20
|
-
* @returns {
|
|
25
|
+
* @param {Object} [params.track] - The track (optional)
|
|
26
|
+
* @returns {Object} Data object ready for Mustache template
|
|
21
27
|
*/
|
|
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
|
-
|
|
28
|
+
function prepareJobDescriptionData({ job, discipline, grade, track }) {
|
|
41
29
|
// Build role summary from discipline - use manager version if applicable
|
|
42
30
|
const isManagement = discipline.isManagement === true;
|
|
43
31
|
let roleSummary =
|
|
@@ -48,16 +36,9 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
48
36
|
const { roleTitle, specialization } = discipline;
|
|
49
37
|
roleSummary = roleSummary.replace(/\{roleTitle\}/g, roleTitle);
|
|
50
38
|
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
39
|
|
|
60
|
-
//
|
|
40
|
+
// Build expectations paragraph
|
|
41
|
+
let expectationsParagraph = "";
|
|
61
42
|
if (job.expectations) {
|
|
62
43
|
const exp = job.expectations;
|
|
63
44
|
const expectationSentences = [];
|
|
@@ -89,45 +70,20 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
89
70
|
}
|
|
90
71
|
|
|
91
72
|
if (expectationSentences.length > 0) {
|
|
92
|
-
|
|
93
|
-
lines.push("");
|
|
73
|
+
expectationsParagraph = expectationSentences.join(" ");
|
|
94
74
|
}
|
|
95
75
|
}
|
|
96
76
|
|
|
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
77
|
// Sort behaviours by maturity level (highest first)
|
|
114
78
|
const sortedBehaviours = [...job.behaviourProfile].sort((a, b) => {
|
|
115
79
|
const indexA = BEHAVIOUR_MATURITY_ORDER.indexOf(a.maturity);
|
|
116
80
|
const indexB = BEHAVIOUR_MATURITY_ORDER.indexOf(b.maturity);
|
|
117
|
-
// Sort in reverse order (exemplifying first, emerging last)
|
|
118
81
|
if (indexA === -1 && indexB === -1) return 0;
|
|
119
82
|
if (indexA === -1) return 1;
|
|
120
83
|
if (indexB === -1) return -1;
|
|
121
84
|
return indexB - indexA;
|
|
122
85
|
});
|
|
123
86
|
|
|
124
|
-
for (const behaviour of sortedBehaviours) {
|
|
125
|
-
lines.push(
|
|
126
|
-
`- **${behaviour.behaviourName}:** ${behaviour.maturityDescription || ""}`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
lines.push("");
|
|
130
|
-
|
|
131
87
|
// Group skills by level
|
|
132
88
|
const skillsByLevel = {};
|
|
133
89
|
for (const skill of job.skillMatrix) {
|
|
@@ -138,41 +94,77 @@ export function formatJobDescription({ job, discipline, grade, track }) {
|
|
|
138
94
|
skillsByLevel[level].push(skill);
|
|
139
95
|
}
|
|
140
96
|
|
|
141
|
-
// Sort levels in
|
|
97
|
+
// Sort levels in reverse order (expert first, awareness last)
|
|
142
98
|
const sortedLevels = Object.keys(skillsByLevel).sort((a, b) => {
|
|
143
99
|
const indexA = SKILL_LEVEL_ORDER.indexOf(a.toLowerCase());
|
|
144
100
|
const indexB = SKILL_LEVEL_ORDER.indexOf(b.toLowerCase());
|
|
145
|
-
// Sort in reverse order (expert first, awareness last)
|
|
146
101
|
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
|
|
147
102
|
if (indexA === -1) return 1;
|
|
148
103
|
if (indexB === -1) return -1;
|
|
149
104
|
return indexB - indexA;
|
|
150
105
|
});
|
|
151
106
|
|
|
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
|
-
}
|
|
107
|
+
// Keep only the top 2 skill levels for job descriptions
|
|
108
|
+
const topLevels = sortedLevels.slice(0, 2);
|
|
167
109
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
110
|
+
// Build skill levels array for template
|
|
111
|
+
const skillLevels = topLevels.map((level) => {
|
|
112
|
+
const skills = skillsByLevel[level];
|
|
113
|
+
const sortedSkills = [...skills].sort((a, b) =>
|
|
114
|
+
(a.skillName || "").localeCompare(b.skillName || ""),
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
117
|
+
levelHeading: `${level.toUpperCase()}-LEVEL SKILLS`,
|
|
118
|
+
skills: sortedSkills.map((s) => ({
|
|
119
|
+
skillName: s.skillName,
|
|
120
|
+
levelDescription: s.levelDescription || "",
|
|
121
|
+
})),
|
|
122
|
+
};
|
|
123
|
+
});
|
|
171
124
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
125
|
+
// Build qualification summary with placeholder replacement
|
|
126
|
+
const qualificationSummary =
|
|
127
|
+
(grade.qualificationSummary || "").replace(
|
|
128
|
+
/\{typicalExperienceRange\}/g,
|
|
129
|
+
grade.typicalExperienceRange || "",
|
|
130
|
+
) || null;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
title: job.title,
|
|
134
|
+
gradeId: grade.id,
|
|
135
|
+
typicalExperienceRange: grade.typicalExperienceRange,
|
|
136
|
+
trackName: track?.name || null,
|
|
137
|
+
roleSummary: trimValue(roleSummary),
|
|
138
|
+
trackRoleContext: trimValue(track?.roleContext),
|
|
139
|
+
expectationsParagraph: trimValue(expectationsParagraph),
|
|
140
|
+
responsibilities: trimFields(job.derivedResponsibilities, {
|
|
141
|
+
responsibility: "required",
|
|
142
|
+
}),
|
|
143
|
+
behaviours: trimFields(sortedBehaviours, {
|
|
144
|
+
maturityDescription: "optional",
|
|
145
|
+
}),
|
|
146
|
+
skillLevels: skillLevels.map((level) => ({
|
|
147
|
+
...level,
|
|
148
|
+
skills: trimFields(level.skills, { levelDescription: "optional" }),
|
|
149
|
+
})),
|
|
150
|
+
qualificationSummary: trimValue(qualificationSummary),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
176
153
|
|
|
177
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Format job as a markdown job description using Mustache template
|
|
156
|
+
* @param {Object} params
|
|
157
|
+
* @param {Object} params.job - The job definition
|
|
158
|
+
* @param {Object} params.discipline - The discipline
|
|
159
|
+
* @param {Object} params.grade - The grade
|
|
160
|
+
* @param {Object} [params.track] - The track (optional)
|
|
161
|
+
* @param {string} template - Mustache template string
|
|
162
|
+
* @returns {string} Markdown formatted job description
|
|
163
|
+
*/
|
|
164
|
+
export function formatJobDescription(
|
|
165
|
+
{ job, discipline, grade, track },
|
|
166
|
+
template,
|
|
167
|
+
) {
|
|
168
|
+
const data = prepareJobDescriptionData({ job, discipline, grade, track });
|
|
169
|
+
return Mustache.render(template, data);
|
|
178
170
|
}
|