@forwardimpact/pathway 0.1.0 → 0.3.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 (140) hide show
  1. package/app/commands/agent.js +119 -31
  2. package/app/commands/command-factory.js +3 -3
  3. package/app/commands/interview.js +14 -7
  4. package/app/commands/job.js +52 -33
  5. package/app/commands/progress.js +14 -7
  6. package/app/commands/serve.js +5 -0
  7. package/app/commands/stage.js +0 -10
  8. package/app/commands/track.js +5 -8
  9. package/app/components/builder.js +117 -30
  10. package/app/css/components/surfaces.css +16 -0
  11. package/app/formatters/agent/profile.js +30 -115
  12. package/app/formatters/agent/skill.js +23 -44
  13. package/app/formatters/behaviour/dom.js +3 -0
  14. package/app/formatters/behaviour/microdata.js +106 -0
  15. package/app/formatters/discipline/dom.js +28 -1
  16. package/app/formatters/discipline/microdata.js +117 -0
  17. package/app/formatters/discipline/shared.js +49 -8
  18. package/app/formatters/driver/dom.js +3 -0
  19. package/app/formatters/driver/microdata.js +91 -0
  20. package/app/formatters/grade/dom.js +5 -4
  21. package/app/formatters/grade/microdata.js +151 -0
  22. package/app/formatters/index.js +32 -1
  23. package/app/formatters/interview/shared.js +13 -8
  24. package/app/formatters/job/description.js +70 -81
  25. package/app/formatters/job/dom.js +40 -113
  26. package/app/formatters/job/markdown.js +17 -13
  27. package/app/formatters/json-ld.js +242 -0
  28. package/app/formatters/microdata-shared.js +184 -0
  29. package/app/formatters/progress/shared.js +14 -11
  30. package/app/formatters/shared.js +7 -2
  31. package/app/formatters/skill/dom.js +3 -0
  32. package/app/formatters/skill/microdata.js +151 -0
  33. package/app/formatters/stage/dom.js +3 -18
  34. package/app/formatters/stage/microdata.js +110 -0
  35. package/app/formatters/stage/shared.js +0 -27
  36. package/app/formatters/track/dom.js +5 -30
  37. package/app/formatters/track/markdown.js +2 -25
  38. package/app/formatters/track/microdata.js +111 -0
  39. package/app/formatters/track/shared.js +6 -58
  40. package/app/handout-main.js +26 -12
  41. package/app/handout.html +7 -0
  42. package/app/index.html +11 -0
  43. package/app/lib/card-mappers.js +17 -12
  44. package/app/lib/form-controls.js +64 -1
  45. package/app/lib/job-cache.js +12 -9
  46. package/app/lib/render.js +8 -1
  47. package/app/lib/template-loader.js +75 -0
  48. package/app/lib/yaml-loader.js +25 -8
  49. package/app/main.js +8 -4
  50. package/app/model/agent.js +158 -130
  51. package/app/model/checklist.js +57 -91
  52. package/app/model/derivation.js +135 -68
  53. package/app/model/index-generator.js +1 -7
  54. package/app/model/job.js +19 -13
  55. package/app/model/levels.js +20 -12
  56. package/app/model/loader.js +41 -17
  57. package/app/model/matching.js +33 -3
  58. package/app/model/profile.js +38 -45
  59. package/app/model/schema-validation.js +438 -0
  60. package/app/model/validation.js +747 -68
  61. package/app/pages/agent-builder.js +125 -28
  62. package/app/pages/assessment-results.js +10 -4
  63. package/app/pages/discipline.js +36 -6
  64. package/app/pages/driver.js +9 -47
  65. package/app/pages/interview-builder.js +3 -1
  66. package/app/pages/interview.js +15 -4
  67. package/app/pages/job-builder.js +4 -1
  68. package/app/pages/job.js +43 -8
  69. package/app/pages/landing.js +10 -10
  70. package/app/pages/progress-builder.js +3 -1
  71. package/app/pages/progress.js +78 -26
  72. package/app/pages/self-assessment.js +3 -3
  73. package/app/pages/stage.js +3 -126
  74. package/app/slide-main.js +45 -17
  75. package/app/slides/index.js +3 -1
  76. package/app/slides/overview.js +40 -4
  77. package/app/slides/progress.js +4 -2
  78. package/app/slides.html +7 -0
  79. package/bin/pathway.js +28 -75
  80. package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
  81. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
  82. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
  83. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
  84. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
  85. package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
  86. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
  87. package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
  88. package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
  89. package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
  90. package/examples/agents/.vscode/settings.json +1 -1
  91. package/examples/behaviours/outcome_ownership.yaml +1 -2
  92. package/examples/behaviours/polymathic_knowledge.yaml +1 -2
  93. package/examples/behaviours/precise_communication.yaml +1 -2
  94. package/examples/behaviours/relentless_curiosity.yaml +1 -2
  95. package/examples/behaviours/systems_thinking.yaml +1 -2
  96. package/examples/capabilities/business.yaml +80 -142
  97. package/examples/capabilities/delivery.yaml +155 -219
  98. package/examples/capabilities/people.yaml +2 -34
  99. package/examples/capabilities/reliability.yaml +161 -80
  100. package/examples/capabilities/scale.yaml +234 -252
  101. package/examples/copilot-setup-steps.yaml +25 -0
  102. package/examples/devcontainer.yaml +21 -0
  103. package/examples/disciplines/_index.yaml +1 -0
  104. package/examples/disciplines/data_engineering.yaml +14 -12
  105. package/examples/disciplines/engineering_management.yaml +63 -0
  106. package/examples/disciplines/software_engineering.yaml +14 -12
  107. package/examples/drivers.yaml +1 -4
  108. package/examples/framework.yaml +1 -2
  109. package/examples/grades.yaml +14 -15
  110. package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
  111. package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
  112. package/examples/questions/behaviours/precise_communication.yaml +1 -2
  113. package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
  114. package/examples/questions/behaviours/systems_thinking.yaml +1 -2
  115. package/examples/questions/skills/architecture_design.yaml +1 -2
  116. package/examples/questions/skills/cloud_platforms.yaml +1 -2
  117. package/examples/questions/skills/code_quality.yaml +1 -2
  118. package/examples/questions/skills/data_modeling.yaml +1 -2
  119. package/examples/questions/skills/devops.yaml +1 -2
  120. package/examples/questions/skills/full_stack_development.yaml +1 -2
  121. package/examples/questions/skills/sre_practices.yaml +1 -2
  122. package/examples/questions/skills/stakeholder_management.yaml +1 -2
  123. package/examples/questions/skills/team_collaboration.yaml +1 -2
  124. package/examples/questions/skills/technical_writing.yaml +1 -2
  125. package/examples/self-assessments.yaml +1 -3
  126. package/examples/stages.yaml +101 -46
  127. package/examples/tracks/_index.yaml +0 -1
  128. package/examples/tracks/platform.yaml +8 -13
  129. package/examples/tracks/sre.yaml +8 -18
  130. package/examples/vscode-settings.yaml +2 -7
  131. package/package.json +9 -3
  132. package/templates/agent.template.md +65 -0
  133. package/templates/job.template.md +47 -0
  134. package/templates/skill.template.md +28 -0
  135. package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
  136. package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
  137. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
  138. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
  139. package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
  140. package/examples/tracks/manager.yaml +0 -53
@@ -12,10 +12,15 @@ import {
12
12
  button,
13
13
  label,
14
14
  section,
15
+ select,
16
+ option,
15
17
  } from "../lib/render.js";
16
18
  import { getState } from "../lib/state.js";
17
19
  import { createBadge } from "./card.js";
18
- import { createSelectWithValue } from "../lib/form-controls.js";
20
+ import {
21
+ createSelectWithValue,
22
+ createDisciplineSelect,
23
+ } from "../lib/form-controls.js";
19
24
  import { createReactive } from "../lib/reactive.js";
20
25
 
21
26
  /**
@@ -89,9 +94,70 @@ export function createBuilder({
89
94
  buttonText,
90
95
  );
91
96
 
97
+ // Track select element - created once, options updated when discipline changes
98
+ const trackSelectEl = select(
99
+ { className: "form-select", id: "track-select" },
100
+ option({ value: "" }, "Generalist"),
101
+ );
102
+ // Initially disabled until discipline is selected
103
+ trackSelectEl.disabled = true;
104
+
105
+ /**
106
+ * Check if a discipline allows trackless (generalist) jobs
107
+ * @param {Object|null} disciplineObj
108
+ * @returns {boolean}
109
+ */
110
+ function allowsTrackless(disciplineObj) {
111
+ if (!disciplineObj) return false;
112
+ const validTracks = disciplineObj.validTracks ?? [];
113
+ // Empty array = trackless only (legacy), or null in array = trackless allowed
114
+ return validTracks.length === 0 || validTracks.includes(null);
115
+ }
116
+
117
+ /**
118
+ * Get available tracks for a discipline (excludes null entries)
119
+ * @param {Object|null} disciplineObj
120
+ * @returns {Array}
121
+ */
122
+ function getAvailableTracks(disciplineObj) {
123
+ if (!disciplineObj) return [];
124
+ const validTracks = disciplineObj.validTracks ?? [];
125
+ if (validTracks.length === 0) return [];
126
+ // Filter to actual track IDs (exclude null which means "trackless")
127
+ const trackIds = validTracks.filter((t) => t !== null);
128
+ return data.tracks.filter((t) => trackIds.includes(t.id));
129
+ }
130
+
131
+ /**
132
+ * Update track select options based on selected discipline
133
+ * @param {string} disciplineId
134
+ */
135
+ function updateTrackOptions(disciplineId) {
136
+ const disciplineObj = data.disciplines.find((d) => d.id === disciplineId);
137
+ const availableTracks = getAvailableTracks(disciplineObj);
138
+ const canBeTrackless = allowsTrackless(disciplineObj);
139
+
140
+ // Clear existing options
141
+ trackSelectEl.innerHTML = "";
142
+
143
+ // Add generalist option if trackless is allowed
144
+ if (canBeTrackless) {
145
+ trackSelectEl.appendChild(option({ value: "" }, "Generalist"));
146
+ }
147
+
148
+ // Add available track options
149
+ availableTracks.forEach((t) => {
150
+ trackSelectEl.appendChild(option({ value: t.id }, t.name));
151
+ });
152
+
153
+ // Disable if no options (neither trackless nor tracks)
154
+ trackSelectEl.disabled = !canBeTrackless && availableTracks.length === 0;
155
+ }
156
+
92
157
  // Subscribe to selection changes - all updates happen here
93
158
  selection.subscribe(({ discipline, track, grade }) => {
94
- if (!discipline || !track || !grade) {
159
+ // Track is now optional - only discipline and grade are required
160
+ if (!discipline || !grade) {
95
161
  previewContainer.innerHTML = "";
96
162
  previewContainer.appendChild(
97
163
  p({ className: "text-muted" }, emptyPreviewText),
@@ -101,10 +167,10 @@ export function createBuilder({
101
167
  }
102
168
 
103
169
  const disciplineObj = data.disciplines.find((d) => d.id === discipline);
104
- const trackObj = data.tracks.find((t) => t.id === track);
170
+ const trackObj = track ? data.tracks.find((t) => t.id === track) : null;
105
171
  const gradeObj = data.grades.find((g) => g.id === grade);
106
172
 
107
- if (!disciplineObj || !trackObj || !gradeObj) {
173
+ if (!disciplineObj || !gradeObj) {
108
174
  previewContainer.innerHTML = "";
109
175
  previewContainer.appendChild(
110
176
  p({ className: "text-muted" }, "Invalid selection. Please try again."),
@@ -146,37 +212,29 @@ export function createBuilder({
146
212
  h2({}, formTitle),
147
213
  div(
148
214
  { className: "auto-grid-sm gap-lg" },
149
- // Discipline selector
215
+ // Discipline selector (first)
150
216
  div(
151
217
  { className: "form-group" },
152
218
  label({ className: "form-label" }, labels.discipline || "Discipline"),
153
- createSelectWithValue({
219
+ createDisciplineSelect({
154
220
  id: "discipline-select",
155
- items: data.disciplines,
221
+ disciplines: data.disciplines,
156
222
  initialValue: selection.get().discipline,
157
223
  placeholder: "Select a discipline...",
158
224
  onChange: (value) => {
159
- selection.update((prev) => ({ ...prev, discipline: value }));
225
+ // Update track options when discipline changes
226
+ updateTrackOptions(value);
227
+ // Reset track selection when discipline changes
228
+ selection.update((prev) => ({
229
+ ...prev,
230
+ discipline: value,
231
+ track: "",
232
+ }));
160
233
  },
161
234
  getDisplayName: (d) => d.specialization || d.name,
162
235
  }),
163
236
  ),
164
- // Track selector
165
- div(
166
- { className: "form-group" },
167
- label({ className: "form-label" }, labels.track || "Track"),
168
- createSelectWithValue({
169
- id: "track-select",
170
- items: data.tracks,
171
- initialValue: selection.get().track,
172
- placeholder: "Select a track...",
173
- onChange: (value) => {
174
- selection.update((prev) => ({ ...prev, track: value }));
175
- },
176
- getDisplayName: (t) => t.name,
177
- }),
178
- ),
179
- // Grade selector
237
+ // Grade selector (second)
180
238
  div(
181
239
  { className: "form-group" },
182
240
  label({ className: "form-label" }, labels.grade || "Grade"),
@@ -191,6 +249,31 @@ export function createBuilder({
191
249
  getDisplayName: (g) => g.id,
192
250
  }),
193
251
  ),
252
+ // Track selector (third, optional)
253
+ div(
254
+ { className: "form-group" },
255
+ label(
256
+ { className: "form-label" },
257
+ labels.track || "Track (optional)",
258
+ ),
259
+ (() => {
260
+ // Wire up track select change handler
261
+ trackSelectEl.addEventListener("change", (e) => {
262
+ selection.update((prev) => ({ ...prev, track: e.target.value }));
263
+ });
264
+ // Initialize track options if discipline is pre-selected
265
+ const initialDiscipline = selection.get().discipline;
266
+ if (initialDiscipline) {
267
+ updateTrackOptions(initialDiscipline);
268
+ // Set initial track value if provided
269
+ const initialTrack = selection.get().track;
270
+ if (initialTrack) {
271
+ trackSelectEl.value = initialTrack;
272
+ }
273
+ }
274
+ return trackSelectEl;
275
+ })(),
276
+ ),
194
277
  ),
195
278
  previewContainer,
196
279
  div({ className: "page-actions" }, actionButton),
@@ -289,6 +372,15 @@ export function createProgressPreview(preview, selection) {
289
372
 
290
373
  const { discipline, grade, track } = selection;
291
374
 
375
+ // Build badges array - track is optional
376
+ const badges = [
377
+ createBadge(discipline.specialization, "discipline"),
378
+ createBadge(grade.id, "grade"),
379
+ ];
380
+ if (track) {
381
+ badges.push(createBadge(track.name, "track"));
382
+ }
383
+
292
384
  return div(
293
385
  { className: "job-preview-content" },
294
386
  div(
@@ -296,12 +388,7 @@ export function createProgressPreview(preview, selection) {
296
388
  div({ className: "preview-label" }, "Current Role"),
297
389
  div({ className: "preview-title" }, preview.title),
298
390
  ),
299
- div(
300
- { className: "preview-badges" },
301
- createBadge(discipline.specialization, "discipline"),
302
- createBadge(grade.id, "grade"),
303
- createBadge(track.name, "track"),
304
- ),
391
+ div({ className: "preview-badges" }, ...badges),
305
392
  div(
306
393
  { className: "preview-section", style: "margin-top: 1rem" },
307
394
  div({ className: "preview-label" }, "Progression Paths Available"),
@@ -240,4 +240,20 @@
240
240
  #app-footer p {
241
241
  margin: 0 0 var(--space-sm) 0;
242
242
  }
243
+
244
+ .footer-links {
245
+ display: flex;
246
+ gap: var(--space-md);
247
+ justify-content: center;
248
+ }
249
+
250
+ .footer-links a {
251
+ color: var(--color-text-muted);
252
+ text-decoration: none;
253
+ }
254
+
255
+ .footer-links a:hover {
256
+ color: var(--color-primary);
257
+ text-decoration: underline;
258
+ }
243
259
  }
@@ -3,131 +3,46 @@
3
3
  *
4
4
  * Formats agent profile data into .agent.md file content
5
5
  * following the GitHub Copilot Custom Agents specification.
6
+ *
7
+ * Uses Mustache templates for flexible output formatting.
8
+ * Templates are loaded from data/ directory with fallback to templates/ directory.
6
9
  */
7
10
 
8
- /**
9
- * Format YAML frontmatter value
10
- * @param {any} value - Value to format
11
- * @returns {string} Formatted value
12
- */
13
- function formatYamlValue(value) {
14
- if (Array.isArray(value)) {
15
- return JSON.stringify(value);
16
- }
17
- if (typeof value === "boolean") {
18
- return String(value);
19
- }
20
- if (typeof value === "string") {
21
- // Quote strings that contain special characters or newlines
22
- if (value.includes("\n") || value.includes(":") || value.includes("#")) {
23
- return `"${value.replace(/"/g, '\\"')}"`;
24
- }
25
- return value;
26
- }
27
- return String(value);
28
- }
29
-
30
- /**
31
- * Format handoffs array as YAML
32
- * @param {Array} handoffs - Array of handoff objects
33
- * @returns {string[]} YAML lines for handoffs
34
- */
35
- function formatHandoffs(handoffs) {
36
- const lines = ["handoffs:"];
37
- for (const handoff of handoffs) {
38
- lines.push(` - label: ${formatYamlValue(handoff.label)}`);
39
- if (handoff.agent) {
40
- lines.push(` agent: ${formatYamlValue(handoff.agent)}`);
41
- }
42
-
43
- // Format prompt as single-line string, replacing newlines with spaces
44
- const singleLinePrompt = handoff.prompt
45
- .replace(/\n\n+/g, " ")
46
- .replace(/\n/g, " ")
47
- .replace(/\s+/g, " ")
48
- .trim();
49
- lines.push(` prompt: ${formatYamlValue(singleLinePrompt)}`);
50
-
51
- if (handoff.send !== undefined) {
52
- lines.push(` send: ${formatYamlValue(handoff.send)}`);
53
- }
54
- }
55
- return lines;
56
- }
11
+ import Mustache from "mustache";
57
12
 
58
13
  /**
59
- * Format agent profile as .agent.md file content
60
- * @param {Object} profile - Profile with frontmatter and body
14
+ * Format agent profile as .agent.md file content using Mustache template
15
+ * @param {Object} profile - Profile with frontmatter and bodyData
61
16
  * @param {Object} profile.frontmatter - YAML frontmatter data
62
17
  * @param {string} profile.frontmatter.name - Agent name
63
18
  * @param {string} profile.frontmatter.description - Agent description
64
19
  * @param {string[]} profile.frontmatter.tools - Available tools
65
20
  * @param {boolean} profile.frontmatter.infer - Whether to auto-select
66
21
  * @param {Array} [profile.frontmatter.handoffs] - Handoff definitions
67
- * @param {string} profile.body - Markdown body content
22
+ * @param {Object} profile.bodyData - Structured body data
23
+ * @param {string} profile.bodyData.title - Agent title (e.g. "Software Engineering - Platform - Plan Agent")
24
+ * @param {string} profile.bodyData.stageDescription - Stage description text
25
+ * @param {string} profile.bodyData.identity - Core identity text
26
+ * @param {string} [profile.bodyData.priority] - Priority/philosophy statement (optional)
27
+ * @param {string[]} profile.bodyData.capabilities - List of capability names
28
+ * @param {Array<{index: number, text: string}>} profile.bodyData.beforeMakingChanges - Numbered steps
29
+ * @param {string} [profile.bodyData.delegation] - Delegation guidance (optional)
30
+ * @param {string} profile.bodyData.operationalContext - Operational context text
31
+ * @param {string} profile.bodyData.workingStyle - Working style markdown section
32
+ * @param {string} [profile.bodyData.beforeHandoff] - Before handoff checklist markdown (optional)
33
+ * @param {string[]} profile.bodyData.constraints - List of constraints
34
+ * @param {string} template - Mustache template string
68
35
  * @returns {string} Complete .agent.md file content
69
36
  */
70
- export function formatAgentProfile({ frontmatter, body }) {
71
- const lines = ["---"];
72
-
73
- // Name (optional but recommended)
74
- if (frontmatter.name) {
75
- lines.push(`name: ${formatYamlValue(frontmatter.name)}`);
76
- }
77
-
78
- // Description (required)
79
- lines.push(`description: ${formatYamlValue(frontmatter.description)}`);
80
-
81
- // Tools (optional, defaults to all)
82
- if (frontmatter.tools && frontmatter.tools.length > 0) {
83
- lines.push(`tools: ${formatYamlValue(frontmatter.tools)}`);
84
- }
85
-
86
- // Infer (optional)
87
- if (frontmatter.infer !== undefined) {
88
- lines.push(`infer: ${formatYamlValue(frontmatter.infer)}`);
89
- }
90
-
91
- // Handoffs (optional)
92
- if (frontmatter.handoffs && frontmatter.handoffs.length > 0) {
93
- lines.push(...formatHandoffs(frontmatter.handoffs));
94
- }
95
-
96
- lines.push("---");
97
- lines.push("");
98
- lines.push(body);
99
-
100
- return lines.join("\n");
101
- }
102
-
103
- /**
104
- * Format agent profile for CLI output (markdown)
105
- * @param {Object} profile - Profile with frontmatter and body
106
- * @returns {string} Markdown formatted for CLI display
107
- */
108
- export function formatAgentProfileForCli({ frontmatter, body }) {
109
- const lines = [];
110
-
111
- lines.push(`# Agent Profile: ${frontmatter.name}`);
112
- lines.push("");
113
- lines.push(`**Description:** ${frontmatter.description}`);
114
- lines.push("");
115
- lines.push(`**Tools:** ${frontmatter.tools.join(", ")}`);
116
- lines.push(`**Infer:** ${frontmatter.infer}`);
117
-
118
- if (frontmatter.handoffs && frontmatter.handoffs.length > 0) {
119
- lines.push("");
120
- lines.push("**Handoffs:**");
121
- for (const handoff of frontmatter.handoffs) {
122
- const target = handoff.agent ? ` → ${handoff.agent}` : " (self)";
123
- lines.push(` - ${handoff.label}${target}`);
124
- }
125
- }
126
-
127
- lines.push("");
128
- lines.push("---");
129
- lines.push("");
130
- lines.push(body);
131
-
132
- return lines.join("\n");
37
+ export function formatAgentProfile({ frontmatter, bodyData }, template) {
38
+ const data = {
39
+ // Frontmatter
40
+ name: frontmatter.name,
41
+ description: frontmatter.description,
42
+ infer: frontmatter.infer,
43
+ handoffs: frontmatter.handoffs || [],
44
+ // Body data
45
+ ...bodyData,
46
+ };
47
+ return Mustache.render(template, data);
133
48
  }
@@ -3,56 +3,35 @@
3
3
  *
4
4
  * Formats agent skill data into SKILL.md file content
5
5
  * following the Agent Skills Standard specification.
6
+ *
7
+ * Uses Mustache templates for flexible output formatting.
8
+ * Templates are loaded from data/ directory with fallback to templates/ directory.
6
9
  */
7
10
 
11
+ import Mustache from "mustache";
12
+
8
13
  /**
9
- * Format agent skill as SKILL.md file content
10
- * @param {Object} skill - Skill with frontmatter and body
14
+ * Format agent skill as SKILL.md file content using Mustache template
15
+ * @param {Object} skill - Skill with frontmatter, title, stages, reference
11
16
  * @param {Object} skill.frontmatter - YAML frontmatter data
12
17
  * @param {string} skill.frontmatter.name - Skill name (required)
13
18
  * @param {string} skill.frontmatter.description - Skill description (required)
14
- * @param {string} skill.body - Markdown body content
19
+ * @param {string} skill.title - Human-readable skill title for heading
20
+ * @param {Array} skill.stages - Array of stage objects with stageName, focus, activities, ready
21
+ * @param {string} skill.reference - Reference content (markdown)
22
+ * @param {string} template - Mustache template string
15
23
  * @returns {string} Complete SKILL.md file content
16
24
  */
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");
25
+ export function formatAgentSkill(
26
+ { frontmatter, title, stages, reference },
27
+ template,
28
+ ) {
29
+ const data = {
30
+ name: frontmatter.name,
31
+ descriptionLines: frontmatter.description.trim().split("\n"),
32
+ title,
33
+ stages,
34
+ reference: reference ? reference.trim() : "",
35
+ };
36
+ return Mustache.render(template, data);
58
37
  }
@@ -23,6 +23,7 @@ import {
23
23
  getConceptEmoji,
24
24
  } from "../../model/levels.js";
25
25
  import { prepareBehaviourDetail } from "./shared.js";
26
+ import { createJsonLdScript, behaviourToJsonLd } from "../json-ld.js";
26
27
 
27
28
  /**
28
29
  * Format behaviour detail as DOM elements
@@ -41,6 +42,8 @@ export function behaviourToDOM(
41
42
  const emoji = getConceptEmoji(framework, "behaviour");
42
43
  return div(
43
44
  { className: "detail-page behaviour-detail" },
45
+ // JSON-LD structured data
46
+ createJsonLdScript(behaviourToJsonLd(behaviour)),
44
47
  // Header
45
48
  div(
46
49
  { className: "page-header" },
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Behaviour formatting for microdata HTML output
3
+ *
4
+ * Generates clean, class-less HTML with microdata aligned with behaviour.schema.json
5
+ * RDF vocab: https://schema.forwardimpact.team/rdf/
6
+ */
7
+
8
+ import {
9
+ openTag,
10
+ prop,
11
+ propRaw,
12
+ metaTag,
13
+ section,
14
+ dl,
15
+ ul,
16
+ escapeHtml,
17
+ formatLevelName,
18
+ htmlDocument,
19
+ } from "../microdata-shared.js";
20
+ import { prepareBehavioursList, prepareBehaviourDetail } from "./shared.js";
21
+
22
+ /**
23
+ * Format behaviour list as microdata HTML
24
+ * @param {Array} behaviours - Raw behaviour entities
25
+ * @returns {string} HTML with microdata
26
+ */
27
+ export function behaviourListToMicrodata(behaviours) {
28
+ const { items } = prepareBehavioursList(behaviours);
29
+
30
+ const content = items
31
+ .map(
32
+ (
33
+ behaviour,
34
+ ) => `${openTag("article", { itemtype: "Behaviour", itemid: `#${behaviour.id}` })}
35
+ ${prop("h2", "name", behaviour.name)}
36
+ ${prop("p", "description", behaviour.truncatedDescription)}
37
+ </article>`,
38
+ )
39
+ .join("\n");
40
+
41
+ return htmlDocument(
42
+ "Behaviours",
43
+ `<main>
44
+ <h1>Behaviours</h1>
45
+ ${content}
46
+ </main>`,
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Format behaviour detail as microdata HTML
52
+ * @param {Object} behaviour - Raw behaviour entity
53
+ * @param {Object} context - Additional context
54
+ * @param {Array} context.drivers - All drivers
55
+ * @returns {string} HTML with microdata
56
+ */
57
+ export function behaviourToMicrodata(behaviour, { drivers }) {
58
+ const view = prepareBehaviourDetail(behaviour, { drivers });
59
+
60
+ if (!view) return "";
61
+
62
+ const sections = [];
63
+
64
+ // Maturity descriptions - uses MaturityDescriptions itemtype
65
+ const maturityPairs = Object.entries(view.maturityDescriptions).map(
66
+ ([maturity, desc]) => ({
67
+ term: formatLevelName(maturity),
68
+ definition: desc,
69
+ itemprop: `${maturity.replace(/_([a-z])/g, (_, c) => c.toUpperCase())}Description`,
70
+ }),
71
+ );
72
+ sections.push(
73
+ section(
74
+ "Maturity Levels",
75
+ `${openTag("div", { itemtype: "MaturityDescriptions", itemprop: "maturityDescriptions" })}
76
+ ${dl(maturityPairs)}
77
+ </div>`,
78
+ 2,
79
+ ),
80
+ );
81
+
82
+ // Related drivers
83
+ if (view.relatedDrivers.length > 0) {
84
+ const driverItems = view.relatedDrivers.map(
85
+ (d) => `<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a>`,
86
+ );
87
+ sections.push(section("Linked to Drivers", ul(driverItems), 2));
88
+ }
89
+
90
+ const body = `<main>
91
+ ${openTag("article", { itemtype: "Behaviour", itemid: `#${view.id}` })}
92
+ ${prop("h1", "name", view.name)}
93
+ ${metaTag("id", view.id)}
94
+ ${propRaw(
95
+ "div",
96
+ "human",
97
+ `${openTag("div", { itemtype: "BehaviourHumanSection" })}
98
+ ${prop("p", "description", view.description)}
99
+ ${sections.join("\n")}
100
+ </div>`,
101
+ )}
102
+ </article>
103
+ </main>`;
104
+
105
+ return htmlDocument(view.name, body);
106
+ }
@@ -20,6 +20,24 @@ import {
20
20
  } from "../../components/action-buttons.js";
21
21
  import { getConceptEmoji } from "../../model/levels.js";
22
22
  import { prepareDisciplineDetail } from "./shared.js";
23
+ import { createJsonLdScript, disciplineToJsonLd } from "../json-ld.js";
24
+ import { createBadge } from "../../components/card.js";
25
+
26
+ /**
27
+ * Get type badges for discipline (Management/Professional)
28
+ * @param {Object} discipline - Raw discipline entity
29
+ * @returns {HTMLElement[]}
30
+ */
31
+ function getDisciplineTypeBadges(discipline) {
32
+ const badges = [];
33
+ if (discipline.isProfessional) {
34
+ badges.push(createBadge("Professional", "secondary"));
35
+ }
36
+ if (discipline.isManagement) {
37
+ badges.push(createBadge("Management", "primary"));
38
+ }
39
+ return badges;
40
+ }
23
41
 
24
42
  /**
25
43
  * Format discipline detail as DOM elements
@@ -44,15 +62,24 @@ export function disciplineToDOM(
44
62
  ) {
45
63
  const view = prepareDisciplineDetail(discipline, { skills, behaviours });
46
64
  const emoji = getConceptEmoji(framework, "discipline");
65
+ const typeBadges = getDisciplineTypeBadges(discipline);
47
66
  return div(
48
67
  { className: "detail-page discipline-detail" },
68
+ // JSON-LD structured data
69
+ createJsonLdScript(disciplineToJsonLd(discipline, { skills })),
49
70
  // Header
50
71
  div(
51
72
  { className: "page-header" },
52
73
  showBackLink
53
74
  ? createBackLink("/discipline", "← Back to Disciplines")
54
75
  : null,
55
- heading1({ className: "page-title" }, `${emoji} `, view.name),
76
+ div(
77
+ { className: "page-title-row" },
78
+ heading1({ className: "page-title" }, `${emoji} `, view.name),
79
+ typeBadges.length > 0
80
+ ? div({ className: "page-title-badges" }, ...typeBadges)
81
+ : null,
82
+ ),
56
83
  p({ className: "page-description" }, view.description),
57
84
  showBackLink
58
85
  ? div(