@forwardimpact/pathway 0.1.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 +119 -31
- package/app/commands/command-factory.js +3 -3
- package/app/commands/interview.js +14 -7
- package/app/commands/job.js +52 -33
- package/app/commands/progress.js +14 -7
- package/app/commands/serve.js +5 -0
- package/app/commands/stage.js +0 -10
- package/app/commands/track.js +5 -8
- package/app/components/builder.js +117 -30
- package/app/css/components/surfaces.css +16 -0
- package/app/formatters/agent/profile.js +30 -115
- package/app/formatters/agent/skill.js +23 -44
- package/app/formatters/behaviour/dom.js +3 -0
- package/app/formatters/behaviour/microdata.js +106 -0
- package/app/formatters/discipline/dom.js +28 -1
- package/app/formatters/discipline/microdata.js +117 -0
- package/app/formatters/discipline/shared.js +49 -8
- package/app/formatters/driver/dom.js +3 -0
- package/app/formatters/driver/microdata.js +91 -0
- package/app/formatters/grade/dom.js +5 -4
- package/app/formatters/grade/microdata.js +151 -0
- package/app/formatters/index.js +32 -1
- package/app/formatters/interview/shared.js +13 -8
- package/app/formatters/job/description.js +70 -81
- package/app/formatters/job/dom.js +40 -113
- package/app/formatters/job/markdown.js +17 -13
- package/app/formatters/json-ld.js +242 -0
- package/app/formatters/microdata-shared.js +184 -0
- package/app/formatters/progress/shared.js +14 -11
- package/app/formatters/shared.js +7 -2
- package/app/formatters/skill/dom.js +3 -0
- package/app/formatters/skill/microdata.js +151 -0
- package/app/formatters/stage/dom.js +3 -18
- package/app/formatters/stage/microdata.js +110 -0
- package/app/formatters/stage/shared.js +0 -27
- package/app/formatters/track/dom.js +5 -30
- package/app/formatters/track/markdown.js +2 -25
- package/app/formatters/track/microdata.js +111 -0
- package/app/formatters/track/shared.js +6 -58
- package/app/handout-main.js +26 -12
- package/app/handout.html +7 -0
- package/app/index.html +11 -0
- package/app/lib/card-mappers.js +17 -12
- package/app/lib/form-controls.js +64 -1
- package/app/lib/job-cache.js +12 -9
- package/app/lib/render.js +8 -1
- package/app/lib/template-loader.js +75 -0
- package/app/lib/yaml-loader.js +25 -8
- package/app/main.js +8 -4
- package/app/model/agent.js +158 -130
- package/app/model/checklist.js +57 -91
- package/app/model/derivation.js +135 -68
- package/app/model/index-generator.js +1 -7
- package/app/model/job.js +19 -13
- package/app/model/levels.js +20 -12
- package/app/model/loader.js +41 -17
- package/app/model/matching.js +33 -3
- package/app/model/profile.js +38 -45
- package/app/model/schema-validation.js +438 -0
- package/app/model/validation.js +747 -68
- package/app/pages/agent-builder.js +125 -28
- package/app/pages/assessment-results.js +10 -4
- package/app/pages/discipline.js +36 -6
- package/app/pages/driver.js +9 -47
- package/app/pages/interview-builder.js +3 -1
- package/app/pages/interview.js +15 -4
- package/app/pages/job-builder.js +4 -1
- package/app/pages/job.js +43 -8
- package/app/pages/landing.js +10 -10
- package/app/pages/progress-builder.js +3 -1
- package/app/pages/progress.js +78 -26
- package/app/pages/self-assessment.js +3 -3
- package/app/pages/stage.js +3 -126
- package/app/slide-main.js +45 -17
- package/app/slides/index.js +3 -1
- package/app/slides/overview.js +40 -4
- package/app/slides/progress.js +4 -2
- package/app/slides.html +7 -0
- package/bin/pathway.js +28 -75
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
- package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
- package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
- package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
- package/examples/agents/.vscode/settings.json +1 -1
- package/examples/behaviours/outcome_ownership.yaml +1 -2
- package/examples/behaviours/polymathic_knowledge.yaml +1 -2
- package/examples/behaviours/precise_communication.yaml +1 -2
- package/examples/behaviours/relentless_curiosity.yaml +1 -2
- package/examples/behaviours/systems_thinking.yaml +1 -2
- package/examples/capabilities/business.yaml +80 -142
- package/examples/capabilities/delivery.yaml +155 -219
- package/examples/capabilities/people.yaml +2 -34
- package/examples/capabilities/reliability.yaml +161 -80
- package/examples/capabilities/scale.yaml +234 -252
- package/examples/copilot-setup-steps.yaml +25 -0
- package/examples/devcontainer.yaml +21 -0
- package/examples/disciplines/_index.yaml +1 -0
- package/examples/disciplines/data_engineering.yaml +14 -12
- package/examples/disciplines/engineering_management.yaml +63 -0
- package/examples/disciplines/software_engineering.yaml +14 -12
- package/examples/drivers.yaml +1 -4
- package/examples/framework.yaml +1 -2
- package/examples/grades.yaml +14 -15
- package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
- package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
- package/examples/questions/behaviours/precise_communication.yaml +1 -2
- package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
- package/examples/questions/behaviours/systems_thinking.yaml +1 -2
- package/examples/questions/skills/architecture_design.yaml +1 -2
- package/examples/questions/skills/cloud_platforms.yaml +1 -2
- package/examples/questions/skills/code_quality.yaml +1 -2
- package/examples/questions/skills/data_modeling.yaml +1 -2
- package/examples/questions/skills/devops.yaml +1 -2
- package/examples/questions/skills/full_stack_development.yaml +1 -2
- package/examples/questions/skills/sre_practices.yaml +1 -2
- package/examples/questions/skills/stakeholder_management.yaml +1 -2
- package/examples/questions/skills/team_collaboration.yaml +1 -2
- package/examples/questions/skills/technical_writing.yaml +1 -2
- package/examples/self-assessments.yaml +1 -3
- package/examples/stages.yaml +101 -46
- package/examples/tracks/_index.yaml +0 -1
- package/examples/tracks/platform.yaml +8 -13
- package/examples/tracks/sre.yaml +8 -18
- package/examples/vscode-settings.yaml +2 -7
- package/package.json +9 -3
- package/templates/agent.template.md +65 -0
- package/templates/job.template.md +47 -0
- package/templates/skill.template.md +28 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
- package/examples/tracks/manager.yaml +0 -53
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { div, h2, p, a, span, ul, li } from "../../lib/render.js";
|
|
6
6
|
import { createBackLink } from "../../components/nav.js";
|
|
7
7
|
import { prepareStageDetail, getStageEmoji } from "./shared.js";
|
|
8
|
+
import { createJsonLdScript, stageToJsonLd } from "../json-ld.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Format stage detail as DOM elements
|
|
@@ -20,6 +21,8 @@ export function stageToDOM(stage, { stages = [], showBackLink = true } = {}) {
|
|
|
20
21
|
|
|
21
22
|
return div(
|
|
22
23
|
{ className: "detail-page stage-detail" },
|
|
24
|
+
// JSON-LD structured data
|
|
25
|
+
createJsonLdScript(stageToJsonLd(stage)),
|
|
23
26
|
// Header
|
|
24
27
|
div(
|
|
25
28
|
{ className: "page-header" },
|
|
@@ -27,28 +30,10 @@ export function stageToDOM(stage, { stages = [], showBackLink = true } = {}) {
|
|
|
27
30
|
div(
|
|
28
31
|
{ className: "page-title-row" },
|
|
29
32
|
span({ className: "page-title" }, `${emoji} ${view.name}`),
|
|
30
|
-
span({ className: `badge ${view.modeClassName}` }, view.modeBadge),
|
|
31
33
|
),
|
|
32
34
|
p({ className: "page-description" }, view.description),
|
|
33
35
|
),
|
|
34
36
|
|
|
35
|
-
// Tools section
|
|
36
|
-
view.tools.length > 0
|
|
37
|
-
? div(
|
|
38
|
-
{ className: "section section-detail" },
|
|
39
|
-
h2({ className: "section-title" }, "Available Tools"),
|
|
40
|
-
div(
|
|
41
|
-
{ className: "tool-badges" },
|
|
42
|
-
...view.tools.map((tool) =>
|
|
43
|
-
span(
|
|
44
|
-
{ className: "badge badge-tool", title: tool.label },
|
|
45
|
-
`${tool.icon} ${tool.label}`,
|
|
46
|
-
),
|
|
47
|
-
),
|
|
48
|
-
),
|
|
49
|
-
)
|
|
50
|
-
: null,
|
|
51
|
-
|
|
52
37
|
// Entry/Exit Criteria
|
|
53
38
|
view.entryCriteria.length > 0 || view.exitCriteria.length > 0
|
|
54
39
|
? div(
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage formatting for microdata HTML output
|
|
3
|
+
*
|
|
4
|
+
* Generates clean, class-less HTML with microdata aligned with stages.schema.json
|
|
5
|
+
* RDF vocab: https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
openTag,
|
|
10
|
+
prop,
|
|
11
|
+
metaTag,
|
|
12
|
+
linkTag,
|
|
13
|
+
section,
|
|
14
|
+
ul,
|
|
15
|
+
escapeHtml,
|
|
16
|
+
htmlDocument,
|
|
17
|
+
} from "../microdata-shared.js";
|
|
18
|
+
import { prepareStagesList, prepareStageDetail } from "./shared.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format stage list as microdata HTML
|
|
22
|
+
* @param {Array} stages - Raw stage entities
|
|
23
|
+
* @returns {string} HTML with microdata
|
|
24
|
+
*/
|
|
25
|
+
export function stageListToMicrodata(stages) {
|
|
26
|
+
const { items } = prepareStagesList(stages);
|
|
27
|
+
|
|
28
|
+
const content = items
|
|
29
|
+
.map((stage) => {
|
|
30
|
+
const handoffText =
|
|
31
|
+
stage.handoffs.length > 0
|
|
32
|
+
? `→ ${stage.handoffs.map((h) => h.target).join(", ")}`
|
|
33
|
+
: "";
|
|
34
|
+
return `${openTag("article", { itemtype: "Stage", itemid: `#${stage.id}` })}
|
|
35
|
+
${prop("h2", "name", `${stage.emoji || ""} ${stage.name}`)}
|
|
36
|
+
${prop("p", "description", stage.truncatedDescription)}
|
|
37
|
+
${handoffText ? `<p>Handoffs: ${handoffText}</p>` : ""}
|
|
38
|
+
</article>`;
|
|
39
|
+
})
|
|
40
|
+
.join("\n");
|
|
41
|
+
|
|
42
|
+
return htmlDocument(
|
|
43
|
+
"Stages",
|
|
44
|
+
`<main>
|
|
45
|
+
<h1>Stages</h1>
|
|
46
|
+
${content}
|
|
47
|
+
</main>`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format stage detail as microdata HTML
|
|
53
|
+
* @param {Object} stage - Raw stage entity
|
|
54
|
+
* @returns {string} HTML with microdata
|
|
55
|
+
*/
|
|
56
|
+
export function stageToMicrodata(stage) {
|
|
57
|
+
const view = prepareStageDetail(stage);
|
|
58
|
+
|
|
59
|
+
if (!view) return "";
|
|
60
|
+
|
|
61
|
+
const sections = [];
|
|
62
|
+
|
|
63
|
+
// Entry criteria
|
|
64
|
+
if (view.entryCriteria.length > 0) {
|
|
65
|
+
const criteriaItems = view.entryCriteria.map((c) => escapeHtml(c));
|
|
66
|
+
sections.push(
|
|
67
|
+
section("Entry Criteria", ul(criteriaItems, "entryCriteria"), 2),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Constraints
|
|
72
|
+
if (view.constraints.length > 0) {
|
|
73
|
+
const constraintItems = view.constraints.map((c) => escapeHtml(c));
|
|
74
|
+
sections.push(
|
|
75
|
+
section("Constraints", ul(constraintItems, "constraints"), 2),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Exit criteria
|
|
80
|
+
if (view.exitCriteria.length > 0) {
|
|
81
|
+
const exitItems = view.exitCriteria.map((c) => escapeHtml(c));
|
|
82
|
+
sections.push(section("Exit Criteria", ul(exitItems, "exitCriteria"), 2));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Handoffs - using Handoff itemtype
|
|
86
|
+
if (view.handoffs.length > 0) {
|
|
87
|
+
const handoffItems = view.handoffs.map(
|
|
88
|
+
(
|
|
89
|
+
h,
|
|
90
|
+
) => `${openTag("article", { itemtype: "Handoff", itemprop: "handoffs" })}
|
|
91
|
+
${linkTag("targetStage", `#${h.target}`)}
|
|
92
|
+
<p><strong>${prop("span", "label", h.label)}</strong> → ${escapeHtml(h.target)}</p>
|
|
93
|
+
${prop("p", "prompt", h.prompt)}
|
|
94
|
+
</article>`,
|
|
95
|
+
);
|
|
96
|
+
sections.push(section("Handoffs", handoffItems.join("\n"), 2));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const body = `<main>
|
|
100
|
+
${openTag("article", { itemtype: "Stage", itemid: `#${view.id}` })}
|
|
101
|
+
${prop("h1", "name", view.name)}
|
|
102
|
+
${metaTag("id", view.id)}
|
|
103
|
+
${stage.emoji ? metaTag("emoji", stage.emoji) : ""}
|
|
104
|
+
${prop("p", "description", view.description)}
|
|
105
|
+
${sections.join("\n")}
|
|
106
|
+
</article>
|
|
107
|
+
</main>`;
|
|
108
|
+
|
|
109
|
+
return htmlDocument(view.name, body);
|
|
110
|
+
}
|
|
@@ -6,19 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import { truncate } from "../shared.js";
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Tool display configuration
|
|
11
|
-
* @type {Object<string, {icon: string, label: string}>}
|
|
12
|
-
*/
|
|
13
|
-
const TOOL_CONFIG = {
|
|
14
|
-
search: { icon: "🔍", label: "Search" },
|
|
15
|
-
fetch: { icon: "🌐", label: "Fetch" },
|
|
16
|
-
codebase: { icon: "📂", label: "Codebase" },
|
|
17
|
-
read: { icon: "📖", label: "Read" },
|
|
18
|
-
edit: { icon: "✏️", label: "Edit" },
|
|
19
|
-
terminal: { icon: "💻", label: "Terminal" },
|
|
20
|
-
};
|
|
21
|
-
|
|
22
9
|
/**
|
|
23
10
|
* @typedef {Object} StageListItem
|
|
24
11
|
* @property {string} id
|
|
@@ -26,7 +13,6 @@ const TOOL_CONFIG = {
|
|
|
26
13
|
* @property {string} emoji
|
|
27
14
|
* @property {string} description
|
|
28
15
|
* @property {string} truncatedDescription
|
|
29
|
-
* @property {Array<{id: string, icon: string, label: string}>} tools
|
|
30
16
|
* @property {Array<{target: string, label: string}>} handoffs
|
|
31
17
|
*/
|
|
32
18
|
|
|
@@ -38,18 +24,12 @@ const TOOL_CONFIG = {
|
|
|
38
24
|
*/
|
|
39
25
|
export function prepareStagesList(stages, descriptionLimit = 150) {
|
|
40
26
|
const items = stages.map((stage) => {
|
|
41
|
-
const tools = stage.availableTools || [];
|
|
42
27
|
return {
|
|
43
28
|
id: stage.id,
|
|
44
29
|
name: stage.name,
|
|
45
30
|
emoji: stage.emoji,
|
|
46
31
|
description: stage.description,
|
|
47
32
|
truncatedDescription: truncate(stage.description, descriptionLimit),
|
|
48
|
-
tools: tools.map((toolId) => ({
|
|
49
|
-
id: toolId,
|
|
50
|
-
icon: TOOL_CONFIG[toolId]?.icon || "🔧",
|
|
51
|
-
label: TOOL_CONFIG[toolId]?.label || toolId,
|
|
52
|
-
})),
|
|
53
33
|
handoffs: (stage.handoffs || []).map((h) => ({
|
|
54
34
|
target: h.targetStage,
|
|
55
35
|
label: h.label,
|
|
@@ -65,7 +45,6 @@ export function prepareStagesList(stages, descriptionLimit = 150) {
|
|
|
65
45
|
* @property {string} id
|
|
66
46
|
* @property {string} name
|
|
67
47
|
* @property {string} description
|
|
68
|
-
* @property {Array<{id: string, icon: string, label: string}>} tools
|
|
69
48
|
* @property {string[]} constraints
|
|
70
49
|
* @property {string[]} entryCriteria
|
|
71
50
|
* @property {string[]} exitCriteria
|
|
@@ -78,16 +57,10 @@ export function prepareStagesList(stages, descriptionLimit = 150) {
|
|
|
78
57
|
* @returns {StageDetailView}
|
|
79
58
|
*/
|
|
80
59
|
export function prepareStageDetail(stage) {
|
|
81
|
-
const tools = stage.availableTools || [];
|
|
82
60
|
return {
|
|
83
61
|
id: stage.id,
|
|
84
62
|
name: stage.name,
|
|
85
63
|
description: stage.description,
|
|
86
|
-
tools: tools.map((toolId) => ({
|
|
87
|
-
id: toolId,
|
|
88
|
-
icon: TOOL_CONFIG[toolId]?.icon || "🔧",
|
|
89
|
-
label: TOOL_CONFIG[toolId]?.label || toolId,
|
|
90
|
-
})),
|
|
91
64
|
constraints: stage.constraints || [],
|
|
92
65
|
entryCriteria: stage.entryCriteria || [],
|
|
93
66
|
exitCriteria: stage.exitCriteria || [],
|
|
@@ -4,12 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { div, h1, p } from "../../lib/render.js";
|
|
6
6
|
import { createBackLink } from "../../components/nav.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createStatCard } from "../../components/card.js";
|
|
8
8
|
import { createStatsGrid } from "../../components/grid.js";
|
|
9
|
-
import {
|
|
10
|
-
createDetailSection,
|
|
11
|
-
createLinksList,
|
|
12
|
-
} from "../../components/detail.js";
|
|
9
|
+
import { createDetailSection } from "../../components/detail.js";
|
|
13
10
|
import {
|
|
14
11
|
createJobBuilderButton,
|
|
15
12
|
createInterviewPrepButton,
|
|
@@ -20,22 +17,7 @@ import {
|
|
|
20
17
|
} from "../../components/modifier-table.js";
|
|
21
18
|
import { getConceptEmoji } from "../../model/levels.js";
|
|
22
19
|
import { prepareTrackDetail } from "./shared.js";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get track type badge(s)
|
|
26
|
-
* @param {Object} view
|
|
27
|
-
* @returns {HTMLElement[]}
|
|
28
|
-
*/
|
|
29
|
-
function getTrackTypeBadges(view) {
|
|
30
|
-
const badges = [];
|
|
31
|
-
if (view.isProfessional) {
|
|
32
|
-
badges.push(createBadge("Professional", "secondary"));
|
|
33
|
-
}
|
|
34
|
-
if (view.isManagement) {
|
|
35
|
-
badges.push(createBadge("Management", "default"));
|
|
36
|
-
}
|
|
37
|
-
return badges;
|
|
38
|
-
}
|
|
20
|
+
import { createJsonLdScript, trackToJsonLd } from "../json-ld.js";
|
|
39
21
|
|
|
40
22
|
/**
|
|
41
23
|
* Format track detail as DOM elements
|
|
@@ -80,12 +62,13 @@ export function trackToDOM(
|
|
|
80
62
|
|
|
81
63
|
return div(
|
|
82
64
|
{ className: "detail-page track-detail" },
|
|
65
|
+
// JSON-LD structured data
|
|
66
|
+
createJsonLdScript(trackToJsonLd(track)),
|
|
83
67
|
// Header
|
|
84
68
|
div(
|
|
85
69
|
{ className: "page-header" },
|
|
86
70
|
createBackLink("/track", "← Back to Tracks"),
|
|
87
71
|
h1({ className: "page-title" }, `${emoji} `, view.name),
|
|
88
|
-
div({ className: "page-meta" }, ...getTrackTypeBadges(view)),
|
|
89
72
|
p(
|
|
90
73
|
{ className: "text-muted", style: "margin-top: 0.5rem" },
|
|
91
74
|
view.description,
|
|
@@ -97,14 +80,6 @@ export function trackToDOM(
|
|
|
97
80
|
),
|
|
98
81
|
),
|
|
99
82
|
|
|
100
|
-
// Valid disciplines (if restricted)
|
|
101
|
-
view.validDisciplines.length > 0
|
|
102
|
-
? createDetailSection({
|
|
103
|
-
title: "Valid Disciplines",
|
|
104
|
-
content: createLinksList(view.validDisciplines, "/discipline"),
|
|
105
|
-
})
|
|
106
|
-
: null,
|
|
107
|
-
|
|
108
83
|
// Matching weights (stat cards)
|
|
109
84
|
track.matchingWeights
|
|
110
85
|
? createDetailSection({
|
|
@@ -18,10 +18,7 @@ export function trackListToMarkdown(tracks, framework) {
|
|
|
18
18
|
const lines = [`# ${emoji} Tracks`, ""];
|
|
19
19
|
|
|
20
20
|
for (const track of items) {
|
|
21
|
-
|
|
22
|
-
if (track.isProfessional) types.push("Professional");
|
|
23
|
-
if (track.isManagement) types.push("Management");
|
|
24
|
-
lines.push(`- **${track.name}**: ${types.join(", ")}`);
|
|
21
|
+
lines.push(`- **${track.name}**`);
|
|
25
22
|
}
|
|
26
23
|
lines.push("");
|
|
27
24
|
|
|
@@ -44,19 +41,8 @@ export function trackToMarkdown(
|
|
|
44
41
|
) {
|
|
45
42
|
const view = prepareTrackDetail(track, { skills, behaviours, disciplines });
|
|
46
43
|
|
|
47
|
-
const types = [];
|
|
48
|
-
if (view.isProfessional) types.push("Professional");
|
|
49
|
-
if (view.isManagement) types.push("Management");
|
|
50
|
-
|
|
51
44
|
const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
|
|
52
|
-
const lines = [
|
|
53
|
-
`# ${emoji} ${view.name}`,
|
|
54
|
-
"",
|
|
55
|
-
`**Type**: ${types.join(", ")}`,
|
|
56
|
-
"",
|
|
57
|
-
view.description,
|
|
58
|
-
"",
|
|
59
|
-
];
|
|
45
|
+
const lines = [`# ${emoji} ${view.name}`, "", view.description, ""];
|
|
60
46
|
|
|
61
47
|
// Skill modifiers - show expanded skills for capabilities
|
|
62
48
|
if (view.skillModifiers.length > 0) {
|
|
@@ -92,14 +78,5 @@ export function trackToMarkdown(
|
|
|
92
78
|
lines.push("");
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
// Valid disciplines
|
|
96
|
-
if (view.validDisciplines.length > 0) {
|
|
97
|
-
lines.push("## Valid Disciplines", "");
|
|
98
|
-
for (const d of view.validDisciplines) {
|
|
99
|
-
lines.push(`- ${d.name}`);
|
|
100
|
-
}
|
|
101
|
-
lines.push("");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
81
|
return lines.join("\n");
|
|
105
82
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track formatting for microdata HTML output
|
|
3
|
+
*
|
|
4
|
+
* Generates clean, class-less HTML with microdata aligned with track.schema.json
|
|
5
|
+
* RDF vocab: https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
openTag,
|
|
10
|
+
prop,
|
|
11
|
+
metaTag,
|
|
12
|
+
linkTag,
|
|
13
|
+
section,
|
|
14
|
+
ul,
|
|
15
|
+
escapeHtml,
|
|
16
|
+
htmlDocument,
|
|
17
|
+
} from "../microdata-shared.js";
|
|
18
|
+
import { prepareTracksList, prepareTrackDetail } from "./shared.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format track list as microdata HTML
|
|
22
|
+
* @param {Array} tracks - Raw track entities
|
|
23
|
+
* @returns {string} HTML with microdata
|
|
24
|
+
*/
|
|
25
|
+
export function trackListToMicrodata(tracks) {
|
|
26
|
+
const { items } = prepareTracksList(tracks);
|
|
27
|
+
|
|
28
|
+
const content = items
|
|
29
|
+
.map((track) => {
|
|
30
|
+
return `${openTag("article", { itemtype: "Track", itemid: `#${track.id}` })}
|
|
31
|
+
${prop("h2", "name", track.name)}
|
|
32
|
+
</article>`;
|
|
33
|
+
})
|
|
34
|
+
.join("\n");
|
|
35
|
+
|
|
36
|
+
return htmlDocument(
|
|
37
|
+
"Tracks",
|
|
38
|
+
`<main>
|
|
39
|
+
<h1>Tracks</h1>
|
|
40
|
+
${content}
|
|
41
|
+
</main>`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format track detail as microdata HTML
|
|
47
|
+
* @param {Object} track - Raw track entity
|
|
48
|
+
* @param {Object} context - Additional context
|
|
49
|
+
* @param {Array} context.skills - All skills
|
|
50
|
+
* @param {Array} context.behaviours - All behaviours
|
|
51
|
+
* @param {Array} context.disciplines - All disciplines
|
|
52
|
+
* @returns {string} HTML with microdata
|
|
53
|
+
*/
|
|
54
|
+
export function trackToMicrodata(track, { skills, behaviours, disciplines }) {
|
|
55
|
+
const view = prepareTrackDetail(track, { skills, behaviours, disciplines });
|
|
56
|
+
|
|
57
|
+
if (!view) return "";
|
|
58
|
+
|
|
59
|
+
const sections = [];
|
|
60
|
+
|
|
61
|
+
// Skill modifiers - using SkillModifier itemtype with targetCapability
|
|
62
|
+
if (view.skillModifiers.length > 0) {
|
|
63
|
+
const modifierItems = view.skillModifiers.map((m) => {
|
|
64
|
+
const modifierStr = m.modifier > 0 ? `+${m.modifier}` : `${m.modifier}`;
|
|
65
|
+
|
|
66
|
+
if (m.isCapability && m.skills && m.skills.length > 0) {
|
|
67
|
+
// Capability with expanded skills
|
|
68
|
+
const skillLinks = m.skills
|
|
69
|
+
.map(
|
|
70
|
+
(s) => `<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a>`,
|
|
71
|
+
)
|
|
72
|
+
.join(", ");
|
|
73
|
+
return `${openTag("div", { itemtype: "SkillModifier", itemprop: "skillModifiers" })}
|
|
74
|
+
${linkTag("targetCapability", `#${m.id}`)}
|
|
75
|
+
<strong>${escapeHtml(m.name)} Capability</strong> (${openTag("span", { itemprop: "modifierValue" })}${modifierStr}</span>)
|
|
76
|
+
<p>${skillLinks}</p>
|
|
77
|
+
</div>`;
|
|
78
|
+
} else {
|
|
79
|
+
// Individual skill or capability without skills
|
|
80
|
+
return `${openTag("span", { itemtype: "SkillModifier", itemprop: "skillModifiers" })}
|
|
81
|
+
${linkTag("targetCapability", `#${m.id}`)}
|
|
82
|
+
<strong>${escapeHtml(m.name)}</strong>: ${openTag("span", { itemprop: "modifierValue" })}${modifierStr}</span>
|
|
83
|
+
</span>`;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
sections.push(section("Skill Modifiers", modifierItems.join("\n"), 2));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Behaviour modifiers - using BehaviourModifier itemtype
|
|
90
|
+
if (view.behaviourModifiers.length > 0) {
|
|
91
|
+
const modifierItems = view.behaviourModifiers.map((b) => {
|
|
92
|
+
const modifierStr = b.modifier > 0 ? `+${b.modifier}` : `${b.modifier}`;
|
|
93
|
+
return `${openTag("span", { itemtype: "BehaviourModifier", itemprop: "behaviourModifiers" })}
|
|
94
|
+
${linkTag("targetBehaviour", `#${b.id}`)}
|
|
95
|
+
<a href="#${escapeHtml(b.id)}">${escapeHtml(b.name)}</a>: ${openTag("span", { itemprop: "modifierValue" })}${modifierStr}</span>
|
|
96
|
+
</span>`;
|
|
97
|
+
});
|
|
98
|
+
sections.push(section("Behaviour Modifiers", ul(modifierItems), 2));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const body = `<main>
|
|
102
|
+
${openTag("article", { itemtype: "Track", itemid: `#${view.id}` })}
|
|
103
|
+
${prop("h1", "name", view.name)}
|
|
104
|
+
${metaTag("id", view.id)}
|
|
105
|
+
${prop("p", "description", view.description)}
|
|
106
|
+
${sections.join("\n")}
|
|
107
|
+
</article>
|
|
108
|
+
</main>`;
|
|
109
|
+
|
|
110
|
+
return htmlDocument(view.name, body);
|
|
111
|
+
}
|
|
@@ -8,43 +8,12 @@ import { isCapability, getSkillsByCapability } from "../../model/modifiers.js";
|
|
|
8
8
|
import { truncate } from "../shared.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Sort tracks by
|
|
12
|
-
* Within each type, preserves original order.
|
|
11
|
+
* Sort tracks alphabetically by name.
|
|
13
12
|
* @param {Array} tracks - Raw track entities
|
|
14
13
|
* @returns {Array} Sorted tracks array
|
|
15
14
|
*/
|
|
16
|
-
export function
|
|
17
|
-
return [...tracks].sort((a, b) =>
|
|
18
|
-
const aFlags = getTrackTypeFlags(a);
|
|
19
|
-
const bFlags = getTrackTypeFlags(b);
|
|
20
|
-
|
|
21
|
-
// Professional tracks come first
|
|
22
|
-
if (aFlags.isProfessional && !bFlags.isProfessional) return -1;
|
|
23
|
-
if (!aFlags.isProfessional && bFlags.isProfessional) return 1;
|
|
24
|
-
|
|
25
|
-
// Preserve original order within same type
|
|
26
|
-
return 0;
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Determine track type flags from track data.
|
|
32
|
-
*
|
|
33
|
-
* Logic: Only one flag needs to be explicitly set to true; the other defaults to false.
|
|
34
|
-
* - If isManagement: true → management track (isProfessional = false)
|
|
35
|
-
* - If isProfessional: true (or neither set) → professional track (isManagement = false)
|
|
36
|
-
*
|
|
37
|
-
* @param {Object} track
|
|
38
|
-
* @param {boolean} [track.isProfessional] - Whether this is a professional/IC track
|
|
39
|
-
* @param {boolean} [track.isManagement] - Whether this is a management track
|
|
40
|
-
* @returns {{isProfessional: boolean, isManagement: boolean}}
|
|
41
|
-
*/
|
|
42
|
-
export function getTrackTypeFlags(track) {
|
|
43
|
-
// Management takes precedence if explicitly set to true
|
|
44
|
-
const isManagement = track.isManagement === true;
|
|
45
|
-
// Professional is true if management is not true (default behavior)
|
|
46
|
-
const isProfessional = !isManagement && track.isProfessional !== false;
|
|
47
|
-
return { isProfessional, isManagement };
|
|
15
|
+
export function sortTracksByName(tracks) {
|
|
16
|
+
return [...tracks].sort((a, b) => a.name.localeCompare(b.name));
|
|
48
17
|
}
|
|
49
18
|
|
|
50
19
|
/**
|
|
@@ -53,8 +22,6 @@ export function getTrackTypeFlags(track) {
|
|
|
53
22
|
* @property {string} name
|
|
54
23
|
* @property {string} description
|
|
55
24
|
* @property {string} truncatedDescription
|
|
56
|
-
* @property {boolean} isProfessional
|
|
57
|
-
* @property {boolean} isManagement
|
|
58
25
|
*/
|
|
59
26
|
|
|
60
27
|
/**
|
|
@@ -64,16 +31,13 @@ export function getTrackTypeFlags(track) {
|
|
|
64
31
|
* @returns {{ items: TrackListItem[] }}
|
|
65
32
|
*/
|
|
66
33
|
export function prepareTracksList(tracks, descriptionLimit = 120) {
|
|
67
|
-
const sortedTracks =
|
|
34
|
+
const sortedTracks = sortTracksByName(tracks);
|
|
68
35
|
const items = sortedTracks.map((track) => {
|
|
69
|
-
const { isProfessional, isManagement } = getTrackTypeFlags(track);
|
|
70
36
|
return {
|
|
71
37
|
id: track.id,
|
|
72
38
|
name: track.name,
|
|
73
39
|
description: track.description,
|
|
74
40
|
truncatedDescription: truncate(track.description, descriptionLimit),
|
|
75
|
-
isProfessional,
|
|
76
|
-
isManagement,
|
|
77
41
|
};
|
|
78
42
|
});
|
|
79
43
|
|
|
@@ -101,11 +65,8 @@ export function prepareTracksList(tracks, descriptionLimit = 120) {
|
|
|
101
65
|
* @property {string} id
|
|
102
66
|
* @property {string} name
|
|
103
67
|
* @property {string} description
|
|
104
|
-
* @property {boolean} isProfessional
|
|
105
|
-
* @property {boolean} isManagement
|
|
106
68
|
* @property {SkillModifierRow[]} skillModifiers
|
|
107
69
|
* @property {BehaviourModifierRow[]} behaviourModifiers
|
|
108
|
-
* @property {Array<{id: string, name: string}>} validDisciplines
|
|
109
70
|
*/
|
|
110
71
|
|
|
111
72
|
/**
|
|
@@ -114,14 +75,12 @@ export function prepareTracksList(tracks, descriptionLimit = 120) {
|
|
|
114
75
|
* @param {Object} context - Additional context
|
|
115
76
|
* @param {Array} context.skills - All skills
|
|
116
77
|
* @param {Array} context.behaviours - All behaviours
|
|
117
|
-
* @param {Array} context.disciplines - All disciplines
|
|
78
|
+
* @param {Array} context.disciplines - All disciplines (unused but kept for API compatibility)
|
|
118
79
|
* @returns {TrackDetailView|null}
|
|
119
80
|
*/
|
|
120
|
-
export function prepareTrackDetail(track, { skills, behaviours
|
|
81
|
+
export function prepareTrackDetail(track, { skills, behaviours }) {
|
|
121
82
|
if (!track) return null;
|
|
122
83
|
|
|
123
|
-
const { isProfessional, isManagement } = getTrackTypeFlags(track);
|
|
124
|
-
|
|
125
84
|
// Build skill modifiers
|
|
126
85
|
const skillModifiers = track.skillModifiers
|
|
127
86
|
? Object.entries(track.skillModifiers).map(([key, modifier]) => {
|
|
@@ -160,22 +119,11 @@ export function prepareTrackDetail(track, { skills, behaviours, disciplines }) {
|
|
|
160
119
|
)
|
|
161
120
|
: [];
|
|
162
121
|
|
|
163
|
-
// Get valid disciplines
|
|
164
|
-
const validDisciplines = track.validDisciplines
|
|
165
|
-
? track.validDisciplines
|
|
166
|
-
.map((id) => disciplines.find((d) => d.id === id))
|
|
167
|
-
.filter(Boolean)
|
|
168
|
-
.map((d) => ({ id: d.id, name: d.specialization || d.name }))
|
|
169
|
-
: [];
|
|
170
|
-
|
|
171
122
|
return {
|
|
172
123
|
id: track.id,
|
|
173
124
|
name: track.name,
|
|
174
125
|
description: track.description,
|
|
175
|
-
isProfessional,
|
|
176
|
-
isManagement,
|
|
177
126
|
skillModifiers,
|
|
178
127
|
behaviourModifiers,
|
|
179
|
-
validDisciplines,
|
|
180
128
|
};
|
|
181
129
|
}
|
package/app/handout-main.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
gradeToDOM,
|
|
37
37
|
trackToDOM,
|
|
38
38
|
} from "./formatters/index.js";
|
|
39
|
-
import {
|
|
39
|
+
import { sortTracksByName } from "./formatters/track/shared.js";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Create a chapter cover page
|
|
@@ -271,6 +271,17 @@ function renderBehaviourHandout(data) {
|
|
|
271
271
|
renderHandout(content);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Sort disciplines by type (professional first, then management)
|
|
276
|
+
* @param {Array} disciplines - Raw discipline entities
|
|
277
|
+
* @returns {Array} - Sorted disciplines
|
|
278
|
+
*/
|
|
279
|
+
function sortDisciplinesByType(disciplines) {
|
|
280
|
+
const professional = disciplines.filter((d) => !d.isManagement);
|
|
281
|
+
const management = disciplines.filter((d) => d.isManagement);
|
|
282
|
+
return [...professional, ...management];
|
|
283
|
+
}
|
|
284
|
+
|
|
274
285
|
/**
|
|
275
286
|
* Render all job component slides (disciplines, grades, tracks)
|
|
276
287
|
* @param {Object} data
|
|
@@ -278,7 +289,10 @@ function renderBehaviourHandout(data) {
|
|
|
278
289
|
function renderJobHandout(data) {
|
|
279
290
|
const { framework } = data;
|
|
280
291
|
|
|
281
|
-
|
|
292
|
+
// Sort disciplines by type: professional first, then management
|
|
293
|
+
const sortedDisciplines = sortDisciplinesByType(data.disciplines);
|
|
294
|
+
|
|
295
|
+
const disciplineSlides = sortedDisciplines.map((discipline) => {
|
|
282
296
|
return disciplineToDOM(discipline, {
|
|
283
297
|
skills: data.skills,
|
|
284
298
|
behaviours: data.behaviours,
|
|
@@ -295,7 +309,7 @@ function renderJobHandout(data) {
|
|
|
295
309
|
});
|
|
296
310
|
});
|
|
297
311
|
|
|
298
|
-
const trackSlides =
|
|
312
|
+
const trackSlides = sortTracksByName(data.tracks).map((track) => {
|
|
299
313
|
return trackToDOM(track, {
|
|
300
314
|
skills: data.skills,
|
|
301
315
|
behaviours: data.behaviours,
|
|
@@ -314,21 +328,21 @@ function renderJobHandout(data) {
|
|
|
314
328
|
}),
|
|
315
329
|
...disciplineSlides,
|
|
316
330
|
|
|
317
|
-
//
|
|
318
|
-
createChapterCover({
|
|
319
|
-
emoji: getConceptEmoji(framework, "track"),
|
|
320
|
-
title: framework.entityDefinitions.track.title,
|
|
321
|
-
description: framework.entityDefinitions.track.description,
|
|
322
|
-
}),
|
|
323
|
-
...trackSlides,
|
|
324
|
-
|
|
325
|
-
// Grades chapter
|
|
331
|
+
// Grades chapter (moved before Tracks)
|
|
326
332
|
createChapterCover({
|
|
327
333
|
emoji: getConceptEmoji(framework, "grade"),
|
|
328
334
|
title: framework.entityDefinitions.grade.title,
|
|
329
335
|
description: framework.entityDefinitions.grade.description,
|
|
330
336
|
}),
|
|
331
337
|
...gradeSlides,
|
|
338
|
+
|
|
339
|
+
// Tracks chapter (moved after Grades)
|
|
340
|
+
createChapterCover({
|
|
341
|
+
emoji: getConceptEmoji(framework, "track"),
|
|
342
|
+
title: framework.entityDefinitions.track.title,
|
|
343
|
+
description: framework.entityDefinitions.track.description,
|
|
344
|
+
}),
|
|
345
|
+
...trackSlides,
|
|
332
346
|
);
|
|
333
347
|
|
|
334
348
|
renderHandout(content);
|
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
|