@forwardimpact/pathway 0.3.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 +1 -1
- package/app/commands/index.js +4 -3
- package/app/commands/skill.js +56 -2
- package/app/commands/tool.js +112 -0
- 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 -9
- package/app/formatters/agent/skill.js +48 -6
- package/app/formatters/job/description.js +21 -16
- package/app/formatters/job/dom.js +9 -70
- package/app/formatters/shared.js +58 -0
- 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/index.html +10 -3
- package/app/lib/card-mappers.js +64 -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 +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 +2 -2
- package/app/pages/landing.js +34 -14
- package/app/pages/self-assessment.js +7 -5
- 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/bin/pathway.js +31 -16
- 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 +5 -7
- package/examples/self-assessments.yaml +1 -1
- package/package.json +1 -1
- package/templates/skill.template.md +31 -12
|
@@ -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
|
}
|
|
@@ -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 { createMarkdownTextarea } from "../../components/markdown-textarea.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: createMarkdownTextarea({
|
|
230
|
+
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
|
|
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
|
|
@@ -18,8 +18,9 @@ import {
|
|
|
18
18
|
} from "../../lib/render.js";
|
|
19
19
|
import { createBackLink } from "../../components/nav.js";
|
|
20
20
|
import { createLevelCell } from "../../components/detail.js";
|
|
21
|
+
import { createMarkdownTextarea } from "../../components/markdown-textarea.js";
|
|
21
22
|
import { SKILL_LEVEL_ORDER } from "../../model/levels.js";
|
|
22
|
-
import { prepareSkillDetail
|
|
23
|
+
import { prepareSkillDetail } from "./shared.js";
|
|
23
24
|
import { createJsonLdScript, skillToJsonLd } from "../json-ld.js";
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -61,7 +62,7 @@ export function skillToDOM(
|
|
|
61
62
|
{ className: "page-meta" },
|
|
62
63
|
span(
|
|
63
64
|
{ className: "badge badge-default" },
|
|
64
|
-
|
|
65
|
+
`${view.capabilityEmoji} ${view.capability.toUpperCase()}`,
|
|
65
66
|
),
|
|
66
67
|
view.isHumanOnly
|
|
67
68
|
? span(
|
|
@@ -97,6 +98,60 @@ export function skillToDOM(
|
|
|
97
98
|
),
|
|
98
99
|
),
|
|
99
100
|
|
|
101
|
+
// Recommended Tools
|
|
102
|
+
view.toolReferences.length > 0
|
|
103
|
+
? div(
|
|
104
|
+
{ className: "detail-section" },
|
|
105
|
+
heading2({ className: "section-title" }, "Recommended Tools"),
|
|
106
|
+
table(
|
|
107
|
+
{ className: "tools-table" },
|
|
108
|
+
thead({}, tr({}, th({}, "Tool"), th({}, "Use When"))),
|
|
109
|
+
tbody(
|
|
110
|
+
{},
|
|
111
|
+
...view.toolReferences.map((tool) =>
|
|
112
|
+
tr(
|
|
113
|
+
{},
|
|
114
|
+
td(
|
|
115
|
+
{},
|
|
116
|
+
tool.url
|
|
117
|
+
? a(
|
|
118
|
+
{
|
|
119
|
+
href: tool.url,
|
|
120
|
+
target: "_blank",
|
|
121
|
+
rel: "noopener noreferrer",
|
|
122
|
+
},
|
|
123
|
+
tool.name,
|
|
124
|
+
)
|
|
125
|
+
: tool.name,
|
|
126
|
+
),
|
|
127
|
+
td({}, tool.useWhen),
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
showBackLink
|
|
133
|
+
? p(
|
|
134
|
+
{ className: "see-all-link" },
|
|
135
|
+
a({ href: "#/tool" }, "See all tools →"),
|
|
136
|
+
)
|
|
137
|
+
: null,
|
|
138
|
+
)
|
|
139
|
+
: null,
|
|
140
|
+
|
|
141
|
+
// Implementation Reference
|
|
142
|
+
view.implementationReference
|
|
143
|
+
? div(
|
|
144
|
+
{ className: "detail-section" },
|
|
145
|
+
heading2({ className: "section-title" }, "Implementation Patterns"),
|
|
146
|
+
createMarkdownTextarea({
|
|
147
|
+
markdown: view.implementationReference,
|
|
148
|
+
description:
|
|
149
|
+
"Project-specific implementation guidance for this skill.",
|
|
150
|
+
minHeight: 450,
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
: null,
|
|
154
|
+
|
|
100
155
|
// Used in Disciplines and Linked to Drivers in two columns
|
|
101
156
|
view.relatedDisciplines.length > 0 || view.relatedDrivers.length > 0
|
|
102
157
|
? div(
|
|
@@ -105,5 +105,23 @@ export function skillToMarkdown(
|
|
|
105
105
|
lines.push("");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// Recommended tools
|
|
109
|
+
if (view.toolReferences.length > 0) {
|
|
110
|
+
lines.push("## Recommended Tools", "");
|
|
111
|
+
const toolRows = view.toolReferences.map((tool) => [
|
|
112
|
+
tool.url ? `[${tool.name}](${tool.url})` : tool.name,
|
|
113
|
+
tool.useWhen,
|
|
114
|
+
]);
|
|
115
|
+
lines.push(tableToMarkdown(["Tool", "Use When"], toolRows));
|
|
116
|
+
lines.push("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Implementation reference
|
|
120
|
+
if (view.implementationReference) {
|
|
121
|
+
lines.push("## Implementation Patterns", "");
|
|
122
|
+
lines.push(view.implementationReference);
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
125
|
+
|
|
108
126
|
return lines.join("\n");
|
|
109
127
|
}
|
|
@@ -13,12 +13,12 @@ import { truncate } from "../shared.js";
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Format capability name for display
|
|
16
|
-
* @param {string} capability
|
|
16
|
+
* @param {string} capabilityName - The capability name to display
|
|
17
17
|
* @returns {string}
|
|
18
18
|
*/
|
|
19
|
-
export function formatCapability(
|
|
20
|
-
if (!
|
|
21
|
-
return
|
|
19
|
+
export function formatCapability(capabilityName) {
|
|
20
|
+
if (!capabilityName) return "";
|
|
21
|
+
return capabilityName;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -66,12 +66,15 @@ export function prepareSkillsList(
|
|
|
66
66
|
* @property {string} name
|
|
67
67
|
* @property {string} description
|
|
68
68
|
* @property {string} capability
|
|
69
|
+
* @property {string} capabilityName
|
|
69
70
|
* @property {boolean} isHumanOnly
|
|
70
71
|
* @property {string} capabilityEmoji
|
|
71
72
|
* @property {Object<string, string>} levelDescriptions
|
|
72
73
|
* @property {Array<{id: string, name: string, skillType: string}>} relatedDisciplines
|
|
73
74
|
* @property {Array<{id: string, name: string, modifier: number}>} relatedTracks
|
|
74
75
|
* @property {Array<{id: string, name: string}>} relatedDrivers
|
|
76
|
+
* @property {Array<{name: string, url?: string, description: string, useWhen: string}>} toolReferences
|
|
77
|
+
* @property {string|null} implementationReference
|
|
75
78
|
*/
|
|
76
79
|
|
|
77
80
|
/**
|
|
@@ -110,16 +113,21 @@ export function prepareSkillDetail(
|
|
|
110
113
|
.filter((d) => d.contributingSkills?.includes(skill.id))
|
|
111
114
|
.map((d) => ({ id: d.id, name: d.name }));
|
|
112
115
|
|
|
116
|
+
const capabilityEntity = capabilities.find((c) => c.id === skill.capability);
|
|
117
|
+
|
|
113
118
|
return {
|
|
114
119
|
id: skill.id,
|
|
115
120
|
name: skill.name,
|
|
116
121
|
description: skill.description,
|
|
117
122
|
capability: skill.capability,
|
|
123
|
+
capabilityName: capabilityEntity?.name || skill.capability,
|
|
118
124
|
isHumanOnly: skill.isHumanOnly || false,
|
|
119
125
|
capabilityEmoji: getCapabilityEmoji(capabilities, skill.capability),
|
|
120
126
|
levelDescriptions: skill.levelDescriptions,
|
|
121
127
|
relatedDisciplines,
|
|
122
128
|
relatedTracks,
|
|
123
129
|
relatedDrivers,
|
|
130
|
+
toolReferences: skill.toolReferences || [],
|
|
131
|
+
implementationReference: skill.implementationReference || null,
|
|
124
132
|
};
|
|
125
133
|
}
|
|
@@ -32,7 +32,7 @@ export function stageListToMicrodata(stages) {
|
|
|
32
32
|
? `→ ${stage.handoffs.map((h) => h.target).join(", ")}`
|
|
33
33
|
: "";
|
|
34
34
|
return `${openTag("article", { itemtype: "Stage", itemid: `#${stage.id}` })}
|
|
35
|
-
${prop("h2", "name", `${stage.emoji
|
|
35
|
+
${prop("h2", "name", `${stage.emoji} ${stage.name}`)}
|
|
36
36
|
${prop("p", "description", stage.truncatedDescription)}
|
|
37
37
|
${handoffText ? `<p>Handoffs: ${handoffText}</p>` : ""}
|
|
38
38
|
</article>`;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool presentation helpers
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for formatting tool data across DOM and CLI outputs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} ToolUsage
|
|
9
|
+
* @property {string} skillId
|
|
10
|
+
* @property {string} skillName
|
|
11
|
+
* @property {string} capabilityId
|
|
12
|
+
* @property {string} useWhen
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} AggregatedTool
|
|
17
|
+
* @property {string} name
|
|
18
|
+
* @property {string} [url]
|
|
19
|
+
* @property {string} description
|
|
20
|
+
* @property {ToolUsage[]} usages
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Aggregate tools from all skills, deduplicating by name
|
|
25
|
+
* @param {Array} skills - All skills with toolReferences
|
|
26
|
+
* @returns {AggregatedTool[]}
|
|
27
|
+
*/
|
|
28
|
+
export function aggregateTools(skills) {
|
|
29
|
+
const toolMap = new Map();
|
|
30
|
+
|
|
31
|
+
for (const skill of skills) {
|
|
32
|
+
if (!skill.toolReferences) continue;
|
|
33
|
+
|
|
34
|
+
for (const tool of skill.toolReferences) {
|
|
35
|
+
const usage = {
|
|
36
|
+
skillId: skill.id,
|
|
37
|
+
skillName: skill.name,
|
|
38
|
+
capabilityId: skill.capability,
|
|
39
|
+
useWhen: tool.useWhen,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const existing = toolMap.get(tool.name);
|
|
43
|
+
if (existing) {
|
|
44
|
+
existing.usages.push(usage);
|
|
45
|
+
} else {
|
|
46
|
+
toolMap.set(tool.name, {
|
|
47
|
+
name: tool.name,
|
|
48
|
+
url: tool.url,
|
|
49
|
+
description: tool.description,
|
|
50
|
+
usages: [usage],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Array.from(toolMap.values()).sort((a, b) =>
|
|
57
|
+
a.name.localeCompare(b.name),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Prepare tools list view data
|
|
63
|
+
* @param {Array} skills - All skills
|
|
64
|
+
* @returns {{ tools: AggregatedTool[], totalCount: number }}
|
|
65
|
+
*/
|
|
66
|
+
export function prepareToolsList(skills) {
|
|
67
|
+
const tools = aggregateTools(skills);
|
|
68
|
+
return {
|
|
69
|
+
tools,
|
|
70
|
+
totalCount: tools.length,
|
|
71
|
+
};
|
|
72
|
+
}
|
package/app/handout-main.js
CHANGED
|
@@ -135,25 +135,25 @@ function renderIndex(data) {
|
|
|
135
135
|
`${getConceptEmoji(framework, "job")} ${framework.entityDefinitions.job.title}`,
|
|
136
136
|
),
|
|
137
137
|
" - ",
|
|
138
|
-
`${data.disciplines.length} disciplines, ${data.
|
|
138
|
+
`${data.disciplines.length} disciplines, ${data.grades.length} grades, ${data.tracks.length} tracks`,
|
|
139
139
|
),
|
|
140
140
|
li(
|
|
141
141
|
{},
|
|
142
142
|
a(
|
|
143
|
-
{ href: "#/
|
|
144
|
-
`${getConceptEmoji(framework, "
|
|
143
|
+
{ href: "#/behaviour" },
|
|
144
|
+
`${getConceptEmoji(framework, "behaviour")} ${framework.entityDefinitions.behaviour.title}`,
|
|
145
145
|
),
|
|
146
146
|
" - ",
|
|
147
|
-
`${data.
|
|
147
|
+
`${data.behaviours.length} behaviour definitions`,
|
|
148
148
|
),
|
|
149
149
|
li(
|
|
150
150
|
{},
|
|
151
151
|
a(
|
|
152
|
-
{ href: "#/
|
|
153
|
-
`${getConceptEmoji(framework, "
|
|
152
|
+
{ href: "#/skill" },
|
|
153
|
+
`${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
|
|
154
154
|
),
|
|
155
155
|
" - ",
|
|
156
|
-
`${data.
|
|
156
|
+
`${data.skills.length} skill definitions`,
|
|
157
157
|
),
|
|
158
158
|
li(
|
|
159
159
|
{},
|
package/app/index.html
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Engineering Pathway</title>
|
|
7
7
|
<link rel="stylesheet" href="css/bundles/app.css" />
|
|
8
|
+
<link
|
|
9
|
+
rel="stylesheet"
|
|
10
|
+
href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"
|
|
11
|
+
/>
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markdown.min.js"></script>
|
|
8
14
|
<script type="importmap">
|
|
9
15
|
{
|
|
10
16
|
"imports": {
|
|
@@ -31,12 +37,13 @@
|
|
|
31
37
|
</button>
|
|
32
38
|
<ul class="nav-links" id="nav-links">
|
|
33
39
|
<li><a href="#/discipline">Disciplines</a></li>
|
|
34
|
-
<li><a href="#/track">Tracks</a></li>
|
|
35
40
|
<li><a href="#/grade">Grades</a></li>
|
|
36
|
-
<li><a href="#/
|
|
41
|
+
<li><a href="#/track">Tracks</a></li>
|
|
37
42
|
<li><a href="#/behaviour">Behaviours</a></li>
|
|
38
|
-
<li><a href="#/
|
|
43
|
+
<li><a href="#/skill">Skills</a></li>
|
|
39
44
|
<li><a href="#/driver">Drivers</a></li>
|
|
45
|
+
<li><a href="#/stage">Stages</a></li>
|
|
46
|
+
<li><a href="#/tool">Tools</a></li>
|
|
40
47
|
<li><a href="#/job-builder" class="nav-cta">Build a Job</a></li>
|
|
41
48
|
<li><a href="#/agent-builder" class="nav-cta">Build an Agent</a></li>
|
|
42
49
|
<li><a href="#/interview-prep" class="nav-cta">Interview Prep</a></li>
|