@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.
Files changed (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. 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
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Stage formatters index
3
+ *
4
+ * Re-exports all stage formatting functions.
5
+ */
6
+
7
+ export {
8
+ prepareStagesList,
9
+ prepareStageDetail,
10
+ getStageEmoji,
11
+ } from "./shared.js";
12
+ export { stageToDOM } from "./dom.js";