@forwardimpact/pathway 0.3.0 → 0.4.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.
- package/app/commands/agent.js +1 -1
- package/app/commands/index.js +4 -3
- package/app/commands/skill.js +56 -2
- package/app/commands/tool.js +112 -0
- package/app/components/checklist.js +6 -4
- package/app/components/markdown-textarea.js +132 -0
- package/app/css/components/forms.css +45 -0
- package/app/css/components/layout.css +12 -0
- package/app/css/components/surfaces.css +22 -0
- package/app/css/pages/detail.css +50 -0
- package/app/css/pages/job-builder.css +0 -42
- package/app/formatters/agent/profile.js +61 -9
- package/app/formatters/agent/skill.js +48 -6
- package/app/formatters/job/description.js +21 -16
- package/app/formatters/job/dom.js +9 -70
- package/app/formatters/shared.js +58 -0
- package/app/formatters/skill/dom.js +57 -2
- package/app/formatters/skill/markdown.js +18 -0
- package/app/formatters/skill/shared.js +12 -4
- package/app/formatters/stage/microdata.js +1 -1
- package/app/formatters/stage/shared.js +1 -1
- package/app/formatters/tool/shared.js +72 -0
- package/app/handout-main.js +7 -7
- package/app/index.html +10 -3
- package/app/lib/card-mappers.js +64 -17
- package/app/lib/render.js +4 -0
- package/app/lib/yaml-loader.js +12 -1
- package/app/main.js +4 -0
- package/app/model/agent.js +26 -18
- package/app/model/derivation.js +3 -3
- package/app/model/levels.js +2 -0
- package/app/model/loader.js +12 -1
- package/app/model/validation.js +74 -8
- package/app/pages/agent-builder.js +2 -2
- package/app/pages/landing.js +34 -14
- package/app/pages/self-assessment.js +7 -5
- package/app/pages/skill.js +5 -17
- package/app/pages/stage.js +10 -6
- package/app/pages/tool.js +50 -0
- package/app/slides/index.js +25 -25
- package/bin/pathway.js +31 -16
- package/examples/capabilities/business.yaml +17 -17
- package/examples/capabilities/delivery.yaml +51 -36
- package/examples/capabilities/reliability.yaml +127 -114
- package/examples/capabilities/scale.yaml +38 -36
- package/examples/disciplines/engineering_management.yaml +1 -1
- package/examples/framework.yaml +12 -0
- package/examples/grades.yaml +5 -7
- package/examples/self-assessments.yaml +1 -1
- package/package.json +1 -1
- package/templates/skill.template.md +31 -12
package/app/lib/card-mappers.js
CHANGED
|
@@ -9,6 +9,23 @@ import { createBadge } from "../components/card.js";
|
|
|
9
9
|
import { formatLevel } from "./render.js";
|
|
10
10
|
import { getCapabilityEmoji } from "../model/levels.js";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Create an external link element styled as a badge
|
|
14
|
+
* @param {string} text - Link text
|
|
15
|
+
* @param {string} url - External URL
|
|
16
|
+
* @returns {HTMLElement}
|
|
17
|
+
*/
|
|
18
|
+
function createExternalLink(text, url) {
|
|
19
|
+
const link = document.createElement("a");
|
|
20
|
+
link.href = url;
|
|
21
|
+
link.target = "_blank";
|
|
22
|
+
link.rel = "noopener noreferrer";
|
|
23
|
+
link.className = "badge badge-primary";
|
|
24
|
+
link.textContent = text;
|
|
25
|
+
link.addEventListener("click", (e) => e.stopPropagation()); // Don't trigger card click
|
|
26
|
+
return link;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
/**
|
|
13
30
|
* Map discipline to card config
|
|
14
31
|
* @param {Object} discipline
|
|
@@ -155,24 +172,54 @@ export function jobToCardConfig(job) {
|
|
|
155
172
|
}
|
|
156
173
|
|
|
157
174
|
/**
|
|
158
|
-
*
|
|
159
|
-
* @param {
|
|
175
|
+
* Map tool to card config
|
|
176
|
+
* @param {Object} tool - Aggregated tool with usages
|
|
177
|
+
* @param {Array} capabilities - Capability entities for emoji lookup
|
|
178
|
+
* @returns {Object}
|
|
179
|
+
*/
|
|
180
|
+
export function toolToCardConfig(tool, capabilities) {
|
|
181
|
+
// Create skills list as card content
|
|
182
|
+
const skillsList = createSkillsList(tool.usages, capabilities);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
title: tool.name,
|
|
186
|
+
description: tool.description,
|
|
187
|
+
// Docs link in header badges (upper right)
|
|
188
|
+
badges: tool.url ? [createExternalLink("Docs →", tool.url)] : [],
|
|
189
|
+
content: skillsList,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create an unordered list of skill links with capability emoji
|
|
195
|
+
* @param {Array} usages - Tool usage objects with skillId, skillName, capabilityId
|
|
196
|
+
* @param {Array} capabilities - Capability entities
|
|
197
|
+
* @returns {HTMLElement}
|
|
198
|
+
*/
|
|
199
|
+
function createSkillsList(usages, capabilities) {
|
|
200
|
+
const ul = document.createElement("ul");
|
|
201
|
+
ul.className = "tool-skills-list";
|
|
202
|
+
|
|
203
|
+
for (const usage of usages) {
|
|
204
|
+
const emoji = getCapabilityEmoji(capabilities, usage.capabilityId);
|
|
205
|
+
const li = document.createElement("li");
|
|
206
|
+
const link = document.createElement("a");
|
|
207
|
+
link.href = `#/skill/${usage.skillId}`;
|
|
208
|
+
link.textContent = `${emoji} ${usage.skillName}`;
|
|
209
|
+
li.appendChild(link);
|
|
210
|
+
ul.appendChild(li);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return ul;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Format capability for badge display (short, tag-like)
|
|
218
|
+
* @param {string} capabilityId
|
|
160
219
|
* @param {Array} capabilities
|
|
161
220
|
* @returns {string}
|
|
162
221
|
*/
|
|
163
|
-
function formatCapability(
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
scale: "Scale",
|
|
167
|
-
reliability: "Reliability",
|
|
168
|
-
data: "Data",
|
|
169
|
-
ai: "AI",
|
|
170
|
-
process: "Process",
|
|
171
|
-
business: "Business",
|
|
172
|
-
people: "People",
|
|
173
|
-
documentation: "Documentation",
|
|
174
|
-
};
|
|
175
|
-
const label = capabilityLabels[capability] || formatLevel(capability);
|
|
176
|
-
const emoji = getCapabilityEmoji(capabilities, capability);
|
|
177
|
-
return `${emoji} ${label}`;
|
|
222
|
+
function formatCapability(capabilityId, capabilities) {
|
|
223
|
+
const emoji = getCapabilityEmoji(capabilities, capabilityId);
|
|
224
|
+
return `${emoji} ${capabilityId.toUpperCase()}`;
|
|
178
225
|
}
|
package/app/lib/render.js
CHANGED
|
@@ -111,6 +111,10 @@ export const th = (attrs, ...children) =>
|
|
|
111
111
|
createElement("th", attrs, ...children);
|
|
112
112
|
export const td = (attrs, ...children) =>
|
|
113
113
|
createElement("td", attrs, ...children);
|
|
114
|
+
export const pre = (attrs, ...children) =>
|
|
115
|
+
createElement("pre", attrs, ...children);
|
|
116
|
+
export const code = (attrs, ...children) =>
|
|
117
|
+
createElement("code", attrs, ...children);
|
|
114
118
|
export const button = (attrs, ...children) =>
|
|
115
119
|
createElement("button", attrs, ...children);
|
|
116
120
|
export const input = (attrs) => createElement("input", attrs);
|
package/app/lib/yaml-loader.js
CHANGED
|
@@ -70,7 +70,15 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
|
|
|
70
70
|
|
|
71
71
|
if (capability.skills && Array.isArray(capability.skills)) {
|
|
72
72
|
for (const skill of capability.skills) {
|
|
73
|
-
const {
|
|
73
|
+
const {
|
|
74
|
+
id,
|
|
75
|
+
name,
|
|
76
|
+
isHumanOnly,
|
|
77
|
+
human,
|
|
78
|
+
agent,
|
|
79
|
+
implementationReference,
|
|
80
|
+
toolReferences,
|
|
81
|
+
} = skill;
|
|
74
82
|
allSkills.push({
|
|
75
83
|
id,
|
|
76
84
|
name,
|
|
@@ -80,6 +88,9 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
|
|
|
80
88
|
// Include isHumanOnly flag for agent filtering (defaults to false)
|
|
81
89
|
...(isHumanOnly && { isHumanOnly }),
|
|
82
90
|
...(agent && { agent }),
|
|
91
|
+
// Include implementation reference and tool references (shared by human and agent)
|
|
92
|
+
...(implementationReference && { implementationReference }),
|
|
93
|
+
...(toolReferences && { toolReferences }),
|
|
83
94
|
});
|
|
84
95
|
}
|
|
85
96
|
}
|
package/app/main.js
CHANGED
|
@@ -26,6 +26,7 @@ import { renderTracksList, renderTrackDetail } from "./pages/track.js";
|
|
|
26
26
|
import { renderGradesList, renderGradeDetail } from "./pages/grade.js";
|
|
27
27
|
import { renderDriversList, renderDriverDetail } from "./pages/driver.js";
|
|
28
28
|
import { renderStagesList, renderStageDetail } from "./pages/stage.js";
|
|
29
|
+
import { renderToolsList } from "./pages/tool.js";
|
|
29
30
|
import { renderJobBuilder } from "./pages/job-builder.js";
|
|
30
31
|
import { renderJobDetail } from "./pages/job.js";
|
|
31
32
|
import { renderInterviewPrep } from "./pages/interview-builder.js";
|
|
@@ -99,6 +100,9 @@ function setupRoutes() {
|
|
|
99
100
|
router.on("/stage", renderStagesList);
|
|
100
101
|
router.on("/stage/:id", renderStageDetail);
|
|
101
102
|
|
|
103
|
+
// Tool
|
|
104
|
+
router.on("/tool", renderToolsList);
|
|
105
|
+
|
|
102
106
|
// Job builder
|
|
103
107
|
router.on("/job-builder", renderJobBuilder);
|
|
104
108
|
router.on("/job/:discipline/:grade/:track", renderJobDetail);
|
package/app/model/agent.js
CHANGED
|
@@ -225,21 +225,13 @@ function buildWorkingStyleFromBehaviours(
|
|
|
225
225
|
return sections.join("\n");
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
/**
|
|
229
|
-
* Stage ID to display name and next stage mapping
|
|
230
|
-
*/
|
|
231
|
-
const STAGE_INFO = {
|
|
232
|
-
plan: { name: "Plan", nextStage: "Code" },
|
|
233
|
-
code: { name: "Code", nextStage: "Review" },
|
|
234
|
-
review: { name: "Review", nextStage: "Complete" },
|
|
235
|
-
};
|
|
236
|
-
|
|
237
228
|
/**
|
|
238
229
|
* Generate SKILL.md content from skill data
|
|
239
230
|
* @param {Object} skillData - Skill with agent section containing stages
|
|
231
|
+
* @param {Array} stages - All stage entities
|
|
240
232
|
* @returns {Object} Skill with frontmatter, title, stages array, reference, dirname
|
|
241
233
|
*/
|
|
242
|
-
export function generateSkillMd(skillData) {
|
|
234
|
+
export function generateSkillMd(skillData, stages) {
|
|
243
235
|
const { agent, name } = skillData;
|
|
244
236
|
|
|
245
237
|
if (!agent) {
|
|
@@ -250,17 +242,31 @@ export function generateSkillMd(skillData) {
|
|
|
250
242
|
throw new Error(`Skill ${skillData.id} agent section missing stages`);
|
|
251
243
|
}
|
|
252
244
|
|
|
245
|
+
// Build stage lookup map
|
|
246
|
+
const stageMap = new Map(stages.map((s) => [s.id, s]));
|
|
247
|
+
|
|
253
248
|
// Transform stages object to array for template rendering
|
|
254
249
|
const stagesArray = Object.entries(agent.stages).map(
|
|
255
250
|
([stageId, stageData]) => {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
251
|
+
const stageEntity = stageMap.get(stageId);
|
|
252
|
+
const stageName = stageEntity?.name || stageId;
|
|
253
|
+
|
|
254
|
+
// Find next stage from handoffs
|
|
255
|
+
let nextStageName = "Complete";
|
|
256
|
+
if (stageEntity?.handoffs) {
|
|
257
|
+
const nextHandoff = stageEntity.handoffs.find(
|
|
258
|
+
(h) => h.targetStage !== stageId,
|
|
259
|
+
);
|
|
260
|
+
if (nextHandoff) {
|
|
261
|
+
const nextStage = stageMap.get(nextHandoff.targetStage);
|
|
262
|
+
nextStageName = nextStage?.name || nextHandoff.targetStage;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
260
266
|
return {
|
|
261
267
|
stageId,
|
|
262
|
-
stageName
|
|
263
|
-
nextStageName
|
|
268
|
+
stageName,
|
|
269
|
+
nextStageName,
|
|
264
270
|
focus: stageData.focus,
|
|
265
271
|
activities: stageData.activities || [],
|
|
266
272
|
ready: stageData.ready || [],
|
|
@@ -277,11 +283,13 @@ export function generateSkillMd(skillData) {
|
|
|
277
283
|
return {
|
|
278
284
|
frontmatter: {
|
|
279
285
|
name: agent.name,
|
|
280
|
-
description: agent.description
|
|
286
|
+
description: agent.description,
|
|
287
|
+
useWhen: agent.useWhen || "",
|
|
281
288
|
},
|
|
282
289
|
title: name,
|
|
283
290
|
stages: stagesArray,
|
|
284
|
-
reference:
|
|
291
|
+
reference: skillData.implementationReference || "",
|
|
292
|
+
toolReferences: skillData.toolReferences || [],
|
|
285
293
|
dirname: agent.name,
|
|
286
294
|
};
|
|
287
295
|
}
|
package/app/model/derivation.js
CHANGED
|
@@ -376,17 +376,17 @@ export function isValidJobCombination({
|
|
|
376
376
|
* @returns {string} Generated job title
|
|
377
377
|
*/
|
|
378
378
|
export function generateJobTitle(discipline, grade, track = null) {
|
|
379
|
-
const { roleTitle,
|
|
379
|
+
const { roleTitle, isManagement } = discipline;
|
|
380
380
|
const { professionalTitle, managementTitle } = grade;
|
|
381
381
|
|
|
382
382
|
// Management discipline (no track needed)
|
|
383
383
|
if (isManagement && !track) {
|
|
384
|
-
return `${managementTitle}, ${
|
|
384
|
+
return `${managementTitle}, ${roleTitle}`;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
// Management discipline with track
|
|
388
388
|
if (isManagement && track) {
|
|
389
|
-
return `${managementTitle}, ${track.name}`;
|
|
389
|
+
return `${managementTitle}, ${roleTitle} – ${track.name}`;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
// IC discipline with track
|
package/app/model/levels.js
CHANGED
|
@@ -90,6 +90,7 @@ export const Capability = {
|
|
|
90
90
|
RELIABILITY: "reliability",
|
|
91
91
|
DATA: "data",
|
|
92
92
|
AI: "ai",
|
|
93
|
+
ML: "ml",
|
|
93
94
|
PROCESS: "process",
|
|
94
95
|
BUSINESS: "business",
|
|
95
96
|
PEOPLE: "people",
|
|
@@ -111,6 +112,7 @@ export const CAPABILITY_ORDER = [
|
|
|
111
112
|
Capability.DELIVERY,
|
|
112
113
|
Capability.DATA,
|
|
113
114
|
Capability.AI,
|
|
115
|
+
Capability.ML,
|
|
114
116
|
Capability.SCALE,
|
|
115
117
|
Capability.RELIABILITY,
|
|
116
118
|
Capability.PEOPLE,
|
package/app/model/loader.js
CHANGED
|
@@ -84,7 +84,15 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
|
|
|
84
84
|
|
|
85
85
|
if (capability.skills && Array.isArray(capability.skills)) {
|
|
86
86
|
for (const skill of capability.skills) {
|
|
87
|
-
const {
|
|
87
|
+
const {
|
|
88
|
+
id,
|
|
89
|
+
name,
|
|
90
|
+
isHumanOnly,
|
|
91
|
+
human,
|
|
92
|
+
agent,
|
|
93
|
+
implementationReference,
|
|
94
|
+
toolReferences,
|
|
95
|
+
} = skill;
|
|
88
96
|
allSkills.push({
|
|
89
97
|
id,
|
|
90
98
|
name,
|
|
@@ -95,6 +103,9 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
|
|
|
95
103
|
...(isHumanOnly && { isHumanOnly }),
|
|
96
104
|
// Preserve agent section for agent generation
|
|
97
105
|
...(agent && { agent }),
|
|
106
|
+
// Include implementation reference and tool references (shared by human and agent)
|
|
107
|
+
...(implementationReference && { implementationReference }),
|
|
108
|
+
...(toolReferences && { toolReferences }),
|
|
98
109
|
});
|
|
99
110
|
}
|
|
100
111
|
}
|
package/app/model/validation.js
CHANGED
|
@@ -241,17 +241,13 @@ function validateSkill(skill, index, requiredStageIds = []) {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
//
|
|
245
|
-
if (
|
|
246
|
-
skill.agent.reference !== undefined &&
|
|
247
|
-
typeof skill.agent.reference !== "string"
|
|
248
|
-
) {
|
|
244
|
+
// Error if old 'reference' field is still present (moved to skill.implementationReference)
|
|
245
|
+
if (skill.agent.reference !== undefined) {
|
|
249
246
|
errors.push(
|
|
250
247
|
createError(
|
|
251
|
-
"
|
|
252
|
-
"Skill agent reference
|
|
248
|
+
"INVALID_FIELD",
|
|
249
|
+
"Skill agent 'reference' field is not supported. Use skill.implementationReference instead.",
|
|
253
250
|
`${agentPath}.reference`,
|
|
254
|
-
skill.agent.reference,
|
|
255
251
|
),
|
|
256
252
|
);
|
|
257
253
|
}
|
|
@@ -295,6 +291,76 @@ function validateSkill(skill, index, requiredStageIds = []) {
|
|
|
295
291
|
}
|
|
296
292
|
}
|
|
297
293
|
|
|
294
|
+
// Validate implementationReference if present (optional string)
|
|
295
|
+
if (
|
|
296
|
+
skill.implementationReference !== undefined &&
|
|
297
|
+
typeof skill.implementationReference !== "string"
|
|
298
|
+
) {
|
|
299
|
+
errors.push(
|
|
300
|
+
createError(
|
|
301
|
+
"INVALID_VALUE",
|
|
302
|
+
"Skill implementationReference must be a string",
|
|
303
|
+
`${path}.implementationReference`,
|
|
304
|
+
skill.implementationReference,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Validate toolReferences array if present
|
|
310
|
+
if (skill.toolReferences !== undefined) {
|
|
311
|
+
if (!Array.isArray(skill.toolReferences)) {
|
|
312
|
+
errors.push(
|
|
313
|
+
createError(
|
|
314
|
+
"INVALID_VALUE",
|
|
315
|
+
"Skill toolReferences must be an array",
|
|
316
|
+
`${path}.toolReferences`,
|
|
317
|
+
skill.toolReferences,
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
skill.toolReferences.forEach((tool, i) => {
|
|
322
|
+
const toolPath = `${path}.toolReferences[${i}]`;
|
|
323
|
+
if (!tool.name) {
|
|
324
|
+
errors.push(
|
|
325
|
+
createError(
|
|
326
|
+
"MISSING_REQUIRED",
|
|
327
|
+
"Tool reference missing name",
|
|
328
|
+
`${toolPath}.name`,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (!tool.description) {
|
|
333
|
+
errors.push(
|
|
334
|
+
createError(
|
|
335
|
+
"MISSING_REQUIRED",
|
|
336
|
+
"Tool reference missing description",
|
|
337
|
+
`${toolPath}.description`,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (!tool.useWhen) {
|
|
342
|
+
errors.push(
|
|
343
|
+
createError(
|
|
344
|
+
"MISSING_REQUIRED",
|
|
345
|
+
"Tool reference missing useWhen",
|
|
346
|
+
`${toolPath}.useWhen`,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (tool.url !== undefined && typeof tool.url !== "string") {
|
|
351
|
+
errors.push(
|
|
352
|
+
createError(
|
|
353
|
+
"INVALID_VALUE",
|
|
354
|
+
"Tool reference url must be a string",
|
|
355
|
+
`${toolPath}.url`,
|
|
356
|
+
tool.url,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
298
364
|
return { errors, warnings };
|
|
299
365
|
}
|
|
300
366
|
|
|
@@ -407,7 +407,7 @@ function createAllStagesPreview(context) {
|
|
|
407
407
|
const skillFiles = derivedSkills
|
|
408
408
|
.map((derived) => skills.find((s) => s.id === derived.skillId))
|
|
409
409
|
.filter((skill) => skill?.agent)
|
|
410
|
-
.map((skill) => generateSkillMd(skill));
|
|
410
|
+
.map((skill) => generateSkillMd(skill, stages));
|
|
411
411
|
|
|
412
412
|
return div(
|
|
413
413
|
{ className: "agent-deployment" },
|
|
@@ -522,7 +522,7 @@ function createSingleStagePreview(context, stage) {
|
|
|
522
522
|
const skillFiles = derivedSkills
|
|
523
523
|
.map((d) => skills.find((s) => s.id === d.skillId))
|
|
524
524
|
.filter((skill) => skill?.agent)
|
|
525
|
-
.map((skill) => generateSkillMd(skill));
|
|
525
|
+
.map((skill) => generateSkillMd(skill, stages));
|
|
526
526
|
|
|
527
527
|
return div(
|
|
528
528
|
{ className: "agent-deployment" },
|
package/app/pages/landing.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getState } from "../lib/state.js";
|
|
|
7
7
|
import { createStatCard } from "../components/card.js";
|
|
8
8
|
import { groupSkillsByCapability, getConceptEmoji } from "../model/levels.js";
|
|
9
9
|
import { getStageEmoji } from "../formatters/stage/shared.js";
|
|
10
|
+
import { aggregateTools } from "../formatters/tool/shared.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Create lifecycle flow visualization for landing page
|
|
@@ -43,6 +44,7 @@ export function renderLanding() {
|
|
|
43
44
|
// Calculate stats using centralized capability ordering
|
|
44
45
|
const skillsByCapability = groupSkillsByCapability(data.skills);
|
|
45
46
|
const capabilityCount = Object.keys(skillsByCapability).length;
|
|
47
|
+
const tools = aggregateTools(data.skills);
|
|
46
48
|
|
|
47
49
|
const page = div(
|
|
48
50
|
{ className: "landing-page" },
|
|
@@ -83,7 +85,7 @@ export function renderLanding() {
|
|
|
83
85
|
|
|
84
86
|
// Stats grid
|
|
85
87
|
div(
|
|
86
|
-
{ className: "grid grid-
|
|
88
|
+
{ className: "grid grid-4" },
|
|
87
89
|
createStatCard({
|
|
88
90
|
value: data.disciplines.length,
|
|
89
91
|
label: "Disciplines",
|
|
@@ -99,28 +101,41 @@ export function renderLanding() {
|
|
|
99
101
|
label: "Tracks",
|
|
100
102
|
href: "/track",
|
|
101
103
|
}),
|
|
104
|
+
createStatCard({
|
|
105
|
+
value: data.behaviours.length,
|
|
106
|
+
label: "Behaviours",
|
|
107
|
+
href: "/behaviour",
|
|
108
|
+
}),
|
|
102
109
|
createStatCard({
|
|
103
110
|
value: data.skills.length,
|
|
104
111
|
label: "Skills",
|
|
105
112
|
href: "/skill",
|
|
106
113
|
}),
|
|
107
114
|
createStatCard({
|
|
108
|
-
value: data.
|
|
109
|
-
label: "
|
|
110
|
-
href: "/
|
|
115
|
+
value: data.drivers.length,
|
|
116
|
+
label: "Drivers",
|
|
117
|
+
href: "/driver",
|
|
111
118
|
}),
|
|
112
119
|
createStatCard({
|
|
113
120
|
value: stages.length,
|
|
114
121
|
label: "Stages",
|
|
115
122
|
href: "/stage",
|
|
116
123
|
}),
|
|
124
|
+
createStatCard({
|
|
125
|
+
value: tools.length,
|
|
126
|
+
label: "Tools",
|
|
127
|
+
href: "/tool",
|
|
128
|
+
}),
|
|
117
129
|
),
|
|
118
130
|
|
|
119
131
|
// Lifecycle flow visualization
|
|
120
132
|
stages.length > 0
|
|
121
133
|
? div(
|
|
122
134
|
{ className: "section section-detail" },
|
|
123
|
-
h2(
|
|
135
|
+
h2(
|
|
136
|
+
{ className: "section-title" },
|
|
137
|
+
`${getConceptEmoji(framework, "stage")} Engineering Lifecycle`,
|
|
138
|
+
),
|
|
124
139
|
p(
|
|
125
140
|
{ className: "text-muted", style: "margin-bottom: 1rem" },
|
|
126
141
|
"The three stages of engineering work, from planning through review.",
|
|
@@ -134,7 +149,7 @@ export function renderLanding() {
|
|
|
134
149
|
{ className: "section section-detail" },
|
|
135
150
|
h2({ className: "section-title" }, "Explore the Framework"),
|
|
136
151
|
div(
|
|
137
|
-
{ className: "grid grid-
|
|
152
|
+
{ className: "grid grid-4" },
|
|
138
153
|
createQuickLinkCard(
|
|
139
154
|
`${getConceptEmoji(framework, "discipline")} ${framework.entityDefinitions.discipline.title}`,
|
|
140
155
|
`${data.disciplines.length} ${framework.entityDefinitions.discipline.title.toLowerCase()} — ${framework.entityDefinitions.discipline.description.trim().split("\n")[0]}`,
|
|
@@ -150,26 +165,31 @@ export function renderLanding() {
|
|
|
150
165
|
`${data.tracks.length} ${framework.entityDefinitions.track.title.toLowerCase()} — ${framework.entityDefinitions.track.description.trim().split("\n")[0]}`,
|
|
151
166
|
"/track",
|
|
152
167
|
),
|
|
153
|
-
createQuickLinkCard(
|
|
154
|
-
`${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
|
|
155
|
-
`${data.skills.length} ${framework.entityDefinitions.skill.title.toLowerCase()} across ${capabilityCount} capabilities — ${framework.entityDefinitions.skill.description.trim().split("\n")[0]}`,
|
|
156
|
-
"/skill",
|
|
157
|
-
),
|
|
158
168
|
createQuickLinkCard(
|
|
159
169
|
`${getConceptEmoji(framework, "behaviour")} ${framework.entityDefinitions.behaviour.title}`,
|
|
160
170
|
`${data.behaviours.length} ${framework.entityDefinitions.behaviour.title.toLowerCase()} — ${framework.entityDefinitions.behaviour.description.trim().split("\n")[0]}`,
|
|
161
171
|
"/behaviour",
|
|
162
172
|
),
|
|
163
173
|
createQuickLinkCard(
|
|
164
|
-
"
|
|
165
|
-
`${
|
|
166
|
-
"/
|
|
174
|
+
`${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
|
|
175
|
+
`${data.skills.length} ${framework.entityDefinitions.skill.title.toLowerCase()} across ${capabilityCount} capabilities — ${framework.entityDefinitions.skill.description.trim().split("\n")[0]}`,
|
|
176
|
+
"/skill",
|
|
167
177
|
),
|
|
168
178
|
createQuickLinkCard(
|
|
169
179
|
`${getConceptEmoji(framework, "driver")} ${framework.entityDefinitions.driver.title}`,
|
|
170
180
|
`${data.drivers.length} ${framework.entityDefinitions.driver.title.toLowerCase()} — ${framework.entityDefinitions.driver.description.trim().split("\n")[0]}`,
|
|
171
181
|
"/driver",
|
|
172
182
|
),
|
|
183
|
+
createQuickLinkCard(
|
|
184
|
+
`${getConceptEmoji(framework, "stage")} ${framework.entityDefinitions.stage.title}`,
|
|
185
|
+
`${stages.length} ${framework.entityDefinitions.stage.title.toLowerCase()} — ${framework.entityDefinitions.stage.description.trim().split("\n")[0]}`,
|
|
186
|
+
"/stage",
|
|
187
|
+
),
|
|
188
|
+
createQuickLinkCard(
|
|
189
|
+
`${getConceptEmoji(framework, "tool")} ${framework.entityDefinitions.tool.title}`,
|
|
190
|
+
`${tools.length} ${framework.entityDefinitions.tool.title.toLowerCase()} — ${framework.entityDefinitions.tool.description.trim().split("\n")[0]}`,
|
|
191
|
+
"/tool",
|
|
192
|
+
),
|
|
173
193
|
),
|
|
174
194
|
),
|
|
175
195
|
|
|
@@ -82,7 +82,7 @@ function getWizardSteps(data) {
|
|
|
82
82
|
if (skills && skills.length > 0) {
|
|
83
83
|
steps.push({
|
|
84
84
|
id: `skills-${capability}`,
|
|
85
|
-
name: formatCapability(capability),
|
|
85
|
+
name: formatCapability(capability, data.capabilities),
|
|
86
86
|
icon: getCapabilityEmoji(data.capabilities, capability),
|
|
87
87
|
type: "skills",
|
|
88
88
|
capability: capability,
|
|
@@ -113,11 +113,13 @@ function getWizardSteps(data) {
|
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* Format capability name for display
|
|
116
|
-
* @param {string}
|
|
116
|
+
* @param {string} capabilityId
|
|
117
|
+
* @param {Array} capabilities
|
|
117
118
|
* @returns {string}
|
|
118
119
|
*/
|
|
119
|
-
function formatCapability(
|
|
120
|
-
|
|
120
|
+
function formatCapability(capabilityId, capabilities) {
|
|
121
|
+
const capability = capabilities.find((c) => c.id === capabilityId);
|
|
122
|
+
return capability?.name || capabilityId;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
/**
|
|
@@ -412,7 +414,7 @@ function renderSkillsStep(step, data) {
|
|
|
412
414
|
h2(
|
|
413
415
|
{},
|
|
414
416
|
span({ className: "step-header-icon" }, step.icon),
|
|
415
|
-
` ${formatCapability(capability)} Skills`,
|
|
417
|
+
` ${formatCapability(capability, data.capabilities)} Skills`,
|
|
416
418
|
),
|
|
417
419
|
span(
|
|
418
420
|
{ className: "step-progress" },
|
package/app/pages/skill.js
CHANGED
|
@@ -89,25 +89,13 @@ export function renderSkillDetail(params) {
|
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
91
|
* Format capability for display
|
|
92
|
-
* @param {string}
|
|
92
|
+
* @param {string} capabilityId
|
|
93
93
|
* @param {Array} capabilities
|
|
94
94
|
* @returns {string}
|
|
95
95
|
*/
|
|
96
|
-
function formatCapability(
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
reliability: "Reliability",
|
|
101
|
-
data: "Data",
|
|
102
|
-
ai: "AI",
|
|
103
|
-
process: "Process",
|
|
104
|
-
business: "Business",
|
|
105
|
-
people: "People",
|
|
106
|
-
documentation: "Documentation",
|
|
107
|
-
};
|
|
108
|
-
const label =
|
|
109
|
-
capabilityLabels[capability] ||
|
|
110
|
-
capability.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
111
|
-
const emoji = getCapabilityEmoji(capabilities, capability);
|
|
96
|
+
function formatCapability(capabilityId, capabilities) {
|
|
97
|
+
const capability = capabilities.find((c) => c.id === capabilityId);
|
|
98
|
+
const label = capability?.name || capabilityId;
|
|
99
|
+
const emoji = getCapabilityEmoji(capabilities, capabilityId);
|
|
112
100
|
return `${emoji} ${label}`;
|
|
113
101
|
}
|
package/app/pages/stage.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getState } from "../lib/state.js";
|
|
|
7
7
|
import { createCardList } from "../components/list.js";
|
|
8
8
|
import { renderNotFound } from "../components/error-page.js";
|
|
9
9
|
import { prepareStagesList, stageToDOM } from "../formatters/stage/index.js";
|
|
10
|
+
import { getConceptEmoji } from "../model/levels.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Map stage to card configuration
|
|
@@ -15,7 +16,7 @@ import { prepareStagesList, stageToDOM } from "../formatters/stage/index.js";
|
|
|
15
16
|
*/
|
|
16
17
|
function stageToCardConfig(stage) {
|
|
17
18
|
return {
|
|
18
|
-
title: `${stage.emoji
|
|
19
|
+
title: `${stage.emoji} ${stage.name}`,
|
|
19
20
|
description: stage.truncatedDescription,
|
|
20
21
|
href: `/stage/${stage.id}`,
|
|
21
22
|
};
|
|
@@ -28,14 +29,13 @@ function stageToCardConfig(stage) {
|
|
|
28
29
|
*/
|
|
29
30
|
function createLifecycleFlow(stages) {
|
|
30
31
|
const flowItems = stages.map((stage, index) => {
|
|
31
|
-
const emoji = stage.emoji || "🔄";
|
|
32
32
|
const isLast = index === stages.length - 1;
|
|
33
33
|
|
|
34
34
|
return div(
|
|
35
35
|
{ className: "lifecycle-flow-item" },
|
|
36
36
|
a(
|
|
37
37
|
{ href: `#/stage/${stage.id}`, className: "lifecycle-stage" },
|
|
38
|
-
span({ className: "lifecycle-emoji" }, emoji),
|
|
38
|
+
span({ className: "lifecycle-emoji" }, stage.emoji),
|
|
39
39
|
span({ className: "lifecycle-name" }, stage.name),
|
|
40
40
|
),
|
|
41
41
|
!isLast ? span({ className: "lifecycle-arrow" }, "→") : null,
|
|
@@ -50,7 +50,9 @@ function createLifecycleFlow(stages) {
|
|
|
50
50
|
*/
|
|
51
51
|
export function renderStagesList() {
|
|
52
52
|
const { data } = getState();
|
|
53
|
+
const { framework } = data;
|
|
53
54
|
const stages = data.stages || [];
|
|
55
|
+
const stageEmoji = getConceptEmoji(framework, "stage");
|
|
54
56
|
|
|
55
57
|
// Transform data for list view
|
|
56
58
|
const { items } = prepareStagesList(stages);
|
|
@@ -60,11 +62,13 @@ export function renderStagesList() {
|
|
|
60
62
|
// Header
|
|
61
63
|
div(
|
|
62
64
|
{ className: "page-header" },
|
|
63
|
-
h1(
|
|
65
|
+
h1(
|
|
66
|
+
{ className: "page-title" },
|
|
67
|
+
`${stageEmoji} ${framework.entityDefinitions.stage.title}`,
|
|
68
|
+
),
|
|
64
69
|
p(
|
|
65
70
|
{ className: "page-description" },
|
|
66
|
-
|
|
67
|
-
"constraints, and handoffs to guide work from planning through review.",
|
|
71
|
+
framework.entityDefinitions.stage.description.trim().split("\n")[0],
|
|
68
72
|
),
|
|
69
73
|
),
|
|
70
74
|
|