@forwardimpact/pathway 0.3.0 → 0.5.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 +1 -1
- package/app/commands/behaviour.js +1 -1
- package/app/commands/command-factory.js +2 -2
- package/app/commands/discipline.js +1 -1
- package/app/commands/driver.js +1 -1
- package/app/commands/grade.js +1 -1
- package/app/commands/index.js +4 -3
- package/app/commands/serve.js +2 -2
- package/app/commands/site.js +22 -2
- package/app/commands/skill.js +57 -3
- package/app/commands/stage.js +1 -1
- package/app/commands/tool.js +112 -0
- package/app/commands/track.js +1 -1
- package/app/components/card.js +11 -1
- package/app/components/checklist.js +6 -4
- package/app/components/code-display.js +153 -0
- package/app/components/markdown-textarea.js +153 -0
- package/app/css/bundles/app.css +14 -0
- package/app/css/components/badges.css +15 -8
- package/app/css/components/forms.css +55 -0
- package/app/css/components/layout.css +12 -0
- package/app/css/components/surfaces.css +71 -3
- package/app/css/components/typography.css +1 -2
- package/app/css/pages/agent-builder.css +11 -102
- package/app/css/pages/detail.css +60 -0
- package/app/css/pages/job-builder.css +0 -42
- package/app/css/tokens.css +3 -0
- package/app/formatters/agent/dom.js +26 -71
- package/app/formatters/agent/profile.js +67 -10
- package/app/formatters/agent/skill.js +48 -6
- package/app/formatters/grade/dom.js +6 -6
- package/app/formatters/job/description.js +21 -16
- package/app/formatters/job/dom.js +9 -70
- package/app/formatters/json-ld.js +1 -1
- package/app/formatters/shared.js +58 -0
- package/app/formatters/skill/dom.js +70 -3
- package/app/formatters/skill/markdown.js +18 -0
- package/app/formatters/skill/shared.js +14 -4
- package/app/formatters/stage/microdata.js +2 -2
- package/app/formatters/stage/shared.js +3 -3
- package/app/formatters/tool/shared.js +78 -0
- package/app/handout-main.js +19 -18
- package/app/index.html +16 -3
- package/app/lib/card-mappers.js +91 -17
- package/app/lib/render.js +4 -0
- package/app/lib/yaml-loader.js +12 -1
- package/app/main.js +4 -0
- package/app/model/agent.js +47 -23
- package/app/model/checklist.js +2 -2
- package/app/model/derivation.js +5 -5
- package/app/model/levels.js +4 -2
- package/app/model/loader.js +12 -1
- package/app/model/validation.js +77 -11
- package/app/pages/agent-builder.js +121 -77
- package/app/pages/landing.js +35 -15
- package/app/pages/self-assessment.js +7 -5
- package/app/pages/skill.js +5 -17
- package/app/pages/stage.js +12 -8
- package/app/pages/tool.js +50 -0
- package/app/slide-main.js +1 -1
- package/app/slides/chapter.js +8 -8
- package/app/slides/index.js +26 -26
- package/app/slides/overview.js +8 -8
- package/app/slides/skill.js +1 -0
- package/bin/pathway.js +31 -16
- package/examples/capabilities/business.yaml +18 -18
- package/examples/capabilities/delivery.yaml +54 -37
- package/examples/capabilities/people.yaml +1 -1
- package/examples/capabilities/reliability.yaml +130 -115
- package/examples/capabilities/scale.yaml +39 -37
- package/examples/disciplines/engineering_management.yaml +1 -1
- package/examples/framework.yaml +21 -9
- package/examples/grades.yaml +5 -7
- package/examples/self-assessments.yaml +1 -1
- package/examples/stages.yaml +18 -10
- package/package.json +2 -1
- package/templates/agent.template.md +47 -17
- package/templates/job.template.md +8 -8
- package/templates/skill.template.md +33 -11
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +0 -130
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +0 -131
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +0 -108
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +0 -142
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +0 -134
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +0 -163
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +0 -164
- package/examples/agents/.github/agents/se-platform-code.agent.md +0 -132
- package/examples/agents/.github/agents/se-platform-plan.agent.md +0 -131
- package/examples/agents/.github/agents/se-platform-review.agent.md +0 -136
- package/examples/agents/.vscode/settings.json +0 -8
|
@@ -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
|
}
|
package/app/css/tokens.css
CHANGED
|
@@ -130,6 +130,9 @@
|
|
|
130
130
|
--font-family:
|
|
131
131
|
system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
132
132
|
sans-serif;
|
|
133
|
+
--font-family-mono:
|
|
134
|
+
"JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo,
|
|
135
|
+
Consolas, monospace;
|
|
133
136
|
--font-size-xs: 0.75rem;
|
|
134
137
|
--font-size-sm: 0.875rem;
|
|
135
138
|
--font-size-base: 1rem;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
details,
|
|
17
17
|
summary,
|
|
18
18
|
} from "../../lib/render.js";
|
|
19
|
+
import { createCodeDisplay } from "../../components/code-display.js";
|
|
19
20
|
import { formatAgentProfile } from "./profile.js";
|
|
20
21
|
import { formatAgentSkill } from "./skill.js";
|
|
21
22
|
import { getStageEmoji } from "../stage/shared.js";
|
|
@@ -53,13 +54,12 @@ export function agentDeploymentToDOM({
|
|
|
53
54
|
// Profile section
|
|
54
55
|
section(
|
|
55
56
|
{ className: "agent-section" },
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
createCodeBlock(profileContent),
|
|
57
|
+
h2({}, "Agent Profile"),
|
|
58
|
+
createCodeDisplay({
|
|
59
|
+
content: profileContent,
|
|
60
|
+
filename: profile.filename,
|
|
61
|
+
maxHeight: 600,
|
|
62
|
+
}),
|
|
63
63
|
),
|
|
64
64
|
|
|
65
65
|
// Role Agents section
|
|
@@ -145,48 +145,6 @@ function createDownloadButton(
|
|
|
145
145
|
return btn;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/**
|
|
149
|
-
* Create a copy button for content
|
|
150
|
-
* @param {string} content - Content to copy
|
|
151
|
-
* @returns {HTMLElement}
|
|
152
|
-
*/
|
|
153
|
-
function createCopyButton(content) {
|
|
154
|
-
const btn = button({ className: "btn btn-sm copy-btn" }, "📋 Copy");
|
|
155
|
-
|
|
156
|
-
btn.addEventListener("click", async () => {
|
|
157
|
-
try {
|
|
158
|
-
await navigator.clipboard.writeText(content);
|
|
159
|
-
btn.textContent = "✓ Copied";
|
|
160
|
-
setTimeout(() => {
|
|
161
|
-
btn.textContent = "📋 Copy";
|
|
162
|
-
}, 2000);
|
|
163
|
-
} catch {
|
|
164
|
-
btn.textContent = "Failed";
|
|
165
|
-
setTimeout(() => {
|
|
166
|
-
btn.textContent = "📋 Copy";
|
|
167
|
-
}, 2000);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
return btn;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Create a code block with content
|
|
176
|
-
* @param {string} content - Code content
|
|
177
|
-
* @returns {HTMLElement}
|
|
178
|
-
*/
|
|
179
|
-
function createCodeBlock(content) {
|
|
180
|
-
const pre = document.createElement("pre");
|
|
181
|
-
pre.className = "code-block";
|
|
182
|
-
|
|
183
|
-
const code = document.createElement("code");
|
|
184
|
-
code.textContent = content;
|
|
185
|
-
|
|
186
|
-
pre.appendChild(code);
|
|
187
|
-
return pre;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
148
|
/**
|
|
191
149
|
* Create a skill card with content and copy button
|
|
192
150
|
* @param {Object} skill - Skill with frontmatter and body
|
|
@@ -198,12 +156,11 @@ function createSkillCard(skill) {
|
|
|
198
156
|
|
|
199
157
|
return div(
|
|
200
158
|
{ className: "skill-card" },
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
),
|
|
206
|
-
createCodeBlock(content),
|
|
159
|
+
createCodeDisplay({
|
|
160
|
+
content,
|
|
161
|
+
filename,
|
|
162
|
+
maxHeight: 300,
|
|
163
|
+
}),
|
|
207
164
|
);
|
|
208
165
|
}
|
|
209
166
|
|
|
@@ -235,8 +192,10 @@ function createRoleAgentCard(agent) {
|
|
|
235
192
|
{ className: "text-muted role-description" },
|
|
236
193
|
agent.frontmatter.description,
|
|
237
194
|
),
|
|
238
|
-
|
|
239
|
-
|
|
195
|
+
createCodeDisplay({
|
|
196
|
+
content,
|
|
197
|
+
maxHeight: 400,
|
|
198
|
+
}),
|
|
240
199
|
),
|
|
241
200
|
);
|
|
242
201
|
}
|
|
@@ -254,13 +213,10 @@ function createCliCommand(agentName) {
|
|
|
254
213
|
|
|
255
214
|
const command = `npx pathway agent ${discipline} ${track} --output=.github --all-roles`;
|
|
256
215
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
return container;
|
|
216
|
+
return createCodeDisplay({
|
|
217
|
+
content: command,
|
|
218
|
+
language: "bash",
|
|
219
|
+
});
|
|
264
220
|
}
|
|
265
221
|
|
|
266
222
|
/**
|
|
@@ -445,13 +401,12 @@ export function stageAgentToDOM(stageAgent, profile, options = {}) {
|
|
|
445
401
|
// Profile section
|
|
446
402
|
section(
|
|
447
403
|
{ className: "agent-section" },
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
createCodeBlock(profileContent),
|
|
404
|
+
h3({}, "Agent Profile"),
|
|
405
|
+
createCodeDisplay({
|
|
406
|
+
content: profileContent,
|
|
407
|
+
filename: profile.filename,
|
|
408
|
+
maxHeight: 600,
|
|
409
|
+
}),
|
|
455
410
|
),
|
|
456
411
|
|
|
457
412
|
// Download button
|
|
@@ -10,6 +10,71 @@
|
|
|
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 {Array<{name: string, dirname: string, useWhen: string}>} params.bodyData.skillIndex - Skill index entries
|
|
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
|
+
|
|
48
|
+
// Trim skill index entries
|
|
49
|
+
const skillIndex = (bodyData.skillIndex || []).map((s) => ({
|
|
50
|
+
name: trimRequired(s.name),
|
|
51
|
+
dirname: trimRequired(s.dirname),
|
|
52
|
+
useWhen: trimRequired(s.useWhen),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
// Frontmatter
|
|
57
|
+
name: frontmatter.name,
|
|
58
|
+
description: trimRequired(frontmatter.description),
|
|
59
|
+
infer: frontmatter.infer,
|
|
60
|
+
handoffs,
|
|
61
|
+
|
|
62
|
+
// Body data - trim all string fields
|
|
63
|
+
title: bodyData.title,
|
|
64
|
+
stageDescription: trimValue(bodyData.stageDescription),
|
|
65
|
+
identity: trimValue(bodyData.identity),
|
|
66
|
+
priority: trimValue(bodyData.priority),
|
|
67
|
+
skillIndex,
|
|
68
|
+
hasSkills: skillIndex.length > 0,
|
|
69
|
+
beforeMakingChanges,
|
|
70
|
+
delegation: trimValue(bodyData.delegation),
|
|
71
|
+
operationalContext: trimValue(bodyData.operationalContext),
|
|
72
|
+
workingStyle: trimValue(bodyData.workingStyle),
|
|
73
|
+
beforeHandoff: trimValue(bodyData.beforeHandoff),
|
|
74
|
+
constraints,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
13
78
|
/**
|
|
14
79
|
* Format agent profile as .agent.md file content using Mustache template
|
|
15
80
|
* @param {Object} profile - Profile with frontmatter and bodyData
|
|
@@ -24,7 +89,7 @@ import Mustache from "mustache";
|
|
|
24
89
|
* @param {string} profile.bodyData.stageDescription - Stage description text
|
|
25
90
|
* @param {string} profile.bodyData.identity - Core identity text
|
|
26
91
|
* @param {string} [profile.bodyData.priority] - Priority/philosophy statement (optional)
|
|
27
|
-
* @param {string
|
|
92
|
+
* @param {Array<{name: string, dirname: string, useWhen: string}>} profile.bodyData.skillIndex - Skill index entries
|
|
28
93
|
* @param {Array<{index: number, text: string}>} profile.bodyData.beforeMakingChanges - Numbered steps
|
|
29
94
|
* @param {string} [profile.bodyData.delegation] - Delegation guidance (optional)
|
|
30
95
|
* @param {string} profile.bodyData.operationalContext - Operational context text
|
|
@@ -35,14 +100,6 @@ import Mustache from "mustache";
|
|
|
35
100
|
* @returns {string} Complete .agent.md file content
|
|
36
101
|
*/
|
|
37
102
|
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
|
-
};
|
|
103
|
+
const data = prepareAgentProfileData({ frontmatter, bodyData });
|
|
47
104
|
return Mustache.render(template, data);
|
|
48
105
|
}
|
|
@@ -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,19 +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
|
}
|
|
@@ -77,16 +77,16 @@ export function gradeToDOM(grade, { framework, showBackLink = true } = {}) {
|
|
|
77
77
|
{ className: "content-columns" },
|
|
78
78
|
view.professionalTitle
|
|
79
79
|
? div(
|
|
80
|
-
{ className: "
|
|
80
|
+
{ className: "card" },
|
|
81
81
|
p({ className: "label" }, "Professional Track"),
|
|
82
|
-
p({}, view.professionalTitle),
|
|
82
|
+
p({ className: "card-description" }, view.professionalTitle),
|
|
83
83
|
)
|
|
84
84
|
: null,
|
|
85
85
|
view.managementTitle
|
|
86
86
|
? div(
|
|
87
|
-
{ className: "
|
|
87
|
+
{ className: "card" },
|
|
88
88
|
p({ className: "label" }, "Management Track"),
|
|
89
|
-
p({}, view.managementTitle),
|
|
89
|
+
p({ className: "card-description" }, view.managementTitle),
|
|
90
90
|
)
|
|
91
91
|
: null,
|
|
92
92
|
),
|
|
@@ -102,9 +102,9 @@ export function gradeToDOM(grade, { framework, showBackLink = true } = {}) {
|
|
|
102
102
|
{ className: "content-columns" },
|
|
103
103
|
...Object.entries(view.expectations).map(([key, value]) =>
|
|
104
104
|
div(
|
|
105
|
-
{ className: "
|
|
105
|
+
{ className: "card" },
|
|
106
106
|
p({ className: "label" }, formatLevel(key)),
|
|
107
|
-
p({}, value),
|
|
107
|
+
p({ className: "card-description" }, value),
|
|
108
108
|
),
|
|
109
109
|
),
|
|
110
110
|
),
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
SKILL_LEVEL_ORDER,
|
|
15
15
|
BEHAVIOUR_MATURITY_ORDER,
|
|
16
16
|
} from "../../model/levels.js";
|
|
17
|
+
import { trimValue, trimFields } from "../shared.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Prepare job data for template rendering
|
|
@@ -121,28 +122,32 @@ function prepareJobDescriptionData({ job, discipline, grade, track }) {
|
|
|
121
122
|
};
|
|
122
123
|
});
|
|
123
124
|
|
|
125
|
+
// Build qualification summary with placeholder replacement
|
|
126
|
+
const qualificationSummary =
|
|
127
|
+
(grade.qualificationSummary || "").replace(
|
|
128
|
+
/\{typicalExperienceRange\}/g,
|
|
129
|
+
grade.typicalExperienceRange || "",
|
|
130
|
+
) || null;
|
|
131
|
+
|
|
124
132
|
return {
|
|
125
133
|
title: job.title,
|
|
126
134
|
gradeId: grade.id,
|
|
127
135
|
typicalExperienceRange: grade.typicalExperienceRange,
|
|
128
136
|
trackName: track?.name || null,
|
|
129
|
-
roleSummary,
|
|
130
|
-
trackRoleContext: track?.roleContext
|
|
131
|
-
expectationsParagraph: expectationsParagraph
|
|
132
|
-
responsibilities: (job.derivedResponsibilities
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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" }),
|
|
139
149
|
})),
|
|
140
|
-
|
|
141
|
-
qualificationSummary:
|
|
142
|
-
(grade.qualificationSummary || "").replace(
|
|
143
|
-
/\{typicalExperienceRange\}/g,
|
|
144
|
-
grade.typicalExperienceRange || "",
|
|
145
|
-
) || null,
|
|
150
|
+
qualificationSummary: trimValue(qualificationSummary),
|
|
146
151
|
};
|
|
147
152
|
}
|
|
148
153
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Job formatting for DOM/web output
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { div, h1, h2, p, a, span,
|
|
5
|
+
import { div, h1, h2, p, a, span, section } from "../../lib/render.js";
|
|
6
6
|
import { createBackLink } from "../../components/nav.js";
|
|
7
7
|
import {
|
|
8
8
|
createDetailSection,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../../components/radar-chart.js";
|
|
15
15
|
import { createSkillMatrix } from "../../components/skill-matrix.js";
|
|
16
16
|
import { createBehaviourProfile } from "../../components/behaviour-profile.js";
|
|
17
|
+
import { createCodeDisplay } from "../../components/code-display.js";
|
|
17
18
|
import { markdownToHtml } from "../../lib/markdown.js";
|
|
18
19
|
import { formatJobDescription } from "./description.js";
|
|
19
20
|
|
|
@@ -223,77 +224,15 @@ export function createJobDescriptionSection({
|
|
|
223
224
|
template,
|
|
224
225
|
);
|
|
225
226
|
|
|
226
|
-
const copyButton = button(
|
|
227
|
-
{
|
|
228
|
-
className: "btn btn-primary copy-btn",
|
|
229
|
-
onClick: async () => {
|
|
230
|
-
try {
|
|
231
|
-
await navigator.clipboard.writeText(markdown);
|
|
232
|
-
copyButton.textContent = "✓ Copied!";
|
|
233
|
-
copyButton.classList.add("copied");
|
|
234
|
-
setTimeout(() => {
|
|
235
|
-
copyButton.textContent = "Copy Markdown";
|
|
236
|
-
copyButton.classList.remove("copied");
|
|
237
|
-
}, 2000);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
console.error("Failed to copy:", err);
|
|
240
|
-
copyButton.textContent = "Copy failed";
|
|
241
|
-
setTimeout(() => {
|
|
242
|
-
copyButton.textContent = "Copy Markdown";
|
|
243
|
-
}, 2000);
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
"Copy Markdown",
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
const copyHtmlButton = button(
|
|
251
|
-
{
|
|
252
|
-
className: "btn btn-secondary copy-btn",
|
|
253
|
-
onClick: async () => {
|
|
254
|
-
try {
|
|
255
|
-
const html = markdownToHtml(markdown);
|
|
256
|
-
// Use ClipboardItem with text/html MIME type for rich text pasting in Word
|
|
257
|
-
const blob = new Blob([html], { type: "text/html" });
|
|
258
|
-
const clipboardItem = new ClipboardItem({ "text/html": blob });
|
|
259
|
-
await navigator.clipboard.write([clipboardItem]);
|
|
260
|
-
copyHtmlButton.textContent = "✓ Copied!";
|
|
261
|
-
copyHtmlButton.classList.add("copied");
|
|
262
|
-
setTimeout(() => {
|
|
263
|
-
copyHtmlButton.textContent = "Copy as HTML";
|
|
264
|
-
copyHtmlButton.classList.remove("copied");
|
|
265
|
-
}, 2000);
|
|
266
|
-
} catch (err) {
|
|
267
|
-
console.error("Failed to copy:", err);
|
|
268
|
-
copyHtmlButton.textContent = "Copy failed";
|
|
269
|
-
setTimeout(() => {
|
|
270
|
-
copyHtmlButton.textContent = "Copy as HTML";
|
|
271
|
-
}, 2000);
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
"Copy as HTML",
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
const textarea = document.createElement("textarea");
|
|
279
|
-
textarea.className = "job-description-textarea";
|
|
280
|
-
textarea.readOnly = true;
|
|
281
|
-
textarea.value = markdown;
|
|
282
|
-
|
|
283
227
|
return createDetailSection({
|
|
284
228
|
title: "Job Description",
|
|
285
|
-
content:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
),
|
|
293
|
-
div({ className: "button-group" }, copyButton, copyHtmlButton),
|
|
294
|
-
),
|
|
295
|
-
textarea,
|
|
296
|
-
),
|
|
229
|
+
content: createCodeDisplay({
|
|
230
|
+
content: markdown,
|
|
231
|
+
description:
|
|
232
|
+
"Copy this markdown-formatted job description for use in job postings, documentation, or sharing.",
|
|
233
|
+
toHtml: markdownToHtml,
|
|
234
|
+
minHeight: 450,
|
|
235
|
+
}),
|
|
297
236
|
});
|
|
298
237
|
}
|
|
299
238
|
|
|
@@ -228,7 +228,7 @@ export function stageToJsonLd(stage) {
|
|
|
228
228
|
identifier: stage.id,
|
|
229
229
|
name: stage.name,
|
|
230
230
|
description: stage.description,
|
|
231
|
-
...(stage.
|
|
231
|
+
...(stage.emojiIcon && { emojiIcon: stage.emojiIcon }),
|
|
232
232
|
...(stage.tools?.length > 0 && { tools: stage.tools }),
|
|
233
233
|
...(stage.constraints?.length > 0 && { constraints: stage.constraints }),
|
|
234
234
|
...(stage.handoffs && {
|
package/app/formatters/shared.js
CHANGED
|
@@ -4,6 +4,64 @@
|
|
|
4
4
|
* Common formatting functions used across different output formats (CLI, DOM, markdown)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Trim trailing newlines from a string value
|
|
9
|
+
* Used by template prepare functions for consistent output formatting.
|
|
10
|
+
* @param {string|null|undefined} value - Value to trim
|
|
11
|
+
* @returns {string|null} Trimmed value or null if empty
|
|
12
|
+
*/
|
|
13
|
+
export function trimValue(value) {
|
|
14
|
+
if (value == null) return null;
|
|
15
|
+
const trimmed = value.replace(/\n+$/, "");
|
|
16
|
+
return trimmed || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Trim a required field, preserving original if trim would result in empty
|
|
21
|
+
* Use for fields that must have a value.
|
|
22
|
+
* @param {string|null|undefined} value - Value to trim
|
|
23
|
+
* @returns {string} Trimmed value or original
|
|
24
|
+
*/
|
|
25
|
+
export function trimRequired(value) {
|
|
26
|
+
return trimValue(value) || value || "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Trim and split a string into lines
|
|
31
|
+
* @param {string|null|undefined} value - Value to process
|
|
32
|
+
* @returns {string[]} Array of lines (empty array if no value)
|
|
33
|
+
*/
|
|
34
|
+
export function splitLines(value) {
|
|
35
|
+
const trimmed = trimValue(value);
|
|
36
|
+
return trimmed ? trimmed.split("\n") : [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Transform an array of objects by applying trimValue to specified fields
|
|
41
|
+
* @param {Array<Object>} array - Array of objects to transform
|
|
42
|
+
* @param {Object<string, 'optional'|'required'|'array'>} fieldSpec - Fields to trim and their type
|
|
43
|
+
* - 'optional': use trimValue (returns null if empty)
|
|
44
|
+
* - 'required': use trimRequired (preserves original if empty)
|
|
45
|
+
* - 'array': trim each element in array field
|
|
46
|
+
* @returns {Array<Object>} Transformed array
|
|
47
|
+
*/
|
|
48
|
+
export function trimFields(array, fieldSpec) {
|
|
49
|
+
if (!array) return [];
|
|
50
|
+
return array.map((item) => {
|
|
51
|
+
const result = { ...item };
|
|
52
|
+
for (const [field, type] of Object.entries(fieldSpec)) {
|
|
53
|
+
if (type === "optional") {
|
|
54
|
+
result[field] = trimValue(item[field]);
|
|
55
|
+
} else if (type === "required") {
|
|
56
|
+
result[field] = trimRequired(item[field]);
|
|
57
|
+
} else if (type === "array") {
|
|
58
|
+
result[field] = (item[field] || []).map((v) => trimRequired(v));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
7
65
|
/**
|
|
8
66
|
* Format level as text with dots (for CLI/markdown)
|
|
9
67
|
* @param {number} level - 1-5
|