@forwardimpact/pathway 0.1.0 → 0.2.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 +109 -21
- package/app/commands/command-factory.js +3 -3
- package/app/commands/interview.js +14 -7
- package/app/commands/job.js +43 -29
- 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 +111 -27
- package/app/css/components/surfaces.css +16 -0
- package/app/formatters/agent/profile.js +113 -87
- package/app/formatters/agent/skill.js +64 -31
- 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 +3 -0
- 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 +5 -3
- 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/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/index.html +11 -0
- package/app/lib/card-mappers.js +17 -12
- package/app/lib/job-cache.js +12 -9
- package/app/lib/template-loader.js +66 -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 +119 -25
- 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 +15 -4
- package/app/pages/landing.js +10 -10
- package/app/pages/progress-builder.js +3 -1
- package/app/pages/progress.js +72 -21
- 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/bin/pathway.js +18 -64
- 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 +1 -3
- 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/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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill formatting for microdata HTML output
|
|
3
|
+
*
|
|
4
|
+
* Generates clean, class-less HTML with microdata aligned with capability.schema.json
|
|
5
|
+
* RDF vocab: https://schema.forwardimpact.team/rdf/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
openTag,
|
|
10
|
+
prop,
|
|
11
|
+
propRaw,
|
|
12
|
+
metaTag,
|
|
13
|
+
section,
|
|
14
|
+
dl,
|
|
15
|
+
ul,
|
|
16
|
+
escapeHtml,
|
|
17
|
+
formatLevelName,
|
|
18
|
+
htmlDocument,
|
|
19
|
+
} from "../microdata-shared.js";
|
|
20
|
+
import { prepareSkillsList, prepareSkillDetail } from "./shared.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format skill list as microdata HTML
|
|
24
|
+
* @param {Array} skills - Raw skill entities
|
|
25
|
+
* @param {Array} capabilities - Capability entities
|
|
26
|
+
* @returns {string} HTML with microdata
|
|
27
|
+
*/
|
|
28
|
+
export function skillListToMicrodata(skills, capabilities) {
|
|
29
|
+
const { groups, groupOrder } = prepareSkillsList(skills, capabilities);
|
|
30
|
+
|
|
31
|
+
const content = groupOrder
|
|
32
|
+
.map((capability) => {
|
|
33
|
+
const capabilitySkills = groups[capability];
|
|
34
|
+
const skillItems = capabilitySkills
|
|
35
|
+
.map(
|
|
36
|
+
(
|
|
37
|
+
skill,
|
|
38
|
+
) => `${openTag("article", { itemtype: "Skill", itemid: `#${skill.id}` })}
|
|
39
|
+
${prop("h3", "name", skill.name)}
|
|
40
|
+
${prop("p", "description", skill.truncatedDescription)}
|
|
41
|
+
${metaTag("capability", capability)}
|
|
42
|
+
</article>`,
|
|
43
|
+
)
|
|
44
|
+
.join("\n");
|
|
45
|
+
|
|
46
|
+
return section(formatLevelName(capability), skillItems, 2);
|
|
47
|
+
})
|
|
48
|
+
.join("\n");
|
|
49
|
+
|
|
50
|
+
return htmlDocument(
|
|
51
|
+
"Skills",
|
|
52
|
+
`<main>
|
|
53
|
+
<h1>Skills</h1>
|
|
54
|
+
${content}
|
|
55
|
+
</main>`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Format skill detail as microdata HTML
|
|
61
|
+
* @param {Object} skill - Raw skill entity
|
|
62
|
+
* @param {Object} context - Additional context
|
|
63
|
+
* @param {Array} context.disciplines - All disciplines
|
|
64
|
+
* @param {Array} context.tracks - All tracks
|
|
65
|
+
* @param {Array} context.drivers - All drivers
|
|
66
|
+
* @param {Array} context.capabilities - Capability entities
|
|
67
|
+
* @returns {string} HTML with microdata
|
|
68
|
+
*/
|
|
69
|
+
export function skillToMicrodata(
|
|
70
|
+
skill,
|
|
71
|
+
{ disciplines, tracks, drivers, capabilities },
|
|
72
|
+
) {
|
|
73
|
+
const view = prepareSkillDetail(skill, {
|
|
74
|
+
disciplines,
|
|
75
|
+
tracks,
|
|
76
|
+
drivers,
|
|
77
|
+
capabilities,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!view) return "";
|
|
81
|
+
|
|
82
|
+
const sections = [];
|
|
83
|
+
|
|
84
|
+
// Human-only badge
|
|
85
|
+
if (view.isHumanOnly) {
|
|
86
|
+
sections.push(`<p><strong>Human-Only</strong> — Requires interpersonal skills; excluded from agents</p>
|
|
87
|
+
${metaTag("isHumanOnly", "true")}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Level descriptions - uses LevelDescriptions itemtype
|
|
91
|
+
const levelPairs = Object.entries(view.levelDescriptions).map(
|
|
92
|
+
([level, desc]) => ({
|
|
93
|
+
term: formatLevelName(level),
|
|
94
|
+
definition: desc,
|
|
95
|
+
itemprop: `${level}Description`,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
sections.push(
|
|
99
|
+
section(
|
|
100
|
+
"Level Descriptions",
|
|
101
|
+
`${openTag("div", { itemtype: "LevelDescriptions", itemprop: "levelDescriptions" })}
|
|
102
|
+
${dl(levelPairs)}
|
|
103
|
+
</div>`,
|
|
104
|
+
2,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Related disciplines
|
|
109
|
+
if (view.relatedDisciplines.length > 0) {
|
|
110
|
+
const disciplineItems = view.relatedDisciplines.map(
|
|
111
|
+
(d) =>
|
|
112
|
+
`<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a> (${escapeHtml(d.skillType)})`,
|
|
113
|
+
);
|
|
114
|
+
sections.push(section("Used in Disciplines", ul(disciplineItems), 2));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Related tracks with modifiers
|
|
118
|
+
if (view.relatedTracks.length > 0) {
|
|
119
|
+
const trackItems = view.relatedTracks.map((t) => {
|
|
120
|
+
const modifierStr = t.modifier > 0 ? `+${t.modifier}` : `${t.modifier}`;
|
|
121
|
+
return `<a href="#${escapeHtml(t.id)}">${escapeHtml(t.name)}</a>: ${modifierStr}`;
|
|
122
|
+
});
|
|
123
|
+
sections.push(section("Modified by Tracks", ul(trackItems), 2));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Related drivers
|
|
127
|
+
if (view.relatedDrivers.length > 0) {
|
|
128
|
+
const driverItems = view.relatedDrivers.map(
|
|
129
|
+
(d) => `<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a>`,
|
|
130
|
+
);
|
|
131
|
+
sections.push(section("Linked to Drivers", ul(driverItems), 2));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const body = `<main>
|
|
135
|
+
${openTag("article", { itemtype: "Skill", itemid: `#${view.id}` })}
|
|
136
|
+
${prop("h1", "name", view.name)}
|
|
137
|
+
${metaTag("id", view.id)}
|
|
138
|
+
${metaTag("capability", view.capability)}
|
|
139
|
+
${propRaw(
|
|
140
|
+
"div",
|
|
141
|
+
"human",
|
|
142
|
+
`${openTag("div", { itemtype: "SkillHumanSection" })}
|
|
143
|
+
${prop("p", "description", view.description)}
|
|
144
|
+
${sections.join("\n")}
|
|
145
|
+
</div>`,
|
|
146
|
+
)}
|
|
147
|
+
</article>
|
|
148
|
+
</main>`;
|
|
149
|
+
|
|
150
|
+
return htmlDocument(view.name, body);
|
|
151
|
+
}
|
|
@@ -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
|
+
}
|