@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,58 @@
1
+ /**
2
+ * Agent Skill Formatter
3
+ *
4
+ * Formats agent skill data into SKILL.md file content
5
+ * following the Agent Skills Standard specification.
6
+ */
7
+
8
+ /**
9
+ * Format agent skill as SKILL.md file content
10
+ * @param {Object} skill - Skill with frontmatter and body
11
+ * @param {Object} skill.frontmatter - YAML frontmatter data
12
+ * @param {string} skill.frontmatter.name - Skill name (required)
13
+ * @param {string} skill.frontmatter.description - Skill description (required)
14
+ * @param {string} skill.body - Markdown body content
15
+ * @returns {string} Complete SKILL.md file content
16
+ */
17
+ export function formatAgentSkill({ frontmatter, body }) {
18
+ const lines = ["---"];
19
+
20
+ // Name (required)
21
+ lines.push(`name: ${frontmatter.name}`);
22
+
23
+ // Description (required) - handle multiline
24
+ const description = frontmatter.description.trim();
25
+ if (description.includes("\n")) {
26
+ lines.push("description: |");
27
+ for (const line of description.split("\n")) {
28
+ lines.push(` ${line}`);
29
+ }
30
+ } else {
31
+ lines.push(`description: ${description}`);
32
+ }
33
+
34
+ lines.push("---");
35
+ lines.push("");
36
+ lines.push(body);
37
+
38
+ return lines.join("\n");
39
+ }
40
+
41
+ /**
42
+ * Format agent skill for CLI output (markdown)
43
+ * @param {Object} skill - Skill with frontmatter and body
44
+ * @returns {string} Markdown formatted for CLI display
45
+ */
46
+ export function formatAgentSkillForCli({ frontmatter, body }) {
47
+ const lines = [];
48
+
49
+ lines.push(`# Skill: ${frontmatter.name}`);
50
+ lines.push("");
51
+ lines.push(`**Description:** ${frontmatter.description.trim()}`);
52
+ lines.push("");
53
+ lines.push("---");
54
+ lines.push("");
55
+ lines.push(body);
56
+
57
+ return lines.join("\n");
58
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Behaviour formatting for DOM output
3
+ */
4
+
5
+ import {
6
+ div,
7
+ heading1,
8
+ heading2,
9
+ p,
10
+ table,
11
+ tbody,
12
+ thead,
13
+ tr,
14
+ th,
15
+ td,
16
+ ul,
17
+ li,
18
+ } from "../../lib/render.js";
19
+ import { createBackLink } from "../../components/nav.js";
20
+ import { createLevelCell } from "../../components/detail.js";
21
+ import {
22
+ BEHAVIOUR_MATURITY_ORDER,
23
+ getConceptEmoji,
24
+ } from "../../model/levels.js";
25
+ import { prepareBehaviourDetail } from "./shared.js";
26
+
27
+ /**
28
+ * Format behaviour detail as DOM elements
29
+ * @param {Object} behaviour - Raw behaviour entity
30
+ * @param {Object} context - Additional context and options
31
+ * @param {Array} context.drivers - All drivers
32
+ * @param {Object} [context.framework] - Framework data for emoji lookup
33
+ * @param {boolean} [context.showBackLink=true] - Whether to show back navigation link
34
+ * @returns {HTMLElement}
35
+ */
36
+ export function behaviourToDOM(
37
+ behaviour,
38
+ { drivers, framework, showBackLink = true } = {},
39
+ ) {
40
+ const view = prepareBehaviourDetail(behaviour, { drivers });
41
+ const emoji = getConceptEmoji(framework, "behaviour");
42
+ return div(
43
+ { className: "detail-page behaviour-detail" },
44
+ // Header
45
+ div(
46
+ { className: "page-header" },
47
+ showBackLink
48
+ ? createBackLink("/behaviour", "← Back to Behaviours")
49
+ : null,
50
+ heading1({ className: "page-title" }, `${emoji} `, view.name),
51
+ p({ className: "page-description" }, view.description),
52
+ ),
53
+
54
+ // Maturity descriptions
55
+ div(
56
+ { className: "detail-section" },
57
+ heading2({ className: "section-title" }, "Maturity Levels"),
58
+ table(
59
+ { className: "level-table" },
60
+ thead({}, tr({}, th({}, "Level"), th({}, "Description"))),
61
+ tbody(
62
+ {},
63
+ ...BEHAVIOUR_MATURITY_ORDER.map((maturity, index) => {
64
+ const description = view.maturityDescriptions[maturity] || "—";
65
+ return tr(
66
+ {},
67
+ createLevelCell(
68
+ index + 1,
69
+ BEHAVIOUR_MATURITY_ORDER.length,
70
+ maturity,
71
+ ),
72
+ td({}, description),
73
+ );
74
+ }),
75
+ ),
76
+ ),
77
+ ),
78
+
79
+ // Related drivers
80
+ view.relatedDrivers.length > 0
81
+ ? div(
82
+ { className: "detail-section" },
83
+ heading2({ className: "section-title" }, "Linked to Drivers"),
84
+ ul(
85
+ { className: "related-list" },
86
+ ...view.relatedDrivers.map((d) => li({}, d.name)),
87
+ ),
88
+ )
89
+ : null,
90
+ );
91
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Behaviour formatting for markdown/CLI output
3
+ */
4
+
5
+ import { tableToMarkdown, capitalize } from "../shared.js";
6
+ import { prepareBehavioursList, prepareBehaviourDetail } from "./shared.js";
7
+
8
+ /**
9
+ * Format behaviour list as markdown
10
+ * @param {Array} behaviours - Raw behaviour entities
11
+ * @returns {string}
12
+ */
13
+ export function behaviourListToMarkdown(behaviours) {
14
+ const { items } = prepareBehavioursList(behaviours);
15
+ const lines = ["# 🧠 Behaviours", ""];
16
+
17
+ for (const behaviour of items) {
18
+ lines.push(`- **${behaviour.name}**: ${behaviour.truncatedDescription}`);
19
+ }
20
+ lines.push("");
21
+
22
+ return lines.join("\n");
23
+ }
24
+
25
+ /**
26
+ * Format behaviour detail as markdown
27
+ * @param {Object} behaviour - Raw behaviour entity
28
+ * @param {Object} context - Additional context
29
+ * @param {Array} context.drivers - All drivers
30
+ * @returns {string}
31
+ */
32
+ export function behaviourToMarkdown(behaviour, { drivers }) {
33
+ const view = prepareBehaviourDetail(behaviour, { drivers });
34
+ const lines = [`# 🧠 ${view.name}`, "", view.description, ""];
35
+
36
+ // Maturity descriptions table
37
+ lines.push("## Maturity Levels", "");
38
+ const maturityRows = Object.entries(view.maturityDescriptions).map(
39
+ ([maturity, desc]) => [capitalize(maturity.replace(/_/g, " ")), desc],
40
+ );
41
+ lines.push(tableToMarkdown(["Maturity", "Description"], maturityRows));
42
+ lines.push("");
43
+
44
+ // Related drivers
45
+ if (view.relatedDrivers.length > 0) {
46
+ lines.push("## Linked to Drivers", "");
47
+ for (const d of view.relatedDrivers) {
48
+ lines.push(`- ${d.name}`);
49
+ }
50
+ lines.push("");
51
+ }
52
+
53
+ return lines.join("\n");
54
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Behaviour presentation helpers
3
+ *
4
+ * Shared utilities for formatting behaviour data across DOM and markdown outputs.
5
+ */
6
+
7
+ import { truncate } from "../shared.js";
8
+
9
+ /**
10
+ * @typedef {Object} BehaviourListItem
11
+ * @property {string} id
12
+ * @property {string} name
13
+ * @property {string} description
14
+ * @property {string} truncatedDescription
15
+ */
16
+
17
+ /**
18
+ * Transform behaviours for list view
19
+ * @param {Array} behaviours - Raw behaviour entities
20
+ * @param {number} [descriptionLimit=150] - Maximum description length
21
+ * @returns {{ items: BehaviourListItem[] }}
22
+ */
23
+ export function prepareBehavioursList(behaviours, descriptionLimit = 150) {
24
+ const items = behaviours.map((behaviour) => ({
25
+ id: behaviour.id,
26
+ name: behaviour.name,
27
+ description: behaviour.description,
28
+ truncatedDescription: truncate(behaviour.description, descriptionLimit),
29
+ }));
30
+
31
+ return { items };
32
+ }
33
+
34
+ /**
35
+ * @typedef {Object} BehaviourDetailView
36
+ * @property {string} id
37
+ * @property {string} name
38
+ * @property {string} description
39
+ * @property {Object<string, string>} maturityDescriptions
40
+ * @property {Array<{id: string, name: string}>} relatedDrivers
41
+ */
42
+
43
+ /**
44
+ * Transform behaviour for detail view
45
+ * @param {Object} behaviour - Raw behaviour entity
46
+ * @param {Object} context - Additional context
47
+ * @param {Array} context.drivers - All drivers
48
+ * @returns {BehaviourDetailView|null}
49
+ */
50
+ export function prepareBehaviourDetail(behaviour, { drivers }) {
51
+ if (!behaviour) return null;
52
+
53
+ const relatedDrivers = drivers
54
+ .filter((d) => d.contributingBehaviours?.includes(behaviour.id))
55
+ .map((d) => ({ id: d.id, name: d.name }));
56
+
57
+ return {
58
+ id: behaviour.id,
59
+ name: behaviour.name,
60
+ description: behaviour.description,
61
+ maturityDescriptions: behaviour.maturityDescriptions,
62
+ relatedDrivers,
63
+ };
64
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Discipline formatting for DOM output
3
+ */
4
+
5
+ import {
6
+ a,
7
+ div,
8
+ heading1,
9
+ heading2,
10
+ heading3,
11
+ p,
12
+ span,
13
+ ul,
14
+ li,
15
+ } from "../../lib/render.js";
16
+ import { createBackLink } from "../../components/nav.js";
17
+ import {
18
+ createJobBuilderButton,
19
+ createInterviewPrepButton,
20
+ } from "../../components/action-buttons.js";
21
+ import { getConceptEmoji } from "../../model/levels.js";
22
+ import { prepareDisciplineDetail } from "./shared.js";
23
+
24
+ /**
25
+ * Format discipline detail as DOM elements
26
+ * @param {Object} discipline - Raw discipline entity
27
+ * @param {Object} context - Additional context and options
28
+ * @param {Array} context.skills - All skills
29
+ * @param {Array} context.behaviours - All behaviours
30
+ * @param {Object} [context.framework] - Framework data for emoji lookup
31
+ * @param {boolean} [context.showBackLink=true] - Whether to show back navigation link
32
+ * @param {boolean} [context.showBehaviourModifiers=true] - Whether to show behaviour modifiers section
33
+ * @returns {HTMLElement}
34
+ */
35
+ export function disciplineToDOM(
36
+ discipline,
37
+ {
38
+ skills,
39
+ behaviours,
40
+ framework,
41
+ showBackLink = true,
42
+ showBehaviourModifiers = true,
43
+ } = {},
44
+ ) {
45
+ const view = prepareDisciplineDetail(discipline, { skills, behaviours });
46
+ const emoji = getConceptEmoji(framework, "discipline");
47
+ return div(
48
+ { className: "detail-page discipline-detail" },
49
+ // Header
50
+ div(
51
+ { className: "page-header" },
52
+ showBackLink
53
+ ? createBackLink("/discipline", "← Back to Disciplines")
54
+ : null,
55
+ heading1({ className: "page-title" }, `${emoji} `, view.name),
56
+ p({ className: "page-description" }, view.description),
57
+ showBackLink
58
+ ? div(
59
+ { className: "page-actions" },
60
+ createJobBuilderButton({
61
+ paramName: "discipline",
62
+ paramValue: discipline.id,
63
+ }),
64
+ createInterviewPrepButton({
65
+ paramName: "discipline",
66
+ paramValue: discipline.id,
67
+ }),
68
+ )
69
+ : null,
70
+ ),
71
+
72
+ // Stats
73
+ div(
74
+ { className: "stats" },
75
+ div(
76
+ { className: "stat" },
77
+ div({ className: "stat-value" }, String(view.coreSkills.length)),
78
+ div({ className: "stat-label" }, "Core Skills"),
79
+ ),
80
+ div(
81
+ { className: "stat" },
82
+ div({ className: "stat-value" }, String(view.supportingSkills.length)),
83
+ div({ className: "stat-label" }, "Supporting Skills"),
84
+ ),
85
+ div(
86
+ { className: "stat" },
87
+ div({ className: "stat-value" }, String(view.broadSkills.length)),
88
+ div({ className: "stat-label" }, "Broad Skills"),
89
+ ),
90
+ ),
91
+
92
+ // Skills sections - all three columns
93
+ div(
94
+ { className: "detail-section" },
95
+ heading2({ className: "section-title" }, "Skill Profile"),
96
+ div(
97
+ { className: "content-columns" },
98
+ // Core skills
99
+ div(
100
+ {},
101
+ heading3(
102
+ {},
103
+ span({ className: "badge badge-primary" }, "Core Skills"),
104
+ ),
105
+ view.coreSkills.length > 0
106
+ ? ul(
107
+ { className: "related-list" },
108
+ ...view.coreSkills.map((s) =>
109
+ li(
110
+ {},
111
+ showBackLink
112
+ ? a({ href: `#/skill/${s.id}` }, s.name)
113
+ : span({}, s.name),
114
+ ),
115
+ ),
116
+ )
117
+ : p({ className: "text-muted" }, "None"),
118
+ ),
119
+ // Supporting skills
120
+ div(
121
+ {},
122
+ heading3(
123
+ {},
124
+ span({ className: "badge badge-secondary" }, "Supporting Skills"),
125
+ ),
126
+ view.supportingSkills.length > 0
127
+ ? ul(
128
+ { className: "related-list" },
129
+ ...view.supportingSkills.map((s) =>
130
+ li(
131
+ {},
132
+ showBackLink
133
+ ? a({ href: `#/skill/${s.id}` }, s.name)
134
+ : span({}, s.name),
135
+ ),
136
+ ),
137
+ )
138
+ : p({ className: "text-muted" }, "None"),
139
+ ),
140
+ // Broad skills
141
+ div(
142
+ {},
143
+ heading3(
144
+ {},
145
+ span({ className: "badge badge-broad" }, "Broad Skills"),
146
+ ),
147
+ view.broadSkills.length > 0
148
+ ? ul(
149
+ { className: "related-list" },
150
+ ...view.broadSkills.map((s) =>
151
+ li(
152
+ {},
153
+ showBackLink
154
+ ? a({ href: `#/skill/${s.id}` }, s.name)
155
+ : span({}, s.name),
156
+ ),
157
+ ),
158
+ )
159
+ : p({ className: "text-muted" }, "None"),
160
+ ),
161
+ ),
162
+ ),
163
+
164
+ // Behaviour modifiers
165
+ showBehaviourModifiers && view.behaviourModifiers.length > 0
166
+ ? div(
167
+ { className: "detail-section" },
168
+ heading2({ className: "section-title" }, "Behaviour Modifiers"),
169
+ ...view.behaviourModifiers.map((b) =>
170
+ div(
171
+ { className: "list-item" },
172
+ showBackLink
173
+ ? a({ href: `#/behaviour/${b.id}` }, b.name)
174
+ : span({}, b.name),
175
+ " ",
176
+ span(
177
+ {
178
+ className: `modifier ${b.modifier > 0 ? "modifier-positive" : b.modifier < 0 ? "modifier-negative" : "modifier-neutral"}`,
179
+ },
180
+ b.modifier > 0 ? `+${b.modifier}` : String(b.modifier),
181
+ ),
182
+ ),
183
+ ),
184
+ )
185
+ : null,
186
+ );
187
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Discipline formatting for markdown/CLI output
3
+ */
4
+
5
+ import { tableToMarkdown } from "../shared.js";
6
+ import { prepareDisciplinesList, prepareDisciplineDetail } from "./shared.js";
7
+
8
+ /**
9
+ * Format discipline list as markdown
10
+ * @param {Array} disciplines - Raw discipline entities
11
+ * @returns {string}
12
+ */
13
+ export function disciplineListToMarkdown(disciplines) {
14
+ const { items } = prepareDisciplinesList(disciplines);
15
+ const lines = ["# 📋 Disciplines", ""];
16
+
17
+ const rows = items.map((d) => [
18
+ d.name,
19
+ `${d.coreSkillsCount}C`,
20
+ `${d.supportingSkillsCount}S`,
21
+ `${d.broadSkillsCount}B`,
22
+ ]);
23
+
24
+ lines.push(
25
+ tableToMarkdown(["Discipline", "Core", "Supporting", "Broad"], rows),
26
+ );
27
+ lines.push("");
28
+
29
+ return lines.join("\n");
30
+ }
31
+
32
+ /**
33
+ * Format discipline detail as markdown
34
+ * @param {Object} discipline - Raw discipline entity
35
+ * @param {Object} context - Additional context
36
+ * @param {Array} context.skills - All skills
37
+ * @param {Array} context.behaviours - All behaviours
38
+ * @param {boolean} [context.showBehaviourModifiers=true] - Whether to show behaviour modifiers section
39
+ * @returns {string}
40
+ */
41
+ export function disciplineToMarkdown(
42
+ discipline,
43
+ { skills, behaviours, showBehaviourModifiers = true } = {},
44
+ ) {
45
+ const view = prepareDisciplineDetail(discipline, { skills, behaviours });
46
+ const lines = [`# 📋 ${view.name}`, "", view.description, ""];
47
+
48
+ // Core skills
49
+ if (view.coreSkills.length > 0) {
50
+ lines.push("## Core Skills", "");
51
+ for (const s of view.coreSkills) {
52
+ lines.push(`- ${s.name}`);
53
+ }
54
+ lines.push("");
55
+ }
56
+
57
+ // Supporting skills
58
+ if (view.supportingSkills.length > 0) {
59
+ lines.push("## Supporting Skills", "");
60
+ for (const s of view.supportingSkills) {
61
+ lines.push(`- ${s.name}`);
62
+ }
63
+ lines.push("");
64
+ }
65
+
66
+ // Broad skills
67
+ if (view.broadSkills.length > 0) {
68
+ lines.push("## Broad Skills", "");
69
+ for (const s of view.broadSkills) {
70
+ lines.push(`- ${s.name}`);
71
+ }
72
+ lines.push("");
73
+ }
74
+
75
+ // Behaviour modifiers
76
+ if (showBehaviourModifiers && view.behaviourModifiers.length > 0) {
77
+ lines.push("## Behaviour Modifiers", "");
78
+ const modifierRows = view.behaviourModifiers.map((b) => [
79
+ b.name,
80
+ b.modifier > 0 ? `+${b.modifier}` : `${b.modifier}`,
81
+ ]);
82
+ lines.push(tableToMarkdown(["Behaviour", "Modifier"], modifierRows));
83
+ lines.push("");
84
+ }
85
+
86
+ return lines.join("\n");
87
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Discipline presentation helpers
3
+ *
4
+ * Shared utilities for formatting discipline 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
+ * Get discipline display name
22
+ * @param {Object} discipline
23
+ * @returns {string}
24
+ */
25
+ export function getDisciplineDisplayName(discipline) {
26
+ return discipline.specialization || discipline.name || discipline.id;
27
+ }
28
+
29
+ /**
30
+ * @typedef {Object} DisciplineListItem
31
+ * @property {string} id
32
+ * @property {string} name
33
+ * @property {string} description
34
+ * @property {string} truncatedDescription
35
+ * @property {number} coreSkillsCount
36
+ * @property {number} supportingSkillsCount
37
+ * @property {number} broadSkillsCount
38
+ */
39
+
40
+ /**
41
+ * Transform disciplines for list view
42
+ * @param {Array} disciplines - Raw discipline entities
43
+ * @param {number} [descriptionLimit=120] - Maximum description length
44
+ * @returns {{ items: DisciplineListItem[] }}
45
+ */
46
+ export function prepareDisciplinesList(disciplines, descriptionLimit = 120) {
47
+ const items = disciplines.map((discipline) => ({
48
+ id: discipline.id,
49
+ name: getDisciplineDisplayName(discipline),
50
+ description: discipline.description,
51
+ truncatedDescription: truncate(discipline.description, descriptionLimit),
52
+ coreSkillsCount: discipline.coreSkills?.length || 0,
53
+ supportingSkillsCount: discipline.supportingSkills?.length || 0,
54
+ broadSkillsCount: discipline.broadSkills?.length || 0,
55
+ }));
56
+
57
+ return { items };
58
+ }
59
+
60
+ /**
61
+ * @typedef {Object} SkillReference
62
+ * @property {string} id
63
+ * @property {string} name
64
+ */
65
+
66
+ /**
67
+ * @typedef {Object} BehaviourModifier
68
+ * @property {string} id
69
+ * @property {string} name
70
+ * @property {number} modifier
71
+ */
72
+
73
+ /**
74
+ * @typedef {Object} DisciplineDetailView
75
+ * @property {string} id
76
+ * @property {string} name
77
+ * @property {string} description
78
+ * @property {SkillReference[]} coreSkills
79
+ * @property {SkillReference[]} supportingSkills
80
+ * @property {SkillReference[]} broadSkills
81
+ * @property {BehaviourModifier[]} behaviourModifiers
82
+ */
83
+
84
+ /**
85
+ * Transform discipline for detail view
86
+ * @param {Object} discipline - Raw discipline entity
87
+ * @param {Object} context - Additional context
88
+ * @param {Array} context.skills - All skills
89
+ * @param {Array} context.behaviours - All behaviours
90
+ * @returns {DisciplineDetailView|null}
91
+ */
92
+ export function prepareDisciplineDetail(discipline, { skills, behaviours }) {
93
+ if (!discipline) return null;
94
+
95
+ const coreSkills = getItemsByIds(skills, discipline.coreSkills).map((s) => ({
96
+ id: s.id,
97
+ name: s.name,
98
+ }));
99
+
100
+ const supportingSkills = getItemsByIds(
101
+ skills,
102
+ discipline.supportingSkills,
103
+ ).map((s) => ({ id: s.id, name: s.name }));
104
+
105
+ const broadSkills = getItemsByIds(skills, discipline.broadSkills).map(
106
+ (s) => ({ id: s.id, name: s.name }),
107
+ );
108
+
109
+ const behaviourModifiers = discipline.behaviourModifiers
110
+ ? Object.entries(discipline.behaviourModifiers).map(
111
+ ([behaviourId, modifier]) => {
112
+ const behaviour = behaviours.find((b) => b.id === behaviourId);
113
+ return {
114
+ id: behaviourId,
115
+ name: behaviour?.name || behaviourId,
116
+ modifier,
117
+ };
118
+ },
119
+ )
120
+ : [];
121
+
122
+ return {
123
+ id: discipline.id,
124
+ name: getDisciplineDisplayName(discipline),
125
+ description: discipline.description,
126
+ coreSkills,
127
+ supportingSkills,
128
+ broadSkills,
129
+ behaviourModifiers,
130
+ };
131
+ }