@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,111 @@
1
+ /**
2
+ * Stage presentation helpers
3
+ *
4
+ * Shared utilities for formatting stage data across DOM and CLI outputs.
5
+ */
6
+
7
+ import { truncate } from "../shared.js";
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
+ /**
23
+ * @typedef {Object} StageListItem
24
+ * @property {string} id
25
+ * @property {string} name
26
+ * @property {string} emoji
27
+ * @property {string} description
28
+ * @property {string} truncatedDescription
29
+ * @property {Array<{id: string, icon: string, label: string}>} tools
30
+ * @property {Array<{target: string, label: string}>} handoffs
31
+ */
32
+
33
+ /**
34
+ * Transform stages for list view
35
+ * @param {Array} stages - Raw stage entities
36
+ * @param {number} [descriptionLimit=150] - Maximum description length
37
+ * @returns {{ items: StageListItem[] }}
38
+ */
39
+ export function prepareStagesList(stages, descriptionLimit = 150) {
40
+ const items = stages.map((stage) => {
41
+ const tools = stage.availableTools || [];
42
+ return {
43
+ id: stage.id,
44
+ name: stage.name,
45
+ emoji: stage.emoji,
46
+ description: stage.description,
47
+ 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
+ handoffs: (stage.handoffs || []).map((h) => ({
54
+ target: h.targetStage,
55
+ label: h.label,
56
+ })),
57
+ };
58
+ });
59
+
60
+ return { items };
61
+ }
62
+
63
+ /**
64
+ * @typedef {Object} StageDetailView
65
+ * @property {string} id
66
+ * @property {string} name
67
+ * @property {string} description
68
+ * @property {Array<{id: string, icon: string, label: string}>} tools
69
+ * @property {string[]} constraints
70
+ * @property {string[]} entryCriteria
71
+ * @property {string[]} exitCriteria
72
+ * @property {Array<{target: string, label: string, prompt: string}>} handoffs
73
+ */
74
+
75
+ /**
76
+ * Transform stage for detail view
77
+ * @param {Object} stage - Raw stage entity
78
+ * @returns {StageDetailView}
79
+ */
80
+ export function prepareStageDetail(stage) {
81
+ const tools = stage.availableTools || [];
82
+ return {
83
+ id: stage.id,
84
+ name: stage.name,
85
+ 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
+ constraints: stage.constraints || [],
92
+ entryCriteria: stage.entryCriteria || [],
93
+ exitCriteria: stage.exitCriteria || [],
94
+ handoffs: (stage.handoffs || []).map((h) => ({
95
+ target: h.targetStage,
96
+ label: h.label,
97
+ prompt: h.prompt,
98
+ })),
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Get the stage emoji from loaded stages data
104
+ * @param {Object[]} stages - Loaded stages array
105
+ * @param {string} stageId - Stage identifier
106
+ * @returns {string}
107
+ */
108
+ export function getStageEmoji(stages, stageId) {
109
+ const stage = stages.find((s) => s.id === stageId);
110
+ return stage?.emoji || "🔄";
111
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Track formatting for DOM/web output
3
+ */
4
+
5
+ import { div, h1, p } from "../../lib/render.js";
6
+ import { createBackLink } from "../../components/nav.js";
7
+ import { createBadge, createStatCard } from "../../components/card.js";
8
+ import { createStatsGrid } from "../../components/grid.js";
9
+ import {
10
+ createDetailSection,
11
+ createLinksList,
12
+ } from "../../components/detail.js";
13
+ import {
14
+ createJobBuilderButton,
15
+ createInterviewPrepButton,
16
+ } from "../../components/action-buttons.js";
17
+ import {
18
+ createBehaviourModifierTable,
19
+ createSkillModifierTableWithCapabilities,
20
+ } from "../../components/modifier-table.js";
21
+ import { getConceptEmoji } from "../../model/levels.js";
22
+ 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
+ }
39
+
40
+ /**
41
+ * Format track detail as DOM elements
42
+ * @param {Object} track - Raw track entity
43
+ * @param {Object} context - Additional context
44
+ * @param {Array} context.skills - All skills
45
+ * @param {Array} context.behaviours - All behaviours
46
+ * @param {Array} context.disciplines - All disciplines
47
+ * @param {Object} [context.framework] - Framework data for emoji lookup
48
+ * @returns {HTMLElement}
49
+ */
50
+ export function trackToDOM(
51
+ track,
52
+ { skills, behaviours, disciplines, framework },
53
+ ) {
54
+ const view = prepareTrackDetail(track, { skills, behaviours, disciplines });
55
+ const emoji = getConceptEmoji(framework, "track");
56
+ // Build modifier sections - group them together for print layout
57
+ const hasSkillModifiers = view.skillModifiers.length > 0;
58
+ const hasBehaviourModifiers = view.behaviourModifiers.length > 0;
59
+
60
+ const modifiersSection =
61
+ hasSkillModifiers || hasBehaviourModifiers
62
+ ? div(
63
+ { className: "print-columns" },
64
+ hasSkillModifiers
65
+ ? createDetailSection({
66
+ title: "Skill Modifiers",
67
+ content: createSkillModifierTableWithCapabilities(
68
+ view.skillModifiers,
69
+ ),
70
+ })
71
+ : null,
72
+ hasBehaviourModifiers
73
+ ? createDetailSection({
74
+ title: "Behaviour Modifiers",
75
+ content: createBehaviourModifierTable(view.behaviourModifiers),
76
+ })
77
+ : null,
78
+ )
79
+ : null;
80
+
81
+ return div(
82
+ { className: "detail-page track-detail" },
83
+ // Header
84
+ div(
85
+ { className: "page-header" },
86
+ createBackLink("/track", "← Back to Tracks"),
87
+ h1({ className: "page-title" }, `${emoji} `, view.name),
88
+ div({ className: "page-meta" }, ...getTrackTypeBadges(view)),
89
+ p(
90
+ { className: "text-muted", style: "margin-top: 0.5rem" },
91
+ view.description,
92
+ ),
93
+ div(
94
+ { className: "page-actions" },
95
+ createJobBuilderButton({ paramName: "track", paramValue: track.id }),
96
+ createInterviewPrepButton({ paramName: "track", paramValue: track.id }),
97
+ ),
98
+ ),
99
+
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
+ // Matching weights (stat cards)
109
+ track.matchingWeights
110
+ ? createDetailSection({
111
+ title: "Matching Weights",
112
+ content: createStatsGrid([
113
+ createStatCard({
114
+ value: `${(track.matchingWeights.skills * 100).toFixed(0)}%`,
115
+ label: "Skills Weight",
116
+ }),
117
+ createStatCard({
118
+ value: `${(track.matchingWeights.behaviours * 100).toFixed(0)}%`,
119
+ label: "Behaviours Weight",
120
+ }),
121
+ ]),
122
+ })
123
+ : null,
124
+
125
+ // Skill and Behaviour modifiers in columns for print
126
+ modifiersSection,
127
+ );
128
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Track formatting for markdown/CLI output
3
+ */
4
+
5
+ import { tableToMarkdown } from "../shared.js";
6
+ import { prepareTracksList, prepareTrackDetail } from "./shared.js";
7
+ import { getConceptEmoji } from "../../model/levels.js";
8
+
9
+ /**
10
+ * Format track list as markdown
11
+ * @param {Array} tracks - Raw track entities
12
+ * @param {Object} [framework] - Framework config for emojis
13
+ * @returns {string}
14
+ */
15
+ export function trackListToMarkdown(tracks, framework) {
16
+ const { items } = prepareTracksList(tracks);
17
+ const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
18
+ const lines = [`# ${emoji} Tracks`, ""];
19
+
20
+ for (const track of items) {
21
+ const types = [];
22
+ if (track.isProfessional) types.push("Professional");
23
+ if (track.isManagement) types.push("Management");
24
+ lines.push(`- **${track.name}**: ${types.join(", ")}`);
25
+ }
26
+ lines.push("");
27
+
28
+ return lines.join("\n");
29
+ }
30
+
31
+ /**
32
+ * Format track detail as markdown
33
+ * @param {Object} track - Raw track entity
34
+ * @param {Object} context - Additional context
35
+ * @param {Array} context.skills - All skills
36
+ * @param {Array} context.behaviours - All behaviours
37
+ * @param {Array} context.disciplines - All disciplines
38
+ * @param {Object} [context.framework] - Framework config for emojis
39
+ * @returns {string}
40
+ */
41
+ export function trackToMarkdown(
42
+ track,
43
+ { skills, behaviours, disciplines, framework },
44
+ ) {
45
+ const view = prepareTrackDetail(track, { skills, behaviours, disciplines });
46
+
47
+ const types = [];
48
+ if (view.isProfessional) types.push("Professional");
49
+ if (view.isManagement) types.push("Management");
50
+
51
+ const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
52
+ const lines = [
53
+ `# ${emoji} ${view.name}`,
54
+ "",
55
+ `**Type**: ${types.join(", ")}`,
56
+ "",
57
+ view.description,
58
+ "",
59
+ ];
60
+
61
+ // Skill modifiers - show expanded skills for capabilities
62
+ if (view.skillModifiers.length > 0) {
63
+ lines.push("## Skill Modifiers", "");
64
+ for (const m of view.skillModifiers) {
65
+ const modifierStr = m.modifier > 0 ? `+${m.modifier}` : `${m.modifier}`;
66
+ if (m.isCapability && m.skills && m.skills.length > 0) {
67
+ // Capability with expanded skills
68
+ lines.push(`### ${m.name} Capability (${modifierStr})`, "");
69
+ for (const skill of m.skills) {
70
+ lines.push(`- ${skill.name}`);
71
+ }
72
+ lines.push("");
73
+ } else if (m.isCapability) {
74
+ // Capability without expanded skills
75
+ lines.push(`- **All ${m.name} skills**: ${modifierStr}`);
76
+ } else {
77
+ // Individual skill
78
+ lines.push(`- **${m.name}**: ${modifierStr}`);
79
+ }
80
+ }
81
+ lines.push("");
82
+ }
83
+
84
+ // Behaviour modifiers
85
+ if (view.behaviourModifiers.length > 0) {
86
+ lines.push("## Behaviour Modifiers", "");
87
+ const modifierRows = view.behaviourModifiers.map((b) => [
88
+ b.name,
89
+ b.modifier > 0 ? `+${b.modifier}` : `${b.modifier}`,
90
+ ]);
91
+ lines.push(tableToMarkdown(["Behaviour", "Modifier"], modifierRows));
92
+ lines.push("");
93
+ }
94
+
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
+ return lines.join("\n");
105
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Track presentation helpers
3
+ *
4
+ * Shared utilities for formatting track data across DOM and markdown outputs.
5
+ */
6
+
7
+ import { isCapability, getSkillsByCapability } from "../../model/modifiers.js";
8
+ import { truncate } from "../shared.js";
9
+
10
+ /**
11
+ * Sort tracks by type: professional tracks first, then management tracks.
12
+ * Within each type, preserves original order.
13
+ * @param {Array} tracks - Raw track entities
14
+ * @returns {Array} Sorted tracks array
15
+ */
16
+ export function sortTracksByType(tracks) {
17
+ return [...tracks].sort((a, b) => {
18
+ const aFlags = getTrackTypeFlags(a);
19
+ const bFlags = getTrackTypeFlags(b);
20
+
21
+ // Professional tracks come first
22
+ if (aFlags.isProfessional && !bFlags.isProfessional) return -1;
23
+ if (!aFlags.isProfessional && bFlags.isProfessional) return 1;
24
+
25
+ // Preserve original order within same type
26
+ return 0;
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Determine track type flags from track data.
32
+ *
33
+ * Logic: Only one flag needs to be explicitly set to true; the other defaults to false.
34
+ * - If isManagement: true → management track (isProfessional = false)
35
+ * - If isProfessional: true (or neither set) → professional track (isManagement = false)
36
+ *
37
+ * @param {Object} track
38
+ * @param {boolean} [track.isProfessional] - Whether this is a professional/IC track
39
+ * @param {boolean} [track.isManagement] - Whether this is a management track
40
+ * @returns {{isProfessional: boolean, isManagement: boolean}}
41
+ */
42
+ export function getTrackTypeFlags(track) {
43
+ // Management takes precedence if explicitly set to true
44
+ const isManagement = track.isManagement === true;
45
+ // Professional is true if management is not true (default behavior)
46
+ const isProfessional = !isManagement && track.isProfessional !== false;
47
+ return { isProfessional, isManagement };
48
+ }
49
+
50
+ /**
51
+ * @typedef {Object} TrackListItem
52
+ * @property {string} id
53
+ * @property {string} name
54
+ * @property {string} description
55
+ * @property {string} truncatedDescription
56
+ * @property {boolean} isProfessional
57
+ * @property {boolean} isManagement
58
+ */
59
+
60
+ /**
61
+ * Transform tracks for list view
62
+ * @param {Array} tracks - Raw track entities
63
+ * @param {number} [descriptionLimit=120] - Maximum description length
64
+ * @returns {{ items: TrackListItem[] }}
65
+ */
66
+ export function prepareTracksList(tracks, descriptionLimit = 120) {
67
+ const sortedTracks = sortTracksByType(tracks);
68
+ const items = sortedTracks.map((track) => {
69
+ const { isProfessional, isManagement } = getTrackTypeFlags(track);
70
+ return {
71
+ id: track.id,
72
+ name: track.name,
73
+ description: track.description,
74
+ truncatedDescription: truncate(track.description, descriptionLimit),
75
+ isProfessional,
76
+ isManagement,
77
+ };
78
+ });
79
+
80
+ return { items };
81
+ }
82
+
83
+ /**
84
+ * @typedef {Object} SkillModifierRow
85
+ * @property {string} id
86
+ * @property {string} name
87
+ * @property {number} modifier
88
+ * @property {boolean} isCapability
89
+ * @property {Array<{id: string, name: string}>} [skills]
90
+ */
91
+
92
+ /**
93
+ * @typedef {Object} BehaviourModifierRow
94
+ * @property {string} id
95
+ * @property {string} name
96
+ * @property {number} modifier
97
+ */
98
+
99
+ /**
100
+ * @typedef {Object} TrackDetailView
101
+ * @property {string} id
102
+ * @property {string} name
103
+ * @property {string} description
104
+ * @property {boolean} isProfessional
105
+ * @property {boolean} isManagement
106
+ * @property {SkillModifierRow[]} skillModifiers
107
+ * @property {BehaviourModifierRow[]} behaviourModifiers
108
+ * @property {Array<{id: string, name: string}>} validDisciplines
109
+ */
110
+
111
+ /**
112
+ * Transform track for detail view
113
+ * @param {Object} track - Raw track entity
114
+ * @param {Object} context - Additional context
115
+ * @param {Array} context.skills - All skills
116
+ * @param {Array} context.behaviours - All behaviours
117
+ * @param {Array} context.disciplines - All disciplines
118
+ * @returns {TrackDetailView|null}
119
+ */
120
+ export function prepareTrackDetail(track, { skills, behaviours, disciplines }) {
121
+ if (!track) return null;
122
+
123
+ const { isProfessional, isManagement } = getTrackTypeFlags(track);
124
+
125
+ // Build skill modifiers
126
+ const skillModifiers = track.skillModifiers
127
+ ? Object.entries(track.skillModifiers).map(([key, modifier]) => {
128
+ if (isCapability(key)) {
129
+ const capabilitySkills = getSkillsByCapability(skills, key);
130
+ return {
131
+ id: key,
132
+ name: key.charAt(0).toUpperCase() + key.slice(1),
133
+ modifier,
134
+ isCapability: true,
135
+ skills: capabilitySkills.map((s) => ({ id: s.id, name: s.name })),
136
+ };
137
+ } else {
138
+ const skill = skills.find((s) => s.id === key);
139
+ return {
140
+ id: key,
141
+ name: skill?.name || key,
142
+ modifier,
143
+ isCapability: false,
144
+ };
145
+ }
146
+ })
147
+ : [];
148
+
149
+ // Build behaviour modifiers
150
+ const behaviourModifiers = track.behaviourModifiers
151
+ ? Object.entries(track.behaviourModifiers).map(
152
+ ([behaviourId, modifier]) => {
153
+ const behaviour = behaviours.find((b) => b.id === behaviourId);
154
+ return {
155
+ id: behaviourId,
156
+ name: behaviour?.name || behaviourId,
157
+ modifier,
158
+ };
159
+ },
160
+ )
161
+ : [];
162
+
163
+ // Get valid disciplines
164
+ const validDisciplines = track.validDisciplines
165
+ ? track.validDisciplines
166
+ .map((id) => disciplines.find((d) => d.id === id))
167
+ .filter(Boolean)
168
+ .map((d) => ({ id: d.id, name: d.specialization || d.name }))
169
+ : [];
170
+
171
+ return {
172
+ id: track.id,
173
+ name: track.name,
174
+ description: track.description,
175
+ isProfessional,
176
+ isManagement,
177
+ skillModifiers,
178
+ behaviourModifiers,
179
+ validDisciplines,
180
+ };
181
+ }