@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,103 @@
1
+ /**
2
+ * Driver formatting for DOM output
3
+ */
4
+
5
+ import { div, heading1, heading2, p, a, span } from "../../lib/render.js";
6
+ import { createBackLink } from "../../components/nav.js";
7
+ import { prepareDriverDetail } from "./shared.js";
8
+ import { getConceptEmoji } from "../../model/levels.js";
9
+
10
+ /**
11
+ * Format driver detail as DOM elements
12
+ * @param {Object} driver - Raw driver entity
13
+ * @param {Object} context - Additional context and options
14
+ * @param {Array} context.skills - All skills
15
+ * @param {Array} context.behaviours - All behaviours
16
+ * @param {Object} [context.framework] - Framework config for emojis
17
+ * @param {boolean} [context.showBackLink=true] - Whether to show back navigation link
18
+ * @returns {HTMLElement}
19
+ */
20
+ export function driverToDOM(
21
+ driver,
22
+ { skills, behaviours, framework, showBackLink = true } = {},
23
+ ) {
24
+ const view = prepareDriverDetail(driver, { skills, behaviours });
25
+ const emoji = framework ? getConceptEmoji(framework, "driver") : "🎯";
26
+ return div(
27
+ { className: "detail-page driver-detail" },
28
+ // Header
29
+ div(
30
+ { className: "page-header" },
31
+ showBackLink ? createBackLink("/driver", "← Back to Drivers") : null,
32
+ heading1({ className: "page-title" }, `${emoji} `, view.name),
33
+ p({ className: "page-description" }, view.description),
34
+ ),
35
+
36
+ // Stats
37
+ div(
38
+ { className: "stats" },
39
+ div(
40
+ { className: "stat" },
41
+ div(
42
+ { className: "stat-value" },
43
+ String(view.contributingSkills.length),
44
+ ),
45
+ div({ className: "stat-label" }, "Contributing Skills"),
46
+ ),
47
+ div(
48
+ { className: "stat" },
49
+ div(
50
+ { className: "stat-value" },
51
+ String(view.contributingBehaviours.length),
52
+ ),
53
+ div({ className: "stat-label" }, "Contributing Behaviours"),
54
+ ),
55
+ ),
56
+
57
+ // Contributing Skills and Contributing Behaviours in two columns
58
+ view.contributingSkills.length > 0 || view.contributingBehaviours.length > 0
59
+ ? div(
60
+ { className: "detail-section" },
61
+ div(
62
+ { className: "content-columns" },
63
+ // Contributing Skills column
64
+ view.contributingSkills.length > 0
65
+ ? div(
66
+ { className: "column" },
67
+ heading2(
68
+ { className: "section-title" },
69
+ "Contributing Skills",
70
+ ),
71
+ ...view.contributingSkills.map((s) =>
72
+ div(
73
+ { className: "list-item" },
74
+ showBackLink
75
+ ? a({ href: `#/skill/${s.id}` }, s.name)
76
+ : span({}, s.name),
77
+ ),
78
+ ),
79
+ )
80
+ : null,
81
+ // Contributing Behaviours column
82
+ view.contributingBehaviours.length > 0
83
+ ? div(
84
+ { className: "column" },
85
+ heading2(
86
+ { className: "section-title" },
87
+ "Contributing Behaviours",
88
+ ),
89
+ ...view.contributingBehaviours.map((b) =>
90
+ div(
91
+ { className: "list-item" },
92
+ showBackLink
93
+ ? a({ href: `#/behaviour/${b.id}` }, b.name)
94
+ : span({}, b.name),
95
+ ),
96
+ ),
97
+ )
98
+ : null,
99
+ ),
100
+ )
101
+ : null,
102
+ );
103
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Driver presentation helpers
3
+ *
4
+ * Shared utilities for formatting driver data across DOM and markdown outputs.
5
+ */
6
+
7
+ import { truncate } from "../shared.js";
8
+
9
+ /**
10
+ * Get items by their IDs
11
+ * @param {Array} items - Array of items with id property
12
+ * @param {string[]} ids - Array of IDs to find
13
+ * @returns {Array} - Found items
14
+ */
15
+ function getItemsByIds(items, ids) {
16
+ if (!ids) return [];
17
+ return ids.map((id) => items.find((item) => item.id === id)).filter(Boolean);
18
+ }
19
+
20
+ /**
21
+ * @typedef {Object} DriverListItem
22
+ * @property {string} id
23
+ * @property {string} name
24
+ * @property {string} description
25
+ * @property {string} truncatedDescription
26
+ * @property {number} contributingSkillsCount
27
+ * @property {number} contributingBehavioursCount
28
+ */
29
+
30
+ /**
31
+ * Transform drivers for list view
32
+ * @param {Array} drivers - Raw driver entities
33
+ * @param {number} [descriptionLimit=150] - Maximum description length
34
+ * @returns {{ items: DriverListItem[] }}
35
+ */
36
+ export function prepareDriversList(drivers, descriptionLimit = 150) {
37
+ const items = drivers.map((driver) => ({
38
+ id: driver.id,
39
+ name: driver.name,
40
+ description: driver.description,
41
+ truncatedDescription: truncate(driver.description, descriptionLimit),
42
+ contributingSkillsCount: driver.contributingSkills?.length || 0,
43
+ contributingBehavioursCount: driver.contributingBehaviours?.length || 0,
44
+ }));
45
+
46
+ return { items };
47
+ }
48
+
49
+ /**
50
+ * @typedef {Object} DriverDetailView
51
+ * @property {string} id
52
+ * @property {string} name
53
+ * @property {string} description
54
+ * @property {Array<{id: string, name: string}>} contributingSkills
55
+ * @property {Array<{id: string, name: string}>} contributingBehaviours
56
+ */
57
+
58
+ /**
59
+ * Transform driver for detail view
60
+ * @param {Object} driver - Raw driver entity
61
+ * @param {Object} context - Additional context
62
+ * @param {Array} context.skills - All skills
63
+ * @param {Array} context.behaviours - All behaviours
64
+ * @returns {DriverDetailView|null}
65
+ */
66
+ export function prepareDriverDetail(driver, { skills, behaviours }) {
67
+ if (!driver) return null;
68
+
69
+ const contributingSkills = getItemsByIds(
70
+ skills,
71
+ driver.contributingSkills,
72
+ ).map((s) => ({
73
+ id: s.id,
74
+ name: s.name,
75
+ }));
76
+
77
+ const contributingBehaviours = getItemsByIds(
78
+ behaviours,
79
+ driver.contributingBehaviours,
80
+ ).map((b) => ({
81
+ id: b.id,
82
+ name: b.name,
83
+ }));
84
+
85
+ return {
86
+ id: driver.id,
87
+ name: driver.name,
88
+ description: driver.description,
89
+ contributingSkills,
90
+ contributingBehaviours,
91
+ };
92
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Grade formatting for DOM output
3
+ */
4
+
5
+ import {
6
+ div,
7
+ heading1,
8
+ heading2,
9
+ p,
10
+ span,
11
+ table,
12
+ tbody,
13
+ thead,
14
+ tr,
15
+ th,
16
+ td,
17
+ } from "../../lib/render.js";
18
+ import { createBackLink } from "../../components/nav.js";
19
+ import { createLevelDots } from "../../components/detail.js";
20
+ import {
21
+ SKILL_LEVEL_ORDER,
22
+ BEHAVIOUR_MATURITY_ORDER,
23
+ getConceptEmoji,
24
+ } from "../../model/levels.js";
25
+ import { createJobBuilderButton } from "../../components/action-buttons.js";
26
+ import { prepareGradeDetail } from "./shared.js";
27
+
28
+ /**
29
+ * Format grade detail as DOM elements
30
+ * @param {Object} grade - Raw grade entity
31
+ * @param {Object} options - Formatting options
32
+ * @param {Object} [options.framework] - Framework config for emojis
33
+ * @param {boolean} [options.showBackLink=true] - Whether to show back navigation link
34
+ * @returns {HTMLElement}
35
+ */
36
+ export function gradeToDOM(grade, { framework, showBackLink = true } = {}) {
37
+ const view = prepareGradeDetail(grade);
38
+ const emoji = framework ? getConceptEmoji(framework, "grade") : "📊";
39
+ return div(
40
+ { className: "detail-page grade-detail" },
41
+ // Header
42
+ div(
43
+ { className: "page-header" },
44
+ showBackLink ? createBackLink("/grade", "← Back to Grades") : null,
45
+ heading1({ className: "page-title" }, `${emoji} `, view.displayName),
46
+ div(
47
+ { className: "page-meta" },
48
+ span({ className: "badge badge-default" }, view.id),
49
+ ),
50
+ view.typicalExperienceRange
51
+ ? p(
52
+ { className: "page-description" },
53
+ `Typical experience: ${view.typicalExperienceRange}`,
54
+ )
55
+ : null,
56
+ showBackLink
57
+ ? div(
58
+ { className: "page-actions" },
59
+ createJobBuilderButton({
60
+ paramName: "grade",
61
+ paramValue: grade.id,
62
+ }),
63
+ )
64
+ : null,
65
+ ),
66
+
67
+ // Titles section
68
+ view.professionalTitle || view.managementTitle
69
+ ? div(
70
+ { className: "detail-section" },
71
+ heading2({ className: "section-title" }, "Titles"),
72
+ div(
73
+ { className: "content-columns" },
74
+ view.professionalTitle
75
+ ? div(
76
+ { className: "list-item" },
77
+ p({ className: "label" }, "Professional Track"),
78
+ p({}, view.professionalTitle),
79
+ )
80
+ : null,
81
+ view.managementTitle
82
+ ? div(
83
+ { className: "list-item" },
84
+ p({ className: "label" }, "Management Track"),
85
+ p({}, view.managementTitle),
86
+ )
87
+ : null,
88
+ ),
89
+ )
90
+ : null,
91
+
92
+ // Expectations
93
+ view.expectations && Object.keys(view.expectations).length > 0
94
+ ? div(
95
+ { className: "detail-section" },
96
+ heading2({ className: "section-title" }, "Expectations"),
97
+ div(
98
+ { className: "content-columns" },
99
+ ...Object.entries(view.expectations).map(([key, value]) =>
100
+ div(
101
+ { className: "list-item" },
102
+ p(
103
+ { className: "label" },
104
+ key.charAt(0).toUpperCase() + key.slice(1),
105
+ ),
106
+ p({}, value),
107
+ ),
108
+ ),
109
+ ),
110
+ )
111
+ : null,
112
+
113
+ // Base Skill Levels and Base Behaviour Maturity in two columns
114
+ div(
115
+ { className: "detail-section" },
116
+ div(
117
+ { className: "content-columns" },
118
+ // Base Skill Levels column
119
+ div(
120
+ { className: "column" },
121
+ heading2({ className: "section-title" }, "Base Skill Levels"),
122
+ table(
123
+ { className: "level-table" },
124
+ thead({}, tr({}, th({}, "Type"), th({}, "Level"))),
125
+ tbody(
126
+ {},
127
+ tr(
128
+ {},
129
+ td({}, span({ className: "badge badge-primary" }, "Primary")),
130
+ td(
131
+ {},
132
+ view.baseSkillLevels?.primary
133
+ ? createLevelDots(
134
+ SKILL_LEVEL_ORDER.indexOf(view.baseSkillLevels.primary),
135
+ SKILL_LEVEL_ORDER.length,
136
+ )
137
+ : span({ className: "text-muted" }, "—"),
138
+ ),
139
+ ),
140
+ tr(
141
+ {},
142
+ td(
143
+ {},
144
+ span({ className: "badge badge-secondary" }, "Secondary"),
145
+ ),
146
+ td(
147
+ {},
148
+ view.baseSkillLevels?.secondary
149
+ ? createLevelDots(
150
+ SKILL_LEVEL_ORDER.indexOf(
151
+ view.baseSkillLevels.secondary,
152
+ ),
153
+ SKILL_LEVEL_ORDER.length,
154
+ )
155
+ : span({ className: "text-muted" }, "—"),
156
+ ),
157
+ ),
158
+ tr(
159
+ {},
160
+ td({}, span({ className: "badge badge-broad" }, "Broad")),
161
+ td(
162
+ {},
163
+ view.baseSkillLevels?.broad
164
+ ? createLevelDots(
165
+ SKILL_LEVEL_ORDER.indexOf(view.baseSkillLevels.broad),
166
+ SKILL_LEVEL_ORDER.length,
167
+ )
168
+ : span({ className: "text-muted" }, "—"),
169
+ ),
170
+ ),
171
+ ),
172
+ ),
173
+ ),
174
+ // Base Behaviour Maturity column
175
+ div(
176
+ { className: "column" },
177
+ heading2({ className: "section-title" }, "Base Behaviour Maturity"),
178
+ view.baseBehaviourMaturity
179
+ ? table(
180
+ { className: "level-table" },
181
+ thead({}, tr({}, th({}, "Maturity"), th({}, "Level"))),
182
+ tbody(
183
+ {},
184
+ tr(
185
+ {},
186
+ td(
187
+ {},
188
+ view.baseBehaviourMaturity.charAt(0).toUpperCase() +
189
+ view.baseBehaviourMaturity.slice(1),
190
+ ),
191
+ td(
192
+ {},
193
+ createLevelDots(
194
+ BEHAVIOUR_MATURITY_ORDER.indexOf(
195
+ view.baseBehaviourMaturity,
196
+ ),
197
+ BEHAVIOUR_MATURITY_ORDER.length,
198
+ ),
199
+ ),
200
+ ),
201
+ ),
202
+ )
203
+ : p({ className: "text-muted" }, "—"),
204
+ ),
205
+ ),
206
+ ),
207
+ );
208
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Grade formatting for markdown/CLI output
3
+ */
4
+
5
+ import { tableToMarkdown, capitalize } from "../shared.js";
6
+ import { prepareGradesList, prepareGradeDetail } from "./shared.js";
7
+ import { getConceptEmoji } from "../../model/levels.js";
8
+
9
+ /**
10
+ * Format grade list as markdown
11
+ * @param {Array} grades - Raw grade entities
12
+ * @param {Object} [framework] - Framework config for emojis
13
+ * @returns {string}
14
+ */
15
+ export function gradeListToMarkdown(grades, framework) {
16
+ const { items } = prepareGradesList(grades);
17
+ const emoji = framework ? getConceptEmoji(framework, "grade") : "📊";
18
+ const lines = [`# ${emoji} Grades`, ""];
19
+
20
+ const rows = items.map((g) => [
21
+ g.id,
22
+ g.displayName,
23
+ g.typicalExperienceRange || "-",
24
+ capitalize(g.baseSkillLevels?.primary || "-"),
25
+ ]);
26
+
27
+ lines.push(tableToMarkdown(["ID", "Name", "Years", "Primary Level"], rows));
28
+ lines.push("");
29
+
30
+ return lines.join("\n");
31
+ }
32
+
33
+ /**
34
+ * Format grade detail as markdown
35
+ * @param {Object} grade - Raw grade entity
36
+ * @param {Object} [framework] - Framework config for emojis
37
+ * @returns {string}
38
+ */
39
+ export function gradeToMarkdown(grade, framework) {
40
+ const view = prepareGradeDetail(grade);
41
+ const emoji = framework ? getConceptEmoji(framework, "grade") : "📊";
42
+ const lines = [`# ${emoji} ${view.displayName} (${view.id})`, ""];
43
+
44
+ if (view.typicalExperienceRange) {
45
+ lines.push(`**Experience**: ${view.typicalExperienceRange}`, "");
46
+ }
47
+
48
+ // Titles
49
+ if (view.professionalTitle || view.managementTitle) {
50
+ lines.push("## Titles", "");
51
+ if (view.professionalTitle) {
52
+ lines.push(`- **Professional Track**: ${view.professionalTitle}`);
53
+ }
54
+ if (view.managementTitle) {
55
+ lines.push(`- **Management Track**: ${view.managementTitle}`);
56
+ }
57
+ lines.push("");
58
+ }
59
+
60
+ // Base skill levels
61
+ lines.push("## Base Skill Levels", "");
62
+ const skillRows = Object.entries(view.baseSkillLevels).map(
63
+ ([type, level]) => [capitalize(type), capitalize(level)],
64
+ );
65
+ lines.push(tableToMarkdown(["Skill Type", "Level"], skillRows));
66
+ lines.push("");
67
+
68
+ // Base behaviour maturity
69
+ if (
70
+ view.baseBehaviourMaturity &&
71
+ Object.keys(view.baseBehaviourMaturity).length > 0
72
+ ) {
73
+ lines.push("## Base Behaviour Maturity", "");
74
+ const behaviourRows = Object.entries(view.baseBehaviourMaturity).map(
75
+ ([type, maturity]) => [
76
+ capitalize(type),
77
+ capitalize(maturity.replace(/_/g, " ")),
78
+ ],
79
+ );
80
+ lines.push(tableToMarkdown(["Type", "Maturity"], behaviourRows));
81
+ lines.push("");
82
+ }
83
+
84
+ // Expectations
85
+ if (view.expectations && Object.keys(view.expectations).length > 0) {
86
+ lines.push("## Expectations", "");
87
+ for (const [key, value] of Object.entries(view.expectations)) {
88
+ lines.push(`- **${capitalize(key)}**: ${value}`);
89
+ }
90
+ lines.push("");
91
+ }
92
+
93
+ return lines.join("\n");
94
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Grade presentation helpers
3
+ *
4
+ * Shared utilities for formatting grade data across DOM and markdown outputs.
5
+ */
6
+
7
+ /**
8
+ * Get grade display name (shows both professional and management titles)
9
+ * @param {Object} grade
10
+ * @returns {string}
11
+ */
12
+ export function getGradeDisplayName(grade) {
13
+ if (grade.professionalTitle && grade.managementTitle) {
14
+ if (grade.professionalTitle === grade.managementTitle) {
15
+ return grade.professionalTitle;
16
+ }
17
+ return `${grade.professionalTitle} / ${grade.managementTitle}`;
18
+ }
19
+ return grade.professionalTitle || grade.managementTitle || grade.id;
20
+ }
21
+
22
+ /**
23
+ * @typedef {Object} GradeListItem
24
+ * @property {string} id
25
+ * @property {string} displayName
26
+ * @property {number} ordinalRank
27
+ * @property {string|null} typicalExperienceRange
28
+ * @property {Object} baseSkillLevels
29
+ * @property {string|null} impactScope
30
+ */
31
+
32
+ /**
33
+ * Transform grades for list view
34
+ * @param {Array} grades - Raw grade entities
35
+ * @returns {{ items: GradeListItem[] }}
36
+ */
37
+ export function prepareGradesList(grades) {
38
+ const sortedGrades = [...grades].sort(
39
+ (a, b) => a.ordinalRank - b.ordinalRank,
40
+ );
41
+
42
+ const items = sortedGrades.map((grade) => ({
43
+ id: grade.id,
44
+ displayName: getGradeDisplayName(grade),
45
+ ordinalRank: grade.ordinalRank,
46
+ typicalExperienceRange: grade.typicalExperienceRange || null,
47
+ baseSkillLevels: grade.baseSkillLevels || {},
48
+ impactScope: grade.expectations?.impactScope || null,
49
+ }));
50
+
51
+ return { items };
52
+ }
53
+
54
+ /**
55
+ * @typedef {Object} GradeDetailView
56
+ * @property {string} id
57
+ * @property {string} displayName
58
+ * @property {string} professionalTitle
59
+ * @property {string} managementTitle
60
+ * @property {number} ordinalRank
61
+ * @property {string|null} typicalExperienceRange
62
+ * @property {Object} baseSkillLevels
63
+ * @property {Object} baseBehaviourMaturity
64
+ * @property {Object} expectations
65
+ */
66
+
67
+ /**
68
+ * Transform grade for detail view
69
+ * @param {Object} grade - Raw grade entity
70
+ * @returns {GradeDetailView|null}
71
+ */
72
+ export function prepareGradeDetail(grade) {
73
+ if (!grade) return null;
74
+
75
+ return {
76
+ id: grade.id,
77
+ displayName: getGradeDisplayName(grade),
78
+ professionalTitle: grade.professionalTitle || null,
79
+ managementTitle: grade.managementTitle || null,
80
+ ordinalRank: grade.ordinalRank,
81
+ typicalExperienceRange: grade.typicalExperienceRange || null,
82
+ baseSkillLevels: grade.baseSkillLevels || {},
83
+ baseBehaviourMaturity: grade.baseBehaviourMaturity || {},
84
+ expectations: grade.expectations || {},
85
+ };
86
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Formatter Layer
3
+ *
4
+ * Export all formatters for easy importing.
5
+ * Formatters transform presenter output into specific formats (DOM, markdown)
6
+ */
7
+
8
+ // Shared utilities
9
+ export * from "./shared.js";
10
+
11
+ // Job formatters
12
+ export { jobToMarkdown } from "./job/markdown.js";
13
+ export { jobToDOM } from "./job/dom.js";
14
+
15
+ // Interview formatters
16
+ export { interviewToMarkdown } from "./interview/markdown.js";
17
+ export { interviewToDOM } from "./interview/dom.js";
18
+
19
+ // Progress formatters
20
+ export { progressToMarkdown } from "./progress/markdown.js";
21
+ export { progressToDOM } from "./progress/dom.js";
22
+
23
+ // Driver formatters
24
+ export { driverToDOM } from "./driver/dom.js";
25
+
26
+ // Skill formatters
27
+ export { skillListToMarkdown, skillToMarkdown } from "./skill/markdown.js";
28
+ export { skillToDOM } from "./skill/dom.js";
29
+
30
+ // Behaviour formatters
31
+ export {
32
+ behaviourListToMarkdown,
33
+ behaviourToMarkdown,
34
+ } from "./behaviour/markdown.js";
35
+ export { behaviourToDOM } from "./behaviour/dom.js";
36
+
37
+ // Discipline formatters
38
+ export {
39
+ disciplineListToMarkdown,
40
+ disciplineToMarkdown,
41
+ } from "./discipline/markdown.js";
42
+ export { disciplineToDOM } from "./discipline/dom.js";
43
+
44
+ // Grade formatters
45
+ export { gradeListToMarkdown, gradeToMarkdown } from "./grade/markdown.js";
46
+ export { gradeToDOM } from "./grade/dom.js";
47
+
48
+ // Track formatters
49
+ export { trackListToMarkdown, trackToMarkdown } from "./track/markdown.js";
50
+ export { trackToDOM } from "./track/dom.js";