@forwardimpact/pathway 0.1.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/LICENSE +201 -0
- package/README.md +104 -0
- package/app/commands/agent.js +430 -0
- package/app/commands/behaviour.js +61 -0
- package/app/commands/command-factory.js +211 -0
- package/app/commands/discipline.js +58 -0
- package/app/commands/driver.js +94 -0
- package/app/commands/grade.js +60 -0
- package/app/commands/index.js +20 -0
- package/app/commands/init.js +67 -0
- package/app/commands/interview.js +68 -0
- package/app/commands/job.js +157 -0
- package/app/commands/progress.js +77 -0
- package/app/commands/questions.js +179 -0
- package/app/commands/serve.js +143 -0
- package/app/commands/site.js +121 -0
- package/app/commands/skill.js +76 -0
- package/app/commands/stage.js +129 -0
- package/app/commands/track.js +70 -0
- package/app/components/action-buttons.js +66 -0
- package/app/components/behaviour-profile.js +53 -0
- package/app/components/builder.js +341 -0
- package/app/components/card.js +98 -0
- package/app/components/checklist.js +145 -0
- package/app/components/comparison-radar.js +237 -0
- package/app/components/detail.js +230 -0
- package/app/components/error-page.js +72 -0
- package/app/components/grid.js +109 -0
- package/app/components/list.js +120 -0
- package/app/components/modifier-table.js +142 -0
- package/app/components/nav.js +64 -0
- package/app/components/progression-table.js +320 -0
- package/app/components/radar-chart.js +102 -0
- package/app/components/skill-matrix.js +97 -0
- package/app/css/base.css +56 -0
- package/app/css/bundles/app.css +40 -0
- package/app/css/bundles/handout.css +43 -0
- package/app/css/bundles/slides.css +40 -0
- package/app/css/components/badges.css +215 -0
- package/app/css/components/buttons.css +101 -0
- package/app/css/components/forms.css +105 -0
- package/app/css/components/layout.css +209 -0
- package/app/css/components/nav.css +166 -0
- package/app/css/components/progress.css +166 -0
- package/app/css/components/states.css +82 -0
- package/app/css/components/surfaces.css +243 -0
- package/app/css/components/tables.css +362 -0
- package/app/css/components/typography.css +122 -0
- package/app/css/components/utilities.css +41 -0
- package/app/css/pages/agent-builder.css +391 -0
- package/app/css/pages/assessment-results.css +453 -0
- package/app/css/pages/detail.css +59 -0
- package/app/css/pages/interview-builder.css +148 -0
- package/app/css/pages/job-builder.css +134 -0
- package/app/css/pages/landing.css +92 -0
- package/app/css/pages/lifecycle.css +118 -0
- package/app/css/pages/progress-builder.css +274 -0
- package/app/css/pages/self-assessment.css +502 -0
- package/app/css/reset.css +50 -0
- package/app/css/tokens.css +153 -0
- package/app/css/views/handout.css +30 -0
- package/app/css/views/print.css +608 -0
- package/app/css/views/slide-animations.css +113 -0
- package/app/css/views/slide-base.css +330 -0
- package/app/css/views/slide-sections.css +597 -0
- package/app/css/views/slide-tables.css +275 -0
- package/app/formatters/agent/dom.js +540 -0
- package/app/formatters/agent/profile.js +133 -0
- package/app/formatters/agent/skill.js +58 -0
- package/app/formatters/behaviour/dom.js +91 -0
- package/app/formatters/behaviour/markdown.js +54 -0
- package/app/formatters/behaviour/shared.js +64 -0
- package/app/formatters/discipline/dom.js +187 -0
- package/app/formatters/discipline/markdown.js +87 -0
- package/app/formatters/discipline/shared.js +131 -0
- package/app/formatters/driver/dom.js +103 -0
- package/app/formatters/driver/shared.js +92 -0
- package/app/formatters/grade/dom.js +208 -0
- package/app/formatters/grade/markdown.js +94 -0
- package/app/formatters/grade/shared.js +86 -0
- package/app/formatters/index.js +50 -0
- package/app/formatters/interview/dom.js +97 -0
- package/app/formatters/interview/markdown.js +66 -0
- package/app/formatters/interview/shared.js +332 -0
- package/app/formatters/job/description.js +176 -0
- package/app/formatters/job/dom.js +411 -0
- package/app/formatters/job/markdown.js +102 -0
- package/app/formatters/progress/dom.js +135 -0
- package/app/formatters/progress/markdown.js +86 -0
- package/app/formatters/progress/shared.js +339 -0
- package/app/formatters/questions/json.js +43 -0
- package/app/formatters/questions/markdown.js +303 -0
- package/app/formatters/questions/shared.js +274 -0
- package/app/formatters/questions/yaml.js +76 -0
- package/app/formatters/shared.js +71 -0
- package/app/formatters/skill/dom.js +168 -0
- package/app/formatters/skill/markdown.js +109 -0
- package/app/formatters/skill/shared.js +125 -0
- package/app/formatters/stage/dom.js +135 -0
- package/app/formatters/stage/index.js +12 -0
- package/app/formatters/stage/shared.js +111 -0
- package/app/formatters/track/dom.js +128 -0
- package/app/formatters/track/markdown.js +105 -0
- package/app/formatters/track/shared.js +181 -0
- package/app/handout-main.js +421 -0
- package/app/handout.html +21 -0
- package/app/index.html +59 -0
- package/app/lib/card-mappers.js +173 -0
- package/app/lib/cli-output.js +270 -0
- package/app/lib/error-boundary.js +70 -0
- package/app/lib/errors.js +49 -0
- package/app/lib/form-controls.js +47 -0
- package/app/lib/job-cache.js +86 -0
- package/app/lib/markdown.js +114 -0
- package/app/lib/radar.js +866 -0
- package/app/lib/reactive.js +77 -0
- package/app/lib/render.js +212 -0
- package/app/lib/router-core.js +160 -0
- package/app/lib/router-pages.js +16 -0
- package/app/lib/router-slides.js +202 -0
- package/app/lib/state.js +148 -0
- package/app/lib/utils.js +14 -0
- package/app/lib/yaml-loader.js +327 -0
- package/app/main.js +213 -0
- package/app/model/agent.js +702 -0
- package/app/model/checklist.js +137 -0
- package/app/model/derivation.js +699 -0
- package/app/model/index-generator.js +71 -0
- package/app/model/interview.js +539 -0
- package/app/model/job.js +222 -0
- package/app/model/levels.js +591 -0
- package/app/model/loader.js +564 -0
- package/app/model/matching.js +858 -0
- package/app/model/modifiers.js +158 -0
- package/app/model/profile.js +266 -0
- package/app/model/progression.js +507 -0
- package/app/model/validation.js +1385 -0
- package/app/pages/agent-builder.js +823 -0
- package/app/pages/assessment-results.js +507 -0
- package/app/pages/behaviour.js +70 -0
- package/app/pages/discipline.js +71 -0
- package/app/pages/driver.js +106 -0
- package/app/pages/grade.js +117 -0
- package/app/pages/interview-builder.js +50 -0
- package/app/pages/interview.js +304 -0
- package/app/pages/job-builder.js +50 -0
- package/app/pages/job.js +58 -0
- package/app/pages/landing.js +305 -0
- package/app/pages/progress-builder.js +58 -0
- package/app/pages/progress.js +495 -0
- package/app/pages/self-assessment.js +729 -0
- package/app/pages/skill.js +113 -0
- package/app/pages/stage.js +231 -0
- package/app/pages/track.js +69 -0
- package/app/slide-main.js +360 -0
- package/app/slides/behaviour.js +38 -0
- package/app/slides/chapter.js +82 -0
- package/app/slides/discipline.js +40 -0
- package/app/slides/driver.js +39 -0
- package/app/slides/grade.js +32 -0
- package/app/slides/index.js +198 -0
- package/app/slides/interview.js +58 -0
- package/app/slides/job.js +55 -0
- package/app/slides/overview.js +126 -0
- package/app/slides/progress.js +83 -0
- package/app/slides/skill.js +40 -0
- package/app/slides/track.js +39 -0
- package/app/slides.html +56 -0
- package/app/types.js +147 -0
- package/bin/pathway.js +489 -0
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
- package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
- package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
- package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
- package/examples/agents/.vscode/settings.json +8 -0
- package/examples/behaviours/_index.yaml +8 -0
- package/examples/behaviours/outcome_ownership.yaml +44 -0
- package/examples/behaviours/polymathic_knowledge.yaml +42 -0
- package/examples/behaviours/precise_communication.yaml +40 -0
- package/examples/behaviours/relentless_curiosity.yaml +38 -0
- package/examples/behaviours/systems_thinking.yaml +41 -0
- package/examples/capabilities/_index.yaml +8 -0
- package/examples/capabilities/business.yaml +251 -0
- package/examples/capabilities/delivery.yaml +352 -0
- package/examples/capabilities/people.yaml +100 -0
- package/examples/capabilities/reliability.yaml +318 -0
- package/examples/capabilities/scale.yaml +394 -0
- package/examples/disciplines/_index.yaml +5 -0
- package/examples/disciplines/data_engineering.yaml +76 -0
- package/examples/disciplines/software_engineering.yaml +76 -0
- package/examples/drivers.yaml +205 -0
- package/examples/framework.yaml +58 -0
- package/examples/grades.yaml +118 -0
- package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
- package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
- package/examples/questions/behaviours/precise_communication.yaml +55 -0
- package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
- package/examples/questions/behaviours/systems_thinking.yaml +53 -0
- package/examples/questions/skills/architecture_design.yaml +54 -0
- package/examples/questions/skills/cloud_platforms.yaml +48 -0
- package/examples/questions/skills/code_quality.yaml +49 -0
- package/examples/questions/skills/data_modeling.yaml +46 -0
- package/examples/questions/skills/devops.yaml +47 -0
- package/examples/questions/skills/full_stack_development.yaml +48 -0
- package/examples/questions/skills/sre_practices.yaml +44 -0
- package/examples/questions/skills/stakeholder_management.yaml +49 -0
- package/examples/questions/skills/team_collaboration.yaml +43 -0
- package/examples/questions/skills/technical_writing.yaml +43 -0
- package/examples/self-assessments.yaml +66 -0
- package/examples/stages.yaml +76 -0
- package/examples/tracks/_index.yaml +6 -0
- package/examples/tracks/manager.yaml +53 -0
- package/examples/tracks/platform.yaml +54 -0
- package/examples/tracks/sre.yaml +58 -0
- package/examples/vscode-settings.yaml +22 -0
- package/package.json +68 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting utilities
|
|
3
|
+
*
|
|
4
|
+
* Common formatting functions used across different output formats (CLI, DOM, markdown)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format level as text with dots (for CLI/markdown)
|
|
9
|
+
* @param {number} level - 1-5
|
|
10
|
+
* @param {string} name - Level name
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function formatLevelText(level, name) {
|
|
14
|
+
return `${"●".repeat(level)}${"○".repeat(5 - level)} ${name}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Format table as markdown
|
|
19
|
+
* @param {string[]} headers - Column headers
|
|
20
|
+
* @param {string[][]} rows - Table rows
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function tableToMarkdown(headers, rows) {
|
|
24
|
+
const headerRow = `| ${headers.join(" | ")} |`;
|
|
25
|
+
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
26
|
+
const dataRows = rows.map((row) => `| ${row.join(" | ")} |`);
|
|
27
|
+
return [headerRow, separator, ...dataRows].join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format a key-value object as markdown list
|
|
32
|
+
* @param {Object<string, string>} obj - Key-value pairs
|
|
33
|
+
* @param {number} indent - Indent level
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function objectToMarkdownList(obj, indent = 0) {
|
|
37
|
+
const prefix = " ".repeat(indent);
|
|
38
|
+
return Object.entries(obj)
|
|
39
|
+
.map(([key, value]) => `${prefix}- **${key}**: ${value}`)
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Format percentage for display
|
|
45
|
+
* @param {number} value - Decimal value (0-1)
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function formatPercent(value) {
|
|
49
|
+
return `${Math.round(value * 100)}%`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Capitalize first letter of each word
|
|
54
|
+
* @param {string} str
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function capitalize(str) {
|
|
58
|
+
if (!str) return "";
|
|
59
|
+
return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Truncate text to max length (reserves space for ellipsis)
|
|
64
|
+
* @param {string} text
|
|
65
|
+
* @param {number} maxLength - Total length including ellipsis
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function truncate(text, maxLength = 100) {
|
|
69
|
+
if (!text || text.length <= maxLength) return text || "";
|
|
70
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
71
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill formatting for DOM output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
div,
|
|
7
|
+
heading1,
|
|
8
|
+
heading2,
|
|
9
|
+
p,
|
|
10
|
+
span,
|
|
11
|
+
a,
|
|
12
|
+
table,
|
|
13
|
+
tbody,
|
|
14
|
+
thead,
|
|
15
|
+
tr,
|
|
16
|
+
th,
|
|
17
|
+
td,
|
|
18
|
+
} from "../../lib/render.js";
|
|
19
|
+
import { createBackLink } from "../../components/nav.js";
|
|
20
|
+
import { createLevelCell } from "../../components/detail.js";
|
|
21
|
+
import { SKILL_LEVEL_ORDER } from "../../model/levels.js";
|
|
22
|
+
import { prepareSkillDetail, formatCapability } from "./shared.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Format skill detail as DOM elements
|
|
26
|
+
* @param {Object} skill - Raw skill entity
|
|
27
|
+
* @param {Object} context - Additional context and options
|
|
28
|
+
* @param {Array} context.disciplines - All disciplines
|
|
29
|
+
* @param {Array} context.tracks - All tracks
|
|
30
|
+
* @param {Array} context.drivers - All drivers
|
|
31
|
+
* @param {Array} context.capabilities - Capability entities
|
|
32
|
+
* @param {boolean} [context.showBackLink=true] - Whether to show back navigation link
|
|
33
|
+
* @returns {HTMLElement}
|
|
34
|
+
*/
|
|
35
|
+
export function skillToDOM(
|
|
36
|
+
skill,
|
|
37
|
+
{ disciplines, tracks, drivers, capabilities, showBackLink = true } = {},
|
|
38
|
+
) {
|
|
39
|
+
const view = prepareSkillDetail(skill, {
|
|
40
|
+
disciplines,
|
|
41
|
+
tracks,
|
|
42
|
+
drivers,
|
|
43
|
+
capabilities,
|
|
44
|
+
});
|
|
45
|
+
return div(
|
|
46
|
+
{ className: "detail-page skill-detail" },
|
|
47
|
+
// Header
|
|
48
|
+
div(
|
|
49
|
+
{ className: "page-header" },
|
|
50
|
+
showBackLink ? createBackLink("/skill", "← Back to Skills") : null,
|
|
51
|
+
heading1(
|
|
52
|
+
{ className: "page-title" },
|
|
53
|
+
view.capabilityEmoji,
|
|
54
|
+
" ",
|
|
55
|
+
view.name,
|
|
56
|
+
),
|
|
57
|
+
div(
|
|
58
|
+
{ className: "page-meta" },
|
|
59
|
+
span(
|
|
60
|
+
{ className: "badge badge-default" },
|
|
61
|
+
formatCapability(view.capability),
|
|
62
|
+
),
|
|
63
|
+
view.isHumanOnly
|
|
64
|
+
? span(
|
|
65
|
+
{
|
|
66
|
+
className: "badge badge-human-only",
|
|
67
|
+
title: "Requires interpersonal skills; excluded from agents",
|
|
68
|
+
},
|
|
69
|
+
"🤲 Human-Only",
|
|
70
|
+
)
|
|
71
|
+
: null,
|
|
72
|
+
),
|
|
73
|
+
p({ className: "page-description" }, view.description),
|
|
74
|
+
),
|
|
75
|
+
|
|
76
|
+
// Level descriptions
|
|
77
|
+
div(
|
|
78
|
+
{ className: "detail-section" },
|
|
79
|
+
heading2({ className: "section-title" }, "Level Descriptions"),
|
|
80
|
+
table(
|
|
81
|
+
{ className: "level-table" },
|
|
82
|
+
thead({}, tr({}, th({}, "Level"), th({}, "Description"))),
|
|
83
|
+
tbody(
|
|
84
|
+
{},
|
|
85
|
+
...SKILL_LEVEL_ORDER.map((level, index) => {
|
|
86
|
+
const description = view.levelDescriptions[level] || "—";
|
|
87
|
+
return tr(
|
|
88
|
+
{},
|
|
89
|
+
createLevelCell(index + 1, SKILL_LEVEL_ORDER.length, level),
|
|
90
|
+
td({}, description),
|
|
91
|
+
);
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
|
|
97
|
+
// Used in Disciplines and Linked to Drivers in two columns
|
|
98
|
+
view.relatedDisciplines.length > 0 || view.relatedDrivers.length > 0
|
|
99
|
+
? div(
|
|
100
|
+
{ className: "detail-section" },
|
|
101
|
+
div(
|
|
102
|
+
{ className: "content-columns" },
|
|
103
|
+
// Used in Disciplines column
|
|
104
|
+
view.relatedDisciplines.length > 0
|
|
105
|
+
? div(
|
|
106
|
+
{ className: "column" },
|
|
107
|
+
heading2(
|
|
108
|
+
{ className: "section-title" },
|
|
109
|
+
"Used in Disciplines",
|
|
110
|
+
),
|
|
111
|
+
...view.relatedDisciplines.map((d) =>
|
|
112
|
+
div(
|
|
113
|
+
{ className: "list-item" },
|
|
114
|
+
showBackLink
|
|
115
|
+
? a({ href: `#/discipline/${d.id}` }, d.name)
|
|
116
|
+
: span({}, d.name),
|
|
117
|
+
" ",
|
|
118
|
+
span(
|
|
119
|
+
{ className: `badge badge-${d.skillType}` },
|
|
120
|
+
d.skillType,
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
: null,
|
|
126
|
+
// Linked to Drivers column
|
|
127
|
+
view.relatedDrivers.length > 0
|
|
128
|
+
? div(
|
|
129
|
+
{ className: "column" },
|
|
130
|
+
heading2({ className: "section-title" }, "Linked to Drivers"),
|
|
131
|
+
...view.relatedDrivers.map((d) =>
|
|
132
|
+
div(
|
|
133
|
+
{ className: "list-item" },
|
|
134
|
+
showBackLink
|
|
135
|
+
? a({ href: `#/driver/${d.id}` }, d.name)
|
|
136
|
+
: span({}, d.name),
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
: null,
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
: null,
|
|
144
|
+
|
|
145
|
+
// Related tracks
|
|
146
|
+
view.relatedTracks.length > 0
|
|
147
|
+
? div(
|
|
148
|
+
{ className: "detail-section" },
|
|
149
|
+
heading2({ className: "section-title" }, "Modified by Tracks"),
|
|
150
|
+
...view.relatedTracks.map((t) =>
|
|
151
|
+
div(
|
|
152
|
+
{ className: "list-item" },
|
|
153
|
+
showBackLink
|
|
154
|
+
? a({ href: `#/track/${t.id}` }, t.name)
|
|
155
|
+
: span({}, t.name),
|
|
156
|
+
" ",
|
|
157
|
+
span(
|
|
158
|
+
{
|
|
159
|
+
className: `modifier ${t.modifier > 0 ? "modifier-positive" : t.modifier < 0 ? "modifier-negative" : "modifier-neutral"}`,
|
|
160
|
+
},
|
|
161
|
+
t.modifier > 0 ? `+${t.modifier}` : String(t.modifier),
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
: null,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill formatting for markdown/CLI output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { tableToMarkdown, capitalize } from "../shared.js";
|
|
6
|
+
import { prepareSkillsList, prepareSkillDetail } from "./shared.js";
|
|
7
|
+
import { getConceptEmoji } from "../../model/levels.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format skill list as markdown
|
|
11
|
+
* @param {Array} skills - Raw skill entities
|
|
12
|
+
* @param {Array} capabilities - Capability entities
|
|
13
|
+
* @param {Object} [framework] - Framework config for emojis
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function skillListToMarkdown(skills, capabilities, framework) {
|
|
17
|
+
const { groups, groupOrder } = prepareSkillsList(skills, capabilities);
|
|
18
|
+
const emoji = framework ? getConceptEmoji(framework, "skill") : "📚";
|
|
19
|
+
const lines = [`# ${emoji} Skills`, ""];
|
|
20
|
+
|
|
21
|
+
for (const capability of groupOrder) {
|
|
22
|
+
const capabilitySkills = groups[capability];
|
|
23
|
+
lines.push(`## ${capitalize(capability)}`, "");
|
|
24
|
+
|
|
25
|
+
for (const skill of capabilitySkills) {
|
|
26
|
+
lines.push(`- **${skill.name}**: ${skill.truncatedDescription}`);
|
|
27
|
+
}
|
|
28
|
+
lines.push("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format skill detail as markdown
|
|
36
|
+
* @param {Object} skill - Raw skill entity
|
|
37
|
+
* @param {Object} context - Additional context
|
|
38
|
+
* @param {Array} context.disciplines - All disciplines
|
|
39
|
+
* @param {Array} context.tracks - All tracks
|
|
40
|
+
* @param {Array} context.drivers - All drivers
|
|
41
|
+
* @param {Array} context.capabilities - Capability entities
|
|
42
|
+
* @param {Object} [context.framework] - Framework config for emojis
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function skillToMarkdown(
|
|
46
|
+
skill,
|
|
47
|
+
{ disciplines, tracks, drivers, capabilities, framework },
|
|
48
|
+
) {
|
|
49
|
+
const view = prepareSkillDetail(skill, {
|
|
50
|
+
disciplines,
|
|
51
|
+
tracks,
|
|
52
|
+
drivers,
|
|
53
|
+
capabilities,
|
|
54
|
+
});
|
|
55
|
+
const emoji = framework ? getConceptEmoji(framework, "skill") : "🎯";
|
|
56
|
+
const lines = [
|
|
57
|
+
`# ${emoji} ${view.name}`,
|
|
58
|
+
"",
|
|
59
|
+
`${view.capabilityEmoji} ${capitalize(view.capability)}`,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Add human-only badge if applicable
|
|
63
|
+
if (view.isHumanOnly) {
|
|
64
|
+
lines.push(
|
|
65
|
+
"",
|
|
66
|
+
"🤲 **Human-Only** — Requires interpersonal skills; excluded from agents",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lines.push("", view.description, "");
|
|
71
|
+
|
|
72
|
+
// Level descriptions table
|
|
73
|
+
lines.push("## Level Descriptions", "");
|
|
74
|
+
const levelRows = Object.entries(view.levelDescriptions).map(
|
|
75
|
+
([level, desc]) => [capitalize(level), desc],
|
|
76
|
+
);
|
|
77
|
+
lines.push(tableToMarkdown(["Level", "Description"], levelRows));
|
|
78
|
+
lines.push("");
|
|
79
|
+
|
|
80
|
+
// Related disciplines
|
|
81
|
+
if (view.relatedDisciplines.length > 0) {
|
|
82
|
+
lines.push("## Used in Disciplines", "");
|
|
83
|
+
for (const d of view.relatedDisciplines) {
|
|
84
|
+
lines.push(`- **${d.name}**: as ${d.skillType}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Related tracks with modifiers
|
|
90
|
+
if (view.relatedTracks.length > 0) {
|
|
91
|
+
lines.push("## Modified by Tracks", "");
|
|
92
|
+
for (const t of view.relatedTracks) {
|
|
93
|
+
const modifierStr = t.modifier > 0 ? `+${t.modifier}` : `${t.modifier}`;
|
|
94
|
+
lines.push(`- **${t.name}**: ${modifierStr}`);
|
|
95
|
+
}
|
|
96
|
+
lines.push("");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Related drivers
|
|
100
|
+
if (view.relatedDrivers.length > 0) {
|
|
101
|
+
lines.push("## Linked to Drivers", "");
|
|
102
|
+
for (const d of view.relatedDrivers) {
|
|
103
|
+
lines.push(`- ${d.name}`);
|
|
104
|
+
}
|
|
105
|
+
lines.push("");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill presentation helpers
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for formatting skill data across DOM and markdown outputs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
groupSkillsByCapability,
|
|
9
|
+
getCapabilityEmoji,
|
|
10
|
+
} from "../../model/levels.js";
|
|
11
|
+
import { getSkillTypeForDiscipline } from "../../model/derivation.js";
|
|
12
|
+
import { truncate } from "../shared.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format capability name for display
|
|
16
|
+
* @param {string} capability
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function formatCapability(capability) {
|
|
20
|
+
if (!capability) return "";
|
|
21
|
+
return capability.charAt(0).toUpperCase() + capability.slice(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} SkillListItem
|
|
26
|
+
* @property {string} id
|
|
27
|
+
* @property {string} name
|
|
28
|
+
* @property {string} description
|
|
29
|
+
* @property {string} capability
|
|
30
|
+
* @property {string} capabilityEmoji
|
|
31
|
+
* @property {string} truncatedDescription
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transform skills for list view (grouped by capability)
|
|
36
|
+
* @param {Array} skills - Raw skill entities
|
|
37
|
+
* @param {Array} capabilities - Capability entities
|
|
38
|
+
* @param {number} [descriptionLimit=120] - Maximum description length
|
|
39
|
+
* @returns {{ groups: Object<string, SkillListItem[]>, groupOrder: string[] }}
|
|
40
|
+
*/
|
|
41
|
+
export function prepareSkillsList(
|
|
42
|
+
skills,
|
|
43
|
+
capabilities,
|
|
44
|
+
descriptionLimit = 120,
|
|
45
|
+
) {
|
|
46
|
+
const grouped = groupSkillsByCapability(skills);
|
|
47
|
+
|
|
48
|
+
const groups = {};
|
|
49
|
+
for (const [capability, capabilitySkills] of Object.entries(grouped)) {
|
|
50
|
+
groups[capability] = capabilitySkills.map((skill) => ({
|
|
51
|
+
id: skill.id,
|
|
52
|
+
name: skill.name,
|
|
53
|
+
description: skill.description,
|
|
54
|
+
capability: skill.capability,
|
|
55
|
+
capabilityEmoji: getCapabilityEmoji(capabilities, skill.capability),
|
|
56
|
+
truncatedDescription: truncate(skill.description, descriptionLimit),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { groups, groupOrder: Object.keys(groups) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {Object} SkillDetailView
|
|
65
|
+
* @property {string} id
|
|
66
|
+
* @property {string} name
|
|
67
|
+
* @property {string} description
|
|
68
|
+
* @property {string} capability
|
|
69
|
+
* @property {boolean} isHumanOnly
|
|
70
|
+
* @property {string} capabilityEmoji
|
|
71
|
+
* @property {Object<string, string>} levelDescriptions
|
|
72
|
+
* @property {Array<{id: string, name: string, skillType: string}>} relatedDisciplines
|
|
73
|
+
* @property {Array<{id: string, name: string, modifier: number}>} relatedTracks
|
|
74
|
+
* @property {Array<{id: string, name: string}>} relatedDrivers
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Transform skill for detail view
|
|
79
|
+
* @param {Object} skill - Raw skill entity
|
|
80
|
+
* @param {Object} context - Additional context
|
|
81
|
+
* @param {Array} context.disciplines - All disciplines
|
|
82
|
+
* @param {Array} context.tracks - All tracks
|
|
83
|
+
* @param {Array} context.drivers - All drivers
|
|
84
|
+
* @param {Array} context.capabilities - Capability entities
|
|
85
|
+
* @returns {SkillDetailView|null}
|
|
86
|
+
*/
|
|
87
|
+
export function prepareSkillDetail(
|
|
88
|
+
skill,
|
|
89
|
+
{ disciplines, tracks, drivers, capabilities },
|
|
90
|
+
) {
|
|
91
|
+
if (!skill) return null;
|
|
92
|
+
|
|
93
|
+
const relatedDisciplines = disciplines
|
|
94
|
+
.filter((d) => getSkillTypeForDiscipline(d, skill.id) !== null)
|
|
95
|
+
.map((d) => ({
|
|
96
|
+
id: d.id,
|
|
97
|
+
name: d.specialization || d.name,
|
|
98
|
+
skillType: getSkillTypeForDiscipline(d, skill.id),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const relatedTracks = tracks
|
|
102
|
+
.filter((t) => t.skillModifiers?.[skill.id])
|
|
103
|
+
.map((t) => ({
|
|
104
|
+
id: t.id,
|
|
105
|
+
name: t.name,
|
|
106
|
+
modifier: t.skillModifiers[skill.id],
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const relatedDrivers = drivers
|
|
110
|
+
.filter((d) => d.contributingSkills?.includes(skill.id))
|
|
111
|
+
.map((d) => ({ id: d.id, name: d.name }));
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
id: skill.id,
|
|
115
|
+
name: skill.name,
|
|
116
|
+
description: skill.description,
|
|
117
|
+
capability: skill.capability,
|
|
118
|
+
isHumanOnly: skill.isHumanOnly || false,
|
|
119
|
+
capabilityEmoji: getCapabilityEmoji(capabilities, skill.capability),
|
|
120
|
+
levelDescriptions: skill.levelDescriptions,
|
|
121
|
+
relatedDisciplines,
|
|
122
|
+
relatedTracks,
|
|
123
|
+
relatedDrivers,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage formatting for DOM output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { div, h2, p, a, span, ul, li } from "../../lib/render.js";
|
|
6
|
+
import { createBackLink } from "../../components/nav.js";
|
|
7
|
+
import { prepareStageDetail, getStageEmoji } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format stage detail as DOM elements
|
|
11
|
+
* @param {Object} stage - Raw stage entity
|
|
12
|
+
* @param {Object} context - Additional context
|
|
13
|
+
* @param {Array} [context.stages] - All stages (for handoff links)
|
|
14
|
+
* @param {boolean} [context.showBackLink=true] - Whether to show back navigation link
|
|
15
|
+
* @returns {HTMLElement}
|
|
16
|
+
*/
|
|
17
|
+
export function stageToDOM(stage, { stages = [], showBackLink = true } = {}) {
|
|
18
|
+
const view = prepareStageDetail(stage);
|
|
19
|
+
const emoji = getStageEmoji(stages, stage.id);
|
|
20
|
+
|
|
21
|
+
return div(
|
|
22
|
+
{ className: "detail-page stage-detail" },
|
|
23
|
+
// Header
|
|
24
|
+
div(
|
|
25
|
+
{ className: "page-header" },
|
|
26
|
+
showBackLink ? createBackLink("/stage", "← Back to Stages") : null,
|
|
27
|
+
div(
|
|
28
|
+
{ className: "page-title-row" },
|
|
29
|
+
span({ className: "page-title" }, `${emoji} ${view.name}`),
|
|
30
|
+
span({ className: `badge ${view.modeClassName}` }, view.modeBadge),
|
|
31
|
+
),
|
|
32
|
+
p({ className: "page-description" }, view.description),
|
|
33
|
+
),
|
|
34
|
+
|
|
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
|
+
// Entry/Exit Criteria
|
|
53
|
+
view.entryCriteria.length > 0 || view.exitCriteria.length > 0
|
|
54
|
+
? div(
|
|
55
|
+
{ className: "section section-detail" },
|
|
56
|
+
div(
|
|
57
|
+
{ className: "content-columns" },
|
|
58
|
+
// Entry criteria column
|
|
59
|
+
view.entryCriteria.length > 0
|
|
60
|
+
? div(
|
|
61
|
+
{ className: "column" },
|
|
62
|
+
h2({ className: "section-title" }, "Entry Criteria"),
|
|
63
|
+
ul(
|
|
64
|
+
{ className: "criteria-list" },
|
|
65
|
+
...view.entryCriteria.map((item) =>
|
|
66
|
+
li({ className: "criteria-item" }, item),
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
: null,
|
|
71
|
+
// Exit criteria column
|
|
72
|
+
view.exitCriteria.length > 0
|
|
73
|
+
? div(
|
|
74
|
+
{ className: "column" },
|
|
75
|
+
h2({ className: "section-title" }, "Exit Criteria"),
|
|
76
|
+
ul(
|
|
77
|
+
{ className: "criteria-list" },
|
|
78
|
+
...view.exitCriteria.map((item) =>
|
|
79
|
+
li({ className: "criteria-item" }, item),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
: null,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
: null,
|
|
87
|
+
|
|
88
|
+
// Constraints
|
|
89
|
+
view.constraints.length > 0
|
|
90
|
+
? div(
|
|
91
|
+
{ className: "section section-detail" },
|
|
92
|
+
h2({ className: "section-title" }, "Constraints"),
|
|
93
|
+
ul(
|
|
94
|
+
{ className: "constraint-list" },
|
|
95
|
+
...view.constraints.map((item) =>
|
|
96
|
+
li({ className: "constraint-item" }, `⚠️ ${item}`),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
: null,
|
|
101
|
+
|
|
102
|
+
// Handoffs
|
|
103
|
+
view.handoffs.length > 0
|
|
104
|
+
? div(
|
|
105
|
+
{ className: "section section-detail" },
|
|
106
|
+
h2({ className: "section-title" }, "Handoffs"),
|
|
107
|
+
div(
|
|
108
|
+
{ className: "handoff-list" },
|
|
109
|
+
...view.handoffs.map((handoff) => {
|
|
110
|
+
const targetStage = stages.find((s) => s.id === handoff.target);
|
|
111
|
+
const targetEmoji = getStageEmoji(stages, handoff.target);
|
|
112
|
+
return div(
|
|
113
|
+
{ className: "handoff-card" },
|
|
114
|
+
div(
|
|
115
|
+
{ className: "handoff-header" },
|
|
116
|
+
showBackLink && targetStage
|
|
117
|
+
? a(
|
|
118
|
+
{ href: `#/stage/${handoff.target}` },
|
|
119
|
+
`${targetEmoji} ${handoff.label}`,
|
|
120
|
+
)
|
|
121
|
+
: span({}, `${targetEmoji} ${handoff.label}`),
|
|
122
|
+
),
|
|
123
|
+
handoff.prompt
|
|
124
|
+
? p(
|
|
125
|
+
{ className: "handoff-prompt text-muted" },
|
|
126
|
+
handoff.prompt,
|
|
127
|
+
)
|
|
128
|
+
: null,
|
|
129
|
+
);
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
: null,
|
|
134
|
+
);
|
|
135
|
+
}
|