@forwardimpact/pathway 0.1.0 → 0.2.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 +109 -21
- package/app/commands/command-factory.js +3 -3
- package/app/commands/interview.js +14 -7
- package/app/commands/job.js +43 -29
- package/app/commands/progress.js +14 -7
- package/app/commands/serve.js +5 -0
- package/app/commands/stage.js +0 -10
- package/app/commands/track.js +5 -8
- package/app/components/builder.js +111 -27
- package/app/css/components/surfaces.css +16 -0
- package/app/formatters/agent/profile.js +113 -87
- package/app/formatters/agent/skill.js +64 -31
- package/app/formatters/behaviour/dom.js +3 -0
- package/app/formatters/behaviour/microdata.js +106 -0
- package/app/formatters/discipline/dom.js +28 -1
- package/app/formatters/discipline/microdata.js +117 -0
- package/app/formatters/discipline/shared.js +49 -8
- package/app/formatters/driver/dom.js +3 -0
- package/app/formatters/driver/microdata.js +91 -0
- package/app/formatters/grade/dom.js +3 -0
- package/app/formatters/grade/microdata.js +151 -0
- package/app/formatters/index.js +32 -1
- package/app/formatters/interview/shared.js +13 -8
- package/app/formatters/job/description.js +5 -3
- package/app/formatters/json-ld.js +242 -0
- package/app/formatters/microdata-shared.js +184 -0
- package/app/formatters/progress/shared.js +14 -11
- package/app/formatters/skill/dom.js +3 -0
- package/app/formatters/skill/microdata.js +151 -0
- package/app/formatters/stage/dom.js +3 -18
- package/app/formatters/stage/microdata.js +110 -0
- package/app/formatters/stage/shared.js +0 -27
- package/app/formatters/track/dom.js +5 -30
- package/app/formatters/track/markdown.js +2 -25
- package/app/formatters/track/microdata.js +111 -0
- package/app/formatters/track/shared.js +6 -58
- package/app/handout-main.js +26 -12
- package/app/index.html +11 -0
- package/app/lib/card-mappers.js +17 -12
- package/app/lib/job-cache.js +12 -9
- package/app/lib/template-loader.js +66 -0
- package/app/lib/yaml-loader.js +25 -8
- package/app/main.js +8 -4
- package/app/model/agent.js +158 -130
- package/app/model/checklist.js +57 -91
- package/app/model/derivation.js +135 -68
- package/app/model/index-generator.js +1 -7
- package/app/model/job.js +19 -13
- package/app/model/levels.js +20 -12
- package/app/model/loader.js +41 -17
- package/app/model/matching.js +33 -3
- package/app/model/profile.js +38 -45
- package/app/model/schema-validation.js +438 -0
- package/app/model/validation.js +747 -68
- package/app/pages/agent-builder.js +119 -25
- package/app/pages/assessment-results.js +10 -4
- package/app/pages/discipline.js +36 -6
- package/app/pages/driver.js +9 -47
- package/app/pages/interview-builder.js +3 -1
- package/app/pages/interview.js +15 -4
- package/app/pages/job-builder.js +4 -1
- package/app/pages/job.js +15 -4
- package/app/pages/landing.js +10 -10
- package/app/pages/progress-builder.js +3 -1
- package/app/pages/progress.js +72 -21
- package/app/pages/stage.js +3 -126
- package/app/slide-main.js +45 -17
- package/app/slides/index.js +3 -1
- package/app/slides/overview.js +40 -4
- package/app/slides/progress.js +4 -2
- package/bin/pathway.js +18 -64
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
- package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
- package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
- package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
- package/examples/agents/.vscode/settings.json +1 -1
- package/examples/behaviours/outcome_ownership.yaml +1 -2
- package/examples/behaviours/polymathic_knowledge.yaml +1 -2
- package/examples/behaviours/precise_communication.yaml +1 -2
- package/examples/behaviours/relentless_curiosity.yaml +1 -2
- package/examples/behaviours/systems_thinking.yaml +1 -2
- package/examples/capabilities/business.yaml +80 -142
- package/examples/capabilities/delivery.yaml +155 -219
- package/examples/capabilities/people.yaml +2 -34
- package/examples/capabilities/reliability.yaml +161 -80
- package/examples/capabilities/scale.yaml +234 -252
- package/examples/copilot-setup-steps.yaml +25 -0
- package/examples/devcontainer.yaml +21 -0
- package/examples/disciplines/_index.yaml +1 -0
- package/examples/disciplines/data_engineering.yaml +14 -12
- package/examples/disciplines/engineering_management.yaml +63 -0
- package/examples/disciplines/software_engineering.yaml +14 -12
- package/examples/drivers.yaml +1 -4
- package/examples/framework.yaml +1 -2
- package/examples/grades.yaml +1 -3
- package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
- package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
- package/examples/questions/behaviours/precise_communication.yaml +1 -2
- package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
- package/examples/questions/behaviours/systems_thinking.yaml +1 -2
- package/examples/questions/skills/architecture_design.yaml +1 -2
- package/examples/questions/skills/cloud_platforms.yaml +1 -2
- package/examples/questions/skills/code_quality.yaml +1 -2
- package/examples/questions/skills/data_modeling.yaml +1 -2
- package/examples/questions/skills/devops.yaml +1 -2
- package/examples/questions/skills/full_stack_development.yaml +1 -2
- package/examples/questions/skills/sre_practices.yaml +1 -2
- package/examples/questions/skills/stakeholder_management.yaml +1 -2
- package/examples/questions/skills/team_collaboration.yaml +1 -2
- package/examples/questions/skills/technical_writing.yaml +1 -2
- package/examples/self-assessments.yaml +1 -3
- package/examples/stages.yaml +101 -46
- package/examples/tracks/_index.yaml +0 -1
- package/examples/tracks/platform.yaml +8 -13
- package/examples/tracks/sre.yaml +8 -18
- package/examples/vscode-settings.yaml +2 -7
- package/package.json +9 -3
- package/templates/agent.template.md +65 -0
- package/templates/skill.template.md +28 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
- package/examples/tracks/manager.yaml +0 -53
package/app/pages/progress.js
CHANGED
|
@@ -26,18 +26,29 @@ import {
|
|
|
26
26
|
* @param {Object} params - Route params
|
|
27
27
|
*/
|
|
28
28
|
export function renderProgressDetail(params) {
|
|
29
|
-
const { discipline: disciplineId,
|
|
29
|
+
const { discipline: disciplineId, grade: gradeId, track: trackId } = params;
|
|
30
30
|
const { data } = getState();
|
|
31
31
|
|
|
32
32
|
// Find the components
|
|
33
33
|
const discipline = data.disciplines.find((d) => d.id === disciplineId);
|
|
34
|
-
const track = data.tracks.find((t) => t.id === trackId);
|
|
35
34
|
const grade = data.grades.find((g) => g.id === gradeId);
|
|
35
|
+
const track = trackId ? data.tracks.find((t) => t.id === trackId) : null;
|
|
36
36
|
|
|
37
|
-
if (!discipline || !
|
|
37
|
+
if (!discipline || !grade) {
|
|
38
38
|
renderError({
|
|
39
39
|
title: "Role Not Found",
|
|
40
|
-
message: "Invalid role combination.
|
|
40
|
+
message: "Invalid role combination. Discipline or grade not found.",
|
|
41
|
+
backPath: "/career-progress",
|
|
42
|
+
backText: "← Back to Career Progress",
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If trackId was provided but not found, error
|
|
48
|
+
if (trackId && !track) {
|
|
49
|
+
renderError({
|
|
50
|
+
title: "Role Not Found",
|
|
51
|
+
message: `Track "${trackId}" not found.`,
|
|
41
52
|
backPath: "/career-progress",
|
|
42
53
|
backText: "← Back to Career Progress",
|
|
43
54
|
});
|
|
@@ -79,8 +90,9 @@ export function renderProgressDetail(params) {
|
|
|
79
90
|
a({ href: `#/discipline/${discipline.id}` }, discipline.specialization),
|
|
80
91
|
" × ",
|
|
81
92
|
a({ href: `#/grade/${grade.id}` }, grade.id),
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
track
|
|
94
|
+
? [" × ", a({ href: `#/track/${track.id}` }, track.name)]
|
|
95
|
+
: " (Generalist)",
|
|
84
96
|
),
|
|
85
97
|
),
|
|
86
98
|
|
|
@@ -120,7 +132,9 @@ export function renderProgressDetail(params) {
|
|
|
120
132
|
{ className: "page-actions", style: "margin-top: 2rem" },
|
|
121
133
|
a(
|
|
122
134
|
{
|
|
123
|
-
href:
|
|
135
|
+
href: trackId
|
|
136
|
+
? `#/job/${disciplineId}/${gradeId}/${trackId}`
|
|
137
|
+
: `#/job/${disciplineId}/${gradeId}`,
|
|
124
138
|
className: "btn btn-secondary",
|
|
125
139
|
},
|
|
126
140
|
"View Full Job Definition",
|
|
@@ -160,17 +174,29 @@ function createComparisonSelectorsSection({
|
|
|
160
174
|
// State to track current selections - default to same discipline, same track, next grade
|
|
161
175
|
let selectedDisciplineId = discipline.id;
|
|
162
176
|
let selectedGradeId = nextGrade?.id || "";
|
|
163
|
-
let selectedTrackId = currentTrack
|
|
177
|
+
let selectedTrackId = currentTrack?.id || "";
|
|
164
178
|
|
|
165
179
|
// Get available options based on selected discipline
|
|
166
180
|
function getAvailableOptions(disciplineId) {
|
|
167
181
|
const selectedDisc = data.disciplines.find((d) => d.id === disciplineId);
|
|
168
|
-
if (!selectedDisc)
|
|
182
|
+
if (!selectedDisc)
|
|
183
|
+
return { grades: [], tracks: [], allowsTrackless: false };
|
|
169
184
|
|
|
170
185
|
const validGrades = [];
|
|
171
186
|
const validTracks = new Set();
|
|
187
|
+
let allowsTrackless = false;
|
|
172
188
|
|
|
173
189
|
for (const grade of data.grades) {
|
|
190
|
+
// Check trackless combination
|
|
191
|
+
if (
|
|
192
|
+
isValidCombination({ discipline: selectedDisc, grade, track: null })
|
|
193
|
+
) {
|
|
194
|
+
if (!validGrades.find((g) => g.id === grade.id)) {
|
|
195
|
+
validGrades.push(grade);
|
|
196
|
+
}
|
|
197
|
+
allowsTrackless = true;
|
|
198
|
+
}
|
|
199
|
+
// Check each track combination
|
|
174
200
|
for (const track of data.tracks) {
|
|
175
201
|
if (isValidCombination({ discipline: selectedDisc, grade, track })) {
|
|
176
202
|
if (!validGrades.find((g) => g.id === grade.id)) {
|
|
@@ -186,6 +212,7 @@ function createComparisonSelectorsSection({
|
|
|
186
212
|
tracks: data.tracks
|
|
187
213
|
.filter((t) => validTracks.has(t.id))
|
|
188
214
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
215
|
+
allowsTrackless,
|
|
189
216
|
};
|
|
190
217
|
}
|
|
191
218
|
|
|
@@ -196,13 +223,14 @@ function createComparisonSelectorsSection({
|
|
|
196
223
|
// Clear previous results
|
|
197
224
|
comparisonResultsContainer.innerHTML = "";
|
|
198
225
|
|
|
199
|
-
|
|
226
|
+
// Track can be empty string for generalist, but discipline and grade are required
|
|
227
|
+
if (!selectedDisciplineId || !selectedGradeId) {
|
|
200
228
|
comparisonResultsContainer.appendChild(
|
|
201
229
|
div(
|
|
202
230
|
{ className: "comparison-placeholder" },
|
|
203
231
|
p(
|
|
204
232
|
{ className: "text-muted" },
|
|
205
|
-
"Select a discipline
|
|
233
|
+
"Select a discipline and grade to see the comparison.",
|
|
206
234
|
),
|
|
207
235
|
),
|
|
208
236
|
);
|
|
@@ -213,9 +241,12 @@ function createComparisonSelectorsSection({
|
|
|
213
241
|
(d) => d.id === selectedDisciplineId,
|
|
214
242
|
);
|
|
215
243
|
const targetGrade = data.grades.find((g) => g.id === selectedGradeId);
|
|
216
|
-
|
|
244
|
+
// selectedTrackId can be empty string for generalist
|
|
245
|
+
const targetTrack = selectedTrackId
|
|
246
|
+
? data.tracks.find((t) => t.id === selectedTrackId)
|
|
247
|
+
: null;
|
|
217
248
|
|
|
218
|
-
if (!targetDiscipline || !targetGrade
|
|
249
|
+
if (!targetDiscipline || !targetGrade) {
|
|
219
250
|
return;
|
|
220
251
|
}
|
|
221
252
|
|
|
@@ -223,7 +254,7 @@ function createComparisonSelectorsSection({
|
|
|
223
254
|
if (
|
|
224
255
|
targetDiscipline.id === discipline.id &&
|
|
225
256
|
targetGrade.id === currentGrade.id &&
|
|
226
|
-
targetTrack
|
|
257
|
+
targetTrack?.id === currentTrack?.id
|
|
227
258
|
) {
|
|
228
259
|
comparisonResultsContainer.appendChild(
|
|
229
260
|
div(
|
|
@@ -343,10 +374,12 @@ function createComparisonSelectorsSection({
|
|
|
343
374
|
{ className: "page-actions" },
|
|
344
375
|
a(
|
|
345
376
|
{
|
|
346
|
-
href:
|
|
377
|
+
href: targetTrack
|
|
378
|
+
? `#/job/${targetDiscipline.id}/${targetGrade.id}/${targetTrack.id}`
|
|
379
|
+
: `#/job/${targetDiscipline.id}/${targetGrade.id}`,
|
|
347
380
|
className: "btn btn-secondary",
|
|
348
381
|
},
|
|
349
|
-
`View ${targetGrade.id} ${targetTrack.name} Job Definition →`,
|
|
382
|
+
`View ${targetGrade.id}${targetTrack ? ` ${targetTrack.name}` : ""} Job Definition →`,
|
|
350
383
|
),
|
|
351
384
|
),
|
|
352
385
|
);
|
|
@@ -394,10 +427,20 @@ function createComparisonSelectorsSection({
|
|
|
394
427
|
// Update track selector
|
|
395
428
|
if (trackSelectEl) {
|
|
396
429
|
trackSelectEl.innerHTML = "";
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
430
|
+
|
|
431
|
+
// Add generalist option if discipline allows trackless
|
|
432
|
+
if (availableOptions.allowsTrackless) {
|
|
433
|
+
const generalistOpt = document.createElement("option");
|
|
434
|
+
generalistOpt.value = "";
|
|
435
|
+
generalistOpt.textContent = "Generalist";
|
|
436
|
+
trackSelectEl.appendChild(generalistOpt);
|
|
437
|
+
} else {
|
|
438
|
+
const placeholderOpt = document.createElement("option");
|
|
439
|
+
placeholderOpt.value = "";
|
|
440
|
+
placeholderOpt.textContent = "Select track...";
|
|
441
|
+
placeholderOpt.disabled = true;
|
|
442
|
+
trackSelectEl.appendChild(placeholderOpt);
|
|
443
|
+
}
|
|
401
444
|
|
|
402
445
|
for (const track of availableOptions.tracks) {
|
|
403
446
|
const opt = document.createElement("option");
|
|
@@ -407,8 +450,16 @@ function createComparisonSelectorsSection({
|
|
|
407
450
|
}
|
|
408
451
|
|
|
409
452
|
// Try to keep current selection if valid
|
|
410
|
-
|
|
453
|
+
const hasValidTrack = availableOptions.tracks.find(
|
|
454
|
+
(t) => t.id === selectedTrackId,
|
|
455
|
+
);
|
|
456
|
+
const isValidGeneralist =
|
|
457
|
+
selectedTrackId === "" && availableOptions.allowsTrackless;
|
|
458
|
+
if (hasValidTrack || isValidGeneralist) {
|
|
411
459
|
trackSelectEl.value = selectedTrackId;
|
|
460
|
+
} else if (availableOptions.allowsTrackless) {
|
|
461
|
+
selectedTrackId = "";
|
|
462
|
+
trackSelectEl.value = "";
|
|
412
463
|
} else {
|
|
413
464
|
selectedTrackId = "";
|
|
414
465
|
trackSelectEl.value = "";
|
package/app/pages/stage.js
CHANGED
|
@@ -5,14 +5,8 @@
|
|
|
5
5
|
import { render, div, h1, h2, p, span, a, section } from "../lib/render.js";
|
|
6
6
|
import { getState } from "../lib/state.js";
|
|
7
7
|
import { createCardList } from "../components/list.js";
|
|
8
|
-
import { createDetailHeader } from "../components/detail.js";
|
|
9
8
|
import { renderNotFound } from "../components/error-page.js";
|
|
10
|
-
import {
|
|
11
|
-
prepareStagesList,
|
|
12
|
-
prepareStageDetail,
|
|
13
|
-
getStageEmoji,
|
|
14
|
-
} from "../formatters/stage/index.js";
|
|
15
|
-
import { createBadge } from "../components/card.js";
|
|
9
|
+
import { prepareStagesList, stageToDOM } from "../formatters/stage/index.js";
|
|
16
10
|
|
|
17
11
|
/**
|
|
18
12
|
* Map stage to card configuration
|
|
@@ -24,7 +18,6 @@ function stageToCardConfig(stage) {
|
|
|
24
18
|
title: `${stage.emoji || "🔄"} ${stage.name}`,
|
|
25
19
|
description: stage.truncatedDescription,
|
|
26
20
|
href: `/stage/${stage.id}`,
|
|
27
|
-
meta: [createBadge(`${stage.tools.length} tools`, "default")],
|
|
28
21
|
};
|
|
29
22
|
}
|
|
30
23
|
|
|
@@ -110,122 +103,6 @@ export function renderStageDetail(params) {
|
|
|
110
103
|
return;
|
|
111
104
|
}
|
|
112
105
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
const emoji = getStageEmoji(stages, stage.id);
|
|
116
|
-
|
|
117
|
-
const page = div(
|
|
118
|
-
{ className: "stage-detail" },
|
|
119
|
-
createDetailHeader({
|
|
120
|
-
title: `${emoji} ${view.name}`,
|
|
121
|
-
description: view.description,
|
|
122
|
-
backLink: "/stage",
|
|
123
|
-
backText: "← Back to Stages",
|
|
124
|
-
}),
|
|
125
|
-
|
|
126
|
-
// Tools section
|
|
127
|
-
view.tools.length > 0
|
|
128
|
-
? section(
|
|
129
|
-
{ className: "section section-detail" },
|
|
130
|
-
h2({ className: "section-title" }, "Available Tools"),
|
|
131
|
-
div(
|
|
132
|
-
{ className: "tool-badges" },
|
|
133
|
-
...view.tools.map((tool) =>
|
|
134
|
-
span(
|
|
135
|
-
{ className: "badge badge-tool", title: tool.label },
|
|
136
|
-
`${tool.icon} ${tool.label}`,
|
|
137
|
-
),
|
|
138
|
-
),
|
|
139
|
-
),
|
|
140
|
-
)
|
|
141
|
-
: null,
|
|
142
|
-
|
|
143
|
-
// Entry/Exit Criteria
|
|
144
|
-
view.entryCriteria.length > 0 || view.exitCriteria.length > 0
|
|
145
|
-
? section(
|
|
146
|
-
{ className: "section section-detail" },
|
|
147
|
-
div(
|
|
148
|
-
{ className: "content-columns" },
|
|
149
|
-
// Entry criteria column
|
|
150
|
-
view.entryCriteria.length > 0
|
|
151
|
-
? div(
|
|
152
|
-
{ className: "column" },
|
|
153
|
-
h2({ className: "section-title" }, "Entry Criteria"),
|
|
154
|
-
div(
|
|
155
|
-
{ className: "criteria-list" },
|
|
156
|
-
...view.entryCriteria.map((item) =>
|
|
157
|
-
div({ className: "criteria-item" }, `✓ ${item}`),
|
|
158
|
-
),
|
|
159
|
-
),
|
|
160
|
-
)
|
|
161
|
-
: null,
|
|
162
|
-
// Exit criteria column
|
|
163
|
-
view.exitCriteria.length > 0
|
|
164
|
-
? div(
|
|
165
|
-
{ className: "column" },
|
|
166
|
-
h2({ className: "section-title" }, "Exit Criteria"),
|
|
167
|
-
div(
|
|
168
|
-
{ className: "criteria-list" },
|
|
169
|
-
...view.exitCriteria.map((item) =>
|
|
170
|
-
div({ className: "criteria-item" }, `✓ ${item}`),
|
|
171
|
-
),
|
|
172
|
-
),
|
|
173
|
-
)
|
|
174
|
-
: null,
|
|
175
|
-
),
|
|
176
|
-
)
|
|
177
|
-
: null,
|
|
178
|
-
|
|
179
|
-
// Constraints
|
|
180
|
-
view.constraints.length > 0
|
|
181
|
-
? section(
|
|
182
|
-
{ className: "section section-detail" },
|
|
183
|
-
h2({ className: "section-title" }, "Constraints"),
|
|
184
|
-
div(
|
|
185
|
-
{ className: "constraint-list" },
|
|
186
|
-
...view.constraints.map((item) =>
|
|
187
|
-
div({ className: "constraint-item" }, `⚠️ ${item}`),
|
|
188
|
-
),
|
|
189
|
-
),
|
|
190
|
-
)
|
|
191
|
-
: null,
|
|
192
|
-
|
|
193
|
-
// Handoffs
|
|
194
|
-
view.handoffs.length > 0
|
|
195
|
-
? section(
|
|
196
|
-
{ className: "section section-detail" },
|
|
197
|
-
h2({ className: "section-title" }, "Handoffs"),
|
|
198
|
-
div(
|
|
199
|
-
{ className: "handoff-list" },
|
|
200
|
-
...view.handoffs.map((handoff) => {
|
|
201
|
-
const targetStage = stages.find((s) => s.id === handoff.target);
|
|
202
|
-
const targetEmoji = getStageEmoji(stages, handoff.target);
|
|
203
|
-
return div(
|
|
204
|
-
{ className: "card handoff-card" },
|
|
205
|
-
div(
|
|
206
|
-
{ className: "handoff-header" },
|
|
207
|
-
targetStage
|
|
208
|
-
? a(
|
|
209
|
-
{
|
|
210
|
-
href: `#/stage/${handoff.target}`,
|
|
211
|
-
className: "handoff-link",
|
|
212
|
-
},
|
|
213
|
-
`${targetEmoji} ${handoff.label}`,
|
|
214
|
-
)
|
|
215
|
-
: span({}, `${targetEmoji} ${handoff.label}`),
|
|
216
|
-
),
|
|
217
|
-
handoff.prompt
|
|
218
|
-
? p(
|
|
219
|
-
{ className: "handoff-prompt text-muted" },
|
|
220
|
-
handoff.prompt,
|
|
221
|
-
)
|
|
222
|
-
: null,
|
|
223
|
-
);
|
|
224
|
-
}),
|
|
225
|
-
),
|
|
226
|
-
)
|
|
227
|
-
: null,
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
render(page);
|
|
106
|
+
// Use DOM formatter - it handles transformation internally
|
|
107
|
+
render(stageToDOM(stage, { stages }));
|
|
231
108
|
}
|
package/app/slide-main.js
CHANGED
|
@@ -9,7 +9,7 @@ import { setData, getState } from "./lib/state.js";
|
|
|
9
9
|
import { loadAllData } from "./lib/yaml-loader.js";
|
|
10
10
|
import { span, a } from "./lib/render.js";
|
|
11
11
|
import { generateAllJobs } from "./model/derivation.js";
|
|
12
|
-
import {
|
|
12
|
+
import { sortTracksByName } from "./formatters/track/shared.js";
|
|
13
13
|
|
|
14
14
|
// Import slide renderers
|
|
15
15
|
import { renderChapterSlide } from "./slides/chapter.js";
|
|
@@ -150,13 +150,21 @@ function setupRoutes() {
|
|
|
150
150
|
renderTrackSlide({ render: renderSlide, data: getState().data, params });
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
// Jobs
|
|
154
|
-
router.on("/job/:discipline/:track
|
|
153
|
+
// Jobs - new format: discipline/grade/track (track optional)
|
|
154
|
+
router.on("/job/:discipline/:grade/:track", (params) => {
|
|
155
155
|
renderJobSlide({ render: renderSlide, data: getState().data, params });
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
router.on("/job/:discipline/:grade", (params) => {
|
|
159
|
+
renderJobSlide({
|
|
160
|
+
render: renderSlide,
|
|
161
|
+
data: getState().data,
|
|
162
|
+
params: { ...params, track: null },
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Interviews - new format: discipline/grade/track (track optional)
|
|
167
|
+
router.on("/interview/:discipline/:grade/:track", (params) => {
|
|
160
168
|
renderInterviewSlide({
|
|
161
169
|
render: renderSlide,
|
|
162
170
|
data: getState().data,
|
|
@@ -164,10 +172,26 @@ function setupRoutes() {
|
|
|
164
172
|
});
|
|
165
173
|
});
|
|
166
174
|
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
router.on("/interview/:discipline/:grade", (params) => {
|
|
176
|
+
renderInterviewSlide({
|
|
177
|
+
render: renderSlide,
|
|
178
|
+
data: getState().data,
|
|
179
|
+
params: { ...params, track: null },
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Progress - new format: discipline/grade/track (track optional)
|
|
184
|
+
router.on("/progress/:discipline/:grade/:track", (params) => {
|
|
169
185
|
renderProgressSlide({ render: renderSlide, data: getState().data, params });
|
|
170
186
|
});
|
|
187
|
+
|
|
188
|
+
router.on("/progress/:discipline/:grade", (params) => {
|
|
189
|
+
renderProgressSlide({
|
|
190
|
+
render: renderSlide,
|
|
191
|
+
data: getState().data,
|
|
192
|
+
params: { ...params, track: null },
|
|
193
|
+
});
|
|
194
|
+
});
|
|
171
195
|
}
|
|
172
196
|
|
|
173
197
|
/**
|
|
@@ -187,15 +211,7 @@ function buildSlideOrder(data) {
|
|
|
187
211
|
data.disciplines.forEach((d) => order.push(`/discipline/${d.id}`));
|
|
188
212
|
}
|
|
189
213
|
|
|
190
|
-
// Tracks
|
|
191
|
-
if (data.tracks && data.tracks.length > 0) {
|
|
192
|
-
boundaries.push(order.length);
|
|
193
|
-
order.push("/chapter/track");
|
|
194
|
-
order.push("/overview/track");
|
|
195
|
-
sortTracksByType(data.tracks).forEach((t) => order.push(`/track/${t.id}`));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Grades
|
|
214
|
+
// Grades (moved before Tracks)
|
|
199
215
|
if (data.grades && data.grades.length > 0) {
|
|
200
216
|
boundaries.push(order.length);
|
|
201
217
|
order.push("/chapter/grade");
|
|
@@ -203,6 +219,14 @@ function buildSlideOrder(data) {
|
|
|
203
219
|
data.grades.forEach((g) => order.push(`/grade/${g.id}`));
|
|
204
220
|
}
|
|
205
221
|
|
|
222
|
+
// Tracks (moved after Grades)
|
|
223
|
+
if (data.tracks && data.tracks.length > 0) {
|
|
224
|
+
boundaries.push(order.length);
|
|
225
|
+
order.push("/chapter/track");
|
|
226
|
+
order.push("/overview/track");
|
|
227
|
+
sortTracksByName(data.tracks).forEach((t) => order.push(`/track/${t.id}`));
|
|
228
|
+
}
|
|
229
|
+
|
|
206
230
|
// Skills
|
|
207
231
|
if (data.skills && data.skills.length > 0) {
|
|
208
232
|
boundaries.push(order.length);
|
|
@@ -241,7 +265,11 @@ function buildSlideOrder(data) {
|
|
|
241
265
|
order.push("/chapter/job");
|
|
242
266
|
order.push("/overview/job");
|
|
243
267
|
jobs.forEach((job) =>
|
|
244
|
-
order.push(
|
|
268
|
+
order.push(
|
|
269
|
+
job.track
|
|
270
|
+
? `/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
|
|
271
|
+
: `/job/${job.discipline.id}/${job.grade.id}`,
|
|
272
|
+
),
|
|
245
273
|
);
|
|
246
274
|
}
|
|
247
275
|
|
package/app/slides/index.js
CHANGED
|
@@ -184,7 +184,9 @@ export function renderSlideIndex({ render, data }) {
|
|
|
184
184
|
{},
|
|
185
185
|
a(
|
|
186
186
|
{
|
|
187
|
-
href:
|
|
187
|
+
href: job.track
|
|
188
|
+
? `#/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
|
|
189
|
+
: `#/job/${job.discipline.id}/${job.grade.id}`,
|
|
188
190
|
},
|
|
189
191
|
job.title,
|
|
190
192
|
),
|
package/app/slides/overview.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Displays overview slides for each chapter with cards for all entities.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { div, h1, p, span } from "../lib/render.js";
|
|
8
|
-
import { createCardList } from "../components/list.js";
|
|
7
|
+
import { div, h1, h2, p, span } from "../lib/render.js";
|
|
8
|
+
import { createCardList, createGroupedList } from "../components/list.js";
|
|
9
|
+
import { createBadge } from "../components/card.js";
|
|
9
10
|
import {
|
|
10
11
|
disciplineToCardConfig,
|
|
11
12
|
skillToCardConfig,
|
|
@@ -23,6 +24,31 @@ import { prepareGradesList } from "../formatters/grade/shared.js";
|
|
|
23
24
|
import { prepareTracksList } from "../formatters/track/shared.js";
|
|
24
25
|
import { generateAllJobs } from "../model/derivation.js";
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Format discipline group name for display
|
|
29
|
+
* @param {string} groupName - Group name (professional/management)
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function formatDisciplineGroupName(groupName) {
|
|
33
|
+
if (groupName === "professional") return "Professional";
|
|
34
|
+
if (groupName === "management") return "Management";
|
|
35
|
+
return groupName.charAt(0).toUpperCase() + groupName.slice(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Render discipline group header
|
|
40
|
+
* @param {string} groupName - Group name
|
|
41
|
+
* @param {number} count - Number of items in group
|
|
42
|
+
* @returns {HTMLElement}
|
|
43
|
+
*/
|
|
44
|
+
function renderDisciplineGroupHeader(groupName, count) {
|
|
45
|
+
return div(
|
|
46
|
+
{ className: "capability-header" },
|
|
47
|
+
h2({ className: "capability-title" }, formatDisciplineGroupName(groupName)),
|
|
48
|
+
createBadge(`${count}`, "default"),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
/**
|
|
27
53
|
* Render overview slide
|
|
28
54
|
* @param {Object} params
|
|
@@ -62,8 +88,9 @@ export function renderOverviewSlide({ render, data, params }) {
|
|
|
62
88
|
title: framework.entityDefinitions.discipline.title,
|
|
63
89
|
emoji: framework.entityDefinitions.discipline.emoji,
|
|
64
90
|
description: framework.entityDefinitions.discipline.description,
|
|
65
|
-
|
|
91
|
+
groups: prepareDisciplinesList(data.disciplines).groups,
|
|
66
92
|
mapper: disciplineToCardConfig,
|
|
93
|
+
isGrouped: true,
|
|
67
94
|
},
|
|
68
95
|
grade: {
|
|
69
96
|
title: framework.entityDefinitions.grade.title,
|
|
@@ -108,6 +135,15 @@ export function renderOverviewSlide({ render, data, params }) {
|
|
|
108
135
|
return;
|
|
109
136
|
}
|
|
110
137
|
|
|
138
|
+
// Render content based on whether it's grouped or flat
|
|
139
|
+
const contentElement = config.isGrouped
|
|
140
|
+
? createGroupedList(
|
|
141
|
+
config.groups,
|
|
142
|
+
config.mapper,
|
|
143
|
+
renderDisciplineGroupHeader,
|
|
144
|
+
)
|
|
145
|
+
: createCardList(config.entities, config.mapper, "No items found.");
|
|
146
|
+
|
|
111
147
|
const slide = div(
|
|
112
148
|
{ className: "slide overview-slide" },
|
|
113
149
|
div(
|
|
@@ -119,7 +155,7 @@ export function renderOverviewSlide({ render, data, params }) {
|
|
|
119
155
|
),
|
|
120
156
|
p({ className: "overview-description" }, config.description.trim()),
|
|
121
157
|
),
|
|
122
|
-
|
|
158
|
+
contentElement,
|
|
123
159
|
);
|
|
124
160
|
|
|
125
161
|
render(slide);
|
package/app/slides/progress.js
CHANGED
|
@@ -21,9 +21,11 @@ import { progressToDOM } from "../formatters/index.js";
|
|
|
21
21
|
export function renderProgressSlide({ render, data, params }) {
|
|
22
22
|
const discipline = data.disciplines.find((d) => d.id === params.discipline);
|
|
23
23
|
const grade = data.grades.find((g) => g.id === params.grade);
|
|
24
|
-
const track =
|
|
24
|
+
const track = params.track
|
|
25
|
+
? data.tracks.find((t) => t.id === params.track)
|
|
26
|
+
: null;
|
|
25
27
|
|
|
26
|
-
if (!discipline || !grade
|
|
28
|
+
if (!discipline || !grade) {
|
|
27
29
|
render(
|
|
28
30
|
div(
|
|
29
31
|
{ className: "slide-error" },
|
package/bin/pathway.js
CHANGED
|
@@ -31,14 +31,10 @@
|
|
|
31
31
|
import { fileURLToPath } from "url";
|
|
32
32
|
import { dirname, join, resolve } from "path";
|
|
33
33
|
import { existsSync } from "fs";
|
|
34
|
-
import {
|
|
35
|
-
loadAllData,
|
|
36
|
-
loadAgentData,
|
|
37
|
-
loadSkillsWithAgentData,
|
|
38
|
-
loadQuestionBankFromFolder,
|
|
39
|
-
} from "../app/model/loader.js";
|
|
34
|
+
import { loadAllData } from "../app/model/loader.js";
|
|
40
35
|
import { generateAllIndexes } from "../app/model/index-generator.js";
|
|
41
36
|
import { formatError } from "../app/lib/cli-output.js";
|
|
37
|
+
import { runSchemaValidation } from "../app/model/schema-validation.js";
|
|
42
38
|
|
|
43
39
|
// Import command handlers
|
|
44
40
|
import { runSkillCommand } from "../app/commands/skill.js";
|
|
@@ -260,7 +256,7 @@ function printHelp() {
|
|
|
260
256
|
}
|
|
261
257
|
|
|
262
258
|
/**
|
|
263
|
-
* Run full data validation
|
|
259
|
+
* Run full data validation using JSON schemas
|
|
264
260
|
* @param {string} dataDir - Path to data directory
|
|
265
261
|
*/
|
|
266
262
|
async function runFullValidation(dataDir) {
|
|
@@ -268,77 +264,34 @@ async function runFullValidation(dataDir) {
|
|
|
268
264
|
|
|
269
265
|
let hasErrors = false;
|
|
270
266
|
|
|
271
|
-
// Load
|
|
267
|
+
// Load data for referential integrity checking (without old validation)
|
|
272
268
|
const data = await loadAllData(dataDir, {
|
|
273
|
-
validate:
|
|
269
|
+
validate: false,
|
|
274
270
|
throwOnError: false,
|
|
275
271
|
});
|
|
276
272
|
|
|
277
|
-
|
|
278
|
-
|
|
273
|
+
// Run schema validation + referential integrity
|
|
274
|
+
const result = await runSchemaValidation(dataDir, data);
|
|
275
|
+
|
|
276
|
+
if (result.valid) {
|
|
277
|
+
console.log("✅ Schema validation passed");
|
|
279
278
|
} else {
|
|
280
|
-
console.log("❌
|
|
279
|
+
console.log("❌ Schema validation failed");
|
|
281
280
|
hasErrors = true;
|
|
282
|
-
for (const e of
|
|
283
|
-
console.log(
|
|
281
|
+
for (const e of result.errors) {
|
|
282
|
+
console.log(
|
|
283
|
+
` - [${e.type}] ${e.message}${e.path ? ` (${e.path})` : ""}`,
|
|
284
|
+
);
|
|
284
285
|
}
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
if (
|
|
288
|
+
if (result.warnings.length > 0) {
|
|
288
289
|
console.log("\n⚠️ Warnings:");
|
|
289
|
-
for (const w of
|
|
290
|
+
for (const w of result.warnings) {
|
|
290
291
|
console.log(` - [${w.type}] ${w.message}`);
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
// Validate question bank
|
|
295
|
-
try {
|
|
296
|
-
const questionBank = await loadQuestionBankFromFolder(
|
|
297
|
-
join(dataDir, "questions"),
|
|
298
|
-
data.skills,
|
|
299
|
-
data.behaviours,
|
|
300
|
-
{ validate: true, throwOnError: false },
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
if (questionBank.validation?.valid) {
|
|
304
|
-
console.log("✅ Question bank validation passed");
|
|
305
|
-
} else if (questionBank.validation) {
|
|
306
|
-
console.log("❌ Question bank validation failed");
|
|
307
|
-
hasErrors = true;
|
|
308
|
-
for (const e of questionBank.validation.errors) {
|
|
309
|
-
console.log(` - [${e.type}] ${e.message}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
} catch (err) {
|
|
313
|
-
console.log("⚠️ Could not validate question bank:", err.message);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Validate agent data
|
|
317
|
-
try {
|
|
318
|
-
const agentData = await loadAgentData(dataDir);
|
|
319
|
-
const skillsWithAgent = await loadSkillsWithAgentData(dataDir);
|
|
320
|
-
|
|
321
|
-
const skillsWithAgentCount = skillsWithAgent.filter((s) => s.agent).length;
|
|
322
|
-
|
|
323
|
-
console.log(
|
|
324
|
-
`✅ Agent data: ${agentData.disciplines.length} disciplines, ${agentData.tracks.length} tracks, ${skillsWithAgentCount} skills with agent sections`,
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
// Check for orphaned agent definitions
|
|
328
|
-
for (const d of agentData.disciplines) {
|
|
329
|
-
if (!data.disciplines.find((h) => h.id === d.id)) {
|
|
330
|
-
console.log(` ⚠️ Agent discipline '${d.id}' has no human definition`);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
for (const t of agentData.tracks) {
|
|
334
|
-
if (!data.tracks.find((h) => h.id === t.id)) {
|
|
335
|
-
console.log(` ⚠️ Agent track '${t.id}' has no human definition`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
} catch (err) {
|
|
339
|
-
console.log("⚠️ Could not validate agent data:", err.message);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
295
|
// Summary
|
|
343
296
|
console.log("\n📊 Data Summary:");
|
|
344
297
|
console.log(` Skills: ${data.skills?.length || 0}`);
|
|
@@ -347,6 +300,7 @@ async function runFullValidation(dataDir) {
|
|
|
347
300
|
console.log(` Tracks: ${data.tracks?.length || 0}`);
|
|
348
301
|
console.log(` Grades: ${data.grades?.length || 0}`);
|
|
349
302
|
console.log(` Drivers: ${data.drivers?.length || 0}`);
|
|
303
|
+
console.log(` Stages: ${data.stages?.length || 0}`);
|
|
350
304
|
console.log("");
|
|
351
305
|
|
|
352
306
|
return hasErrors ? 1 : 0;
|