@forwardimpact/pathway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/app/commands/agent.js +430 -0
- package/app/commands/behaviour.js +61 -0
- package/app/commands/command-factory.js +211 -0
- package/app/commands/discipline.js +58 -0
- package/app/commands/driver.js +94 -0
- package/app/commands/grade.js +60 -0
- package/app/commands/index.js +20 -0
- package/app/commands/init.js +67 -0
- package/app/commands/interview.js +68 -0
- package/app/commands/job.js +157 -0
- package/app/commands/progress.js +77 -0
- package/app/commands/questions.js +179 -0
- package/app/commands/serve.js +143 -0
- package/app/commands/site.js +121 -0
- package/app/commands/skill.js +76 -0
- package/app/commands/stage.js +129 -0
- package/app/commands/track.js +70 -0
- package/app/components/action-buttons.js +66 -0
- package/app/components/behaviour-profile.js +53 -0
- package/app/components/builder.js +341 -0
- package/app/components/card.js +98 -0
- package/app/components/checklist.js +145 -0
- package/app/components/comparison-radar.js +237 -0
- package/app/components/detail.js +230 -0
- package/app/components/error-page.js +72 -0
- package/app/components/grid.js +109 -0
- package/app/components/list.js +120 -0
- package/app/components/modifier-table.js +142 -0
- package/app/components/nav.js +64 -0
- package/app/components/progression-table.js +320 -0
- package/app/components/radar-chart.js +102 -0
- package/app/components/skill-matrix.js +97 -0
- package/app/css/base.css +56 -0
- package/app/css/bundles/app.css +40 -0
- package/app/css/bundles/handout.css +43 -0
- package/app/css/bundles/slides.css +40 -0
- package/app/css/components/badges.css +215 -0
- package/app/css/components/buttons.css +101 -0
- package/app/css/components/forms.css +105 -0
- package/app/css/components/layout.css +209 -0
- package/app/css/components/nav.css +166 -0
- package/app/css/components/progress.css +166 -0
- package/app/css/components/states.css +82 -0
- package/app/css/components/surfaces.css +243 -0
- package/app/css/components/tables.css +362 -0
- package/app/css/components/typography.css +122 -0
- package/app/css/components/utilities.css +41 -0
- package/app/css/pages/agent-builder.css +391 -0
- package/app/css/pages/assessment-results.css +453 -0
- package/app/css/pages/detail.css +59 -0
- package/app/css/pages/interview-builder.css +148 -0
- package/app/css/pages/job-builder.css +134 -0
- package/app/css/pages/landing.css +92 -0
- package/app/css/pages/lifecycle.css +118 -0
- package/app/css/pages/progress-builder.css +274 -0
- package/app/css/pages/self-assessment.css +502 -0
- package/app/css/reset.css +50 -0
- package/app/css/tokens.css +153 -0
- package/app/css/views/handout.css +30 -0
- package/app/css/views/print.css +608 -0
- package/app/css/views/slide-animations.css +113 -0
- package/app/css/views/slide-base.css +330 -0
- package/app/css/views/slide-sections.css +597 -0
- package/app/css/views/slide-tables.css +275 -0
- package/app/formatters/agent/dom.js +540 -0
- package/app/formatters/agent/profile.js +133 -0
- package/app/formatters/agent/skill.js +58 -0
- package/app/formatters/behaviour/dom.js +91 -0
- package/app/formatters/behaviour/markdown.js +54 -0
- package/app/formatters/behaviour/shared.js +64 -0
- package/app/formatters/discipline/dom.js +187 -0
- package/app/formatters/discipline/markdown.js +87 -0
- package/app/formatters/discipline/shared.js +131 -0
- package/app/formatters/driver/dom.js +103 -0
- package/app/formatters/driver/shared.js +92 -0
- package/app/formatters/grade/dom.js +208 -0
- package/app/formatters/grade/markdown.js +94 -0
- package/app/formatters/grade/shared.js +86 -0
- package/app/formatters/index.js +50 -0
- package/app/formatters/interview/dom.js +97 -0
- package/app/formatters/interview/markdown.js +66 -0
- package/app/formatters/interview/shared.js +332 -0
- package/app/formatters/job/description.js +176 -0
- package/app/formatters/job/dom.js +411 -0
- package/app/formatters/job/markdown.js +102 -0
- package/app/formatters/progress/dom.js +135 -0
- package/app/formatters/progress/markdown.js +86 -0
- package/app/formatters/progress/shared.js +339 -0
- package/app/formatters/questions/json.js +43 -0
- package/app/formatters/questions/markdown.js +303 -0
- package/app/formatters/questions/shared.js +274 -0
- package/app/formatters/questions/yaml.js +76 -0
- package/app/formatters/shared.js +71 -0
- package/app/formatters/skill/dom.js +168 -0
- package/app/formatters/skill/markdown.js +109 -0
- package/app/formatters/skill/shared.js +125 -0
- package/app/formatters/stage/dom.js +135 -0
- package/app/formatters/stage/index.js +12 -0
- package/app/formatters/stage/shared.js +111 -0
- package/app/formatters/track/dom.js +128 -0
- package/app/formatters/track/markdown.js +105 -0
- package/app/formatters/track/shared.js +181 -0
- package/app/handout-main.js +421 -0
- package/app/handout.html +21 -0
- package/app/index.html +59 -0
- package/app/lib/card-mappers.js +173 -0
- package/app/lib/cli-output.js +270 -0
- package/app/lib/error-boundary.js +70 -0
- package/app/lib/errors.js +49 -0
- package/app/lib/form-controls.js +47 -0
- package/app/lib/job-cache.js +86 -0
- package/app/lib/markdown.js +114 -0
- package/app/lib/radar.js +866 -0
- package/app/lib/reactive.js +77 -0
- package/app/lib/render.js +212 -0
- package/app/lib/router-core.js +160 -0
- package/app/lib/router-pages.js +16 -0
- package/app/lib/router-slides.js +202 -0
- package/app/lib/state.js +148 -0
- package/app/lib/utils.js +14 -0
- package/app/lib/yaml-loader.js +327 -0
- package/app/main.js +213 -0
- package/app/model/agent.js +702 -0
- package/app/model/checklist.js +137 -0
- package/app/model/derivation.js +699 -0
- package/app/model/index-generator.js +71 -0
- package/app/model/interview.js +539 -0
- package/app/model/job.js +222 -0
- package/app/model/levels.js +591 -0
- package/app/model/loader.js +564 -0
- package/app/model/matching.js +858 -0
- package/app/model/modifiers.js +158 -0
- package/app/model/profile.js +266 -0
- package/app/model/progression.js +507 -0
- package/app/model/validation.js +1385 -0
- package/app/pages/agent-builder.js +823 -0
- package/app/pages/assessment-results.js +507 -0
- package/app/pages/behaviour.js +70 -0
- package/app/pages/discipline.js +71 -0
- package/app/pages/driver.js +106 -0
- package/app/pages/grade.js +117 -0
- package/app/pages/interview-builder.js +50 -0
- package/app/pages/interview.js +304 -0
- package/app/pages/job-builder.js +50 -0
- package/app/pages/job.js +58 -0
- package/app/pages/landing.js +305 -0
- package/app/pages/progress-builder.js +58 -0
- package/app/pages/progress.js +495 -0
- package/app/pages/self-assessment.js +729 -0
- package/app/pages/skill.js +113 -0
- package/app/pages/stage.js +231 -0
- package/app/pages/track.js +69 -0
- package/app/slide-main.js +360 -0
- package/app/slides/behaviour.js +38 -0
- package/app/slides/chapter.js +82 -0
- package/app/slides/discipline.js +40 -0
- package/app/slides/driver.js +39 -0
- package/app/slides/grade.js +32 -0
- package/app/slides/index.js +198 -0
- package/app/slides/interview.js +58 -0
- package/app/slides/job.js +55 -0
- package/app/slides/overview.js +126 -0
- package/app/slides/progress.js +83 -0
- package/app/slides/skill.js +40 -0
- package/app/slides/track.js +39 -0
- package/app/slides.html +56 -0
- package/app/types.js +147 -0
- package/bin/pathway.js +489 -0
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
- package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
- package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
- package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
- package/examples/agents/.vscode/settings.json +8 -0
- package/examples/behaviours/_index.yaml +8 -0
- package/examples/behaviours/outcome_ownership.yaml +44 -0
- package/examples/behaviours/polymathic_knowledge.yaml +42 -0
- package/examples/behaviours/precise_communication.yaml +40 -0
- package/examples/behaviours/relentless_curiosity.yaml +38 -0
- package/examples/behaviours/systems_thinking.yaml +41 -0
- package/examples/capabilities/_index.yaml +8 -0
- package/examples/capabilities/business.yaml +251 -0
- package/examples/capabilities/delivery.yaml +352 -0
- package/examples/capabilities/people.yaml +100 -0
- package/examples/capabilities/reliability.yaml +318 -0
- package/examples/capabilities/scale.yaml +394 -0
- package/examples/disciplines/_index.yaml +5 -0
- package/examples/disciplines/data_engineering.yaml +76 -0
- package/examples/disciplines/software_engineering.yaml +76 -0
- package/examples/drivers.yaml +205 -0
- package/examples/framework.yaml +58 -0
- package/examples/grades.yaml +118 -0
- package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
- package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
- package/examples/questions/behaviours/precise_communication.yaml +55 -0
- package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
- package/examples/questions/behaviours/systems_thinking.yaml +53 -0
- package/examples/questions/skills/architecture_design.yaml +54 -0
- package/examples/questions/skills/cloud_platforms.yaml +48 -0
- package/examples/questions/skills/code_quality.yaml +49 -0
- package/examples/questions/skills/data_modeling.yaml +46 -0
- package/examples/questions/skills/devops.yaml +47 -0
- package/examples/questions/skills/full_stack_development.yaml +48 -0
- package/examples/questions/skills/sre_practices.yaml +44 -0
- package/examples/questions/skills/stakeholder_management.yaml +49 -0
- package/examples/questions/skills/team_collaboration.yaml +43 -0
- package/examples/questions/skills/technical_writing.yaml +43 -0
- package/examples/self-assessments.yaml +66 -0
- package/examples/stages.yaml +76 -0
- package/examples/tracks/_index.yaml +6 -0
- package/examples/tracks/manager.yaml +53 -0
- package/examples/tracks/platform.yaml +54 -0
- package/examples/tracks/sre.yaml +58 -0
- package/examples/vscode-settings.yaml +22 -0
- package/package.json +68 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Questions Markdown Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats questions for terminal output as tables and lists.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SKILL_LEVELS, BEHAVIOUR_MATURITIES } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Level abbreviations for compact display
|
|
11
|
+
*/
|
|
12
|
+
const LEVEL_ABBREVS = {
|
|
13
|
+
awareness: "aware",
|
|
14
|
+
foundational: "found",
|
|
15
|
+
working: "work",
|
|
16
|
+
practitioner: "pract",
|
|
17
|
+
expert: "expert",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maturity abbreviations for compact display
|
|
22
|
+
*/
|
|
23
|
+
const MATURITY_ABBREVS = {
|
|
24
|
+
emerging: "emerg",
|
|
25
|
+
developing: "dev",
|
|
26
|
+
practicing: "pract",
|
|
27
|
+
role_modeling: "role",
|
|
28
|
+
exemplifying: "exemp",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Truncate text to max length with ellipsis
|
|
33
|
+
* @param {string} text
|
|
34
|
+
* @param {number} maxLen
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function truncate(text, maxLen) {
|
|
38
|
+
if (text.length <= maxLen) return text;
|
|
39
|
+
return text.slice(0, maxLen - 1) + "…";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pad string to width
|
|
44
|
+
* @param {string} str
|
|
45
|
+
* @param {number} width
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function pad(str, width) {
|
|
49
|
+
return str.padEnd(width);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Format stats-only output
|
|
54
|
+
* @param {Object} view - Questions view
|
|
55
|
+
* @param {Array} skills - Skills data
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function formatStats(view, skills) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
const { stats } = view;
|
|
61
|
+
|
|
62
|
+
// Skill question counts
|
|
63
|
+
lines.push("SKILL QUESTION COUNTS");
|
|
64
|
+
lines.push("═".repeat(75));
|
|
65
|
+
lines.push("");
|
|
66
|
+
|
|
67
|
+
// Header
|
|
68
|
+
const skillHeader =
|
|
69
|
+
pad("Skill", 30) +
|
|
70
|
+
SKILL_LEVELS.map((l) => pad(LEVEL_ABBREVS[l], 7)).join("") +
|
|
71
|
+
"TOTAL";
|
|
72
|
+
lines.push(skillHeader);
|
|
73
|
+
lines.push("─".repeat(75));
|
|
74
|
+
|
|
75
|
+
// Rows
|
|
76
|
+
const sortedSkillIds = Object.keys(stats.skillStats).sort();
|
|
77
|
+
let skillTotal = 0;
|
|
78
|
+
const levelTotals = {};
|
|
79
|
+
|
|
80
|
+
for (const skillId of sortedSkillIds) {
|
|
81
|
+
const skillData = stats.skillStats[skillId];
|
|
82
|
+
const skill = skills.find((s) => s.id === skillId);
|
|
83
|
+
const name = skill ? truncate(skill.name, 28) : skillId;
|
|
84
|
+
|
|
85
|
+
const row =
|
|
86
|
+
pad(name, 30) +
|
|
87
|
+
SKILL_LEVELS.map((l) => {
|
|
88
|
+
const count = skillData[l] || 0;
|
|
89
|
+
levelTotals[l] = (levelTotals[l] || 0) + count;
|
|
90
|
+
return pad(String(count), 7);
|
|
91
|
+
}).join("") +
|
|
92
|
+
String(skillData.total || 0);
|
|
93
|
+
|
|
94
|
+
lines.push(row);
|
|
95
|
+
skillTotal += skillData.total || 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lines.push("─".repeat(75));
|
|
99
|
+
const totalsRow =
|
|
100
|
+
pad("TOTAL", 30) +
|
|
101
|
+
SKILL_LEVELS.map((l) => pad(String(levelTotals[l] || 0), 7)).join("") +
|
|
102
|
+
String(skillTotal);
|
|
103
|
+
lines.push(totalsRow);
|
|
104
|
+
lines.push("");
|
|
105
|
+
|
|
106
|
+
// Behaviour question counts
|
|
107
|
+
lines.push("BEHAVIOUR QUESTION COUNTS");
|
|
108
|
+
lines.push("═".repeat(75));
|
|
109
|
+
lines.push("");
|
|
110
|
+
|
|
111
|
+
const behaviourHeader =
|
|
112
|
+
pad("Behaviour", 30) +
|
|
113
|
+
BEHAVIOUR_MATURITIES.map((m) => pad(MATURITY_ABBREVS[m], 7)).join("") +
|
|
114
|
+
"TOTAL";
|
|
115
|
+
lines.push(behaviourHeader);
|
|
116
|
+
lines.push("─".repeat(75));
|
|
117
|
+
|
|
118
|
+
const sortedBehaviourIds = Object.keys(stats.behaviourStats).sort();
|
|
119
|
+
let behaviourTotal = 0;
|
|
120
|
+
const maturityTotals = {};
|
|
121
|
+
|
|
122
|
+
for (const behaviourId of sortedBehaviourIds) {
|
|
123
|
+
const behaviourData = stats.behaviourStats[behaviourId];
|
|
124
|
+
const name = truncate(behaviourId.replace(/_/g, " "), 28);
|
|
125
|
+
|
|
126
|
+
const row =
|
|
127
|
+
pad(name, 30) +
|
|
128
|
+
BEHAVIOUR_MATURITIES.map((m) => {
|
|
129
|
+
const count = behaviourData[m] || 0;
|
|
130
|
+
maturityTotals[m] = (maturityTotals[m] || 0) + count;
|
|
131
|
+
return pad(String(count), 7);
|
|
132
|
+
}).join("") +
|
|
133
|
+
String(behaviourData.total || 0);
|
|
134
|
+
|
|
135
|
+
lines.push(row);
|
|
136
|
+
behaviourTotal += behaviourData.total || 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lines.push("─".repeat(75));
|
|
140
|
+
const bTotalsRow =
|
|
141
|
+
pad("TOTAL", 30) +
|
|
142
|
+
BEHAVIOUR_MATURITIES.map((m) =>
|
|
143
|
+
pad(String(maturityTotals[m] || 0), 7),
|
|
144
|
+
).join("") +
|
|
145
|
+
String(behaviourTotal);
|
|
146
|
+
lines.push(bTotalsRow);
|
|
147
|
+
lines.push("");
|
|
148
|
+
|
|
149
|
+
// Identify gaps (skills with < 2 questions per level)
|
|
150
|
+
const gaps = [];
|
|
151
|
+
for (const skillId of sortedSkillIds) {
|
|
152
|
+
const skillData = stats.skillStats[skillId];
|
|
153
|
+
for (const level of SKILL_LEVELS) {
|
|
154
|
+
if ((skillData[level] || 0) < 1) {
|
|
155
|
+
gaps.push(`${skillId}: missing ${level} questions`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (gaps.length > 0) {
|
|
161
|
+
lines.push("⚠️ GAPS:");
|
|
162
|
+
for (const gap of gaps.slice(0, 10)) {
|
|
163
|
+
lines.push(` - ${gap}`);
|
|
164
|
+
}
|
|
165
|
+
if (gaps.length > 10) {
|
|
166
|
+
lines.push(` ... and ${gaps.length - 10} more`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Format table output for questions at a level/maturity
|
|
175
|
+
* @param {Object} view - Questions view
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
function formatTable(view) {
|
|
179
|
+
const lines = [];
|
|
180
|
+
const { filter, questions, stats } = view;
|
|
181
|
+
|
|
182
|
+
// Header
|
|
183
|
+
const levelOrMaturity = filter.level || filter.maturity || "ALL";
|
|
184
|
+
const sourceType = filter.level
|
|
185
|
+
? "skills"
|
|
186
|
+
: filter.maturity
|
|
187
|
+
? "behaviours"
|
|
188
|
+
: "sources";
|
|
189
|
+
lines.push(
|
|
190
|
+
`${levelOrMaturity.toUpperCase()} LEVEL QUESTIONS (${stats.totalQuestions} from ${Object.keys(stats.bySource).length} ${sourceType})`,
|
|
191
|
+
);
|
|
192
|
+
lines.push("═".repeat(80));
|
|
193
|
+
lines.push("");
|
|
194
|
+
|
|
195
|
+
// Table header
|
|
196
|
+
lines.push(pad("Source", 28) + " │ " + pad("Question", 45) + " │ Min");
|
|
197
|
+
lines.push("─".repeat(28) + "─┼─" + "─".repeat(45) + "─┼─" + "───");
|
|
198
|
+
|
|
199
|
+
// Group by source
|
|
200
|
+
const bySource = {};
|
|
201
|
+
for (const q of questions) {
|
|
202
|
+
if (!bySource[q.source]) {
|
|
203
|
+
bySource[q.source] = { name: q.sourceName, questions: [] };
|
|
204
|
+
}
|
|
205
|
+
bySource[q.source].questions.push(q);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const data of Object.values(bySource).sort((a, b) =>
|
|
209
|
+
a.name.localeCompare(b.name),
|
|
210
|
+
)) {
|
|
211
|
+
for (const q of data.questions) {
|
|
212
|
+
const source = truncate(data.name, 26);
|
|
213
|
+
const text = truncate(q.text, 43);
|
|
214
|
+
const mins = String(q.expectedDurationMinutes);
|
|
215
|
+
lines.push(pad(source, 28) + " │ " + pad(text, 45) + " │ " + mins);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
lines.push("");
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Format single source deep dive
|
|
225
|
+
* @param {Object} view - Questions view
|
|
226
|
+
* @returns {string}
|
|
227
|
+
*/
|
|
228
|
+
function formatSingleSource(view) {
|
|
229
|
+
const lines = [];
|
|
230
|
+
const { questions, stats } = view;
|
|
231
|
+
|
|
232
|
+
if (questions.length === 0) {
|
|
233
|
+
return "No questions found.";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const sourceName = questions[0].sourceName;
|
|
237
|
+
const sourceType = questions[0].sourceType;
|
|
238
|
+
|
|
239
|
+
lines.push(`${sourceName} QUESTIONS (${stats.totalQuestions} total)`);
|
|
240
|
+
lines.push("═".repeat(60));
|
|
241
|
+
lines.push("");
|
|
242
|
+
|
|
243
|
+
// Group by level/maturity
|
|
244
|
+
const byLevel = {};
|
|
245
|
+
for (const q of questions) {
|
|
246
|
+
if (!byLevel[q.level]) {
|
|
247
|
+
byLevel[q.level] = [];
|
|
248
|
+
}
|
|
249
|
+
byLevel[q.level].push(q);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const orderedLevels =
|
|
253
|
+
sourceType === "skill" ? SKILL_LEVELS : BEHAVIOUR_MATURITIES;
|
|
254
|
+
|
|
255
|
+
for (const level of orderedLevels) {
|
|
256
|
+
if (!byLevel[level]) continue;
|
|
257
|
+
|
|
258
|
+
lines.push(level.toUpperCase());
|
|
259
|
+
for (const q of byLevel[level]) {
|
|
260
|
+
lines.push(` • [${q.id}] ${q.text}`);
|
|
261
|
+
lines.push(` Duration: ${q.expectedDurationMinutes} min`);
|
|
262
|
+
if (q.lookingFor.length > 0) {
|
|
263
|
+
lines.push(" Looking for:");
|
|
264
|
+
for (const item of q.lookingFor) {
|
|
265
|
+
lines.push(` - ${item}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (q.followUps.length > 0) {
|
|
269
|
+
lines.push(" Follow-ups:");
|
|
270
|
+
for (const fu of q.followUps) {
|
|
271
|
+
lines.push(` → ${fu}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
lines.push("");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return lines.join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Format questions as markdown for terminal
|
|
283
|
+
* @param {Object} view - Questions view from prepareQuestionsView
|
|
284
|
+
* @param {Object} options - Format options
|
|
285
|
+
* @param {boolean} options.stats - Stats only
|
|
286
|
+
* @param {Array} options.skills - Skills data for name resolution
|
|
287
|
+
* @returns {string}
|
|
288
|
+
*/
|
|
289
|
+
export function questionsToMarkdown(view, options = {}) {
|
|
290
|
+
// Stats only mode
|
|
291
|
+
if (options.stats) {
|
|
292
|
+
return formatStats(view, options.skills || []);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Single source deep dive
|
|
296
|
+
const uniqueSources = new Set(view.questions.map((q) => q.source));
|
|
297
|
+
if (uniqueSources.size === 1) {
|
|
298
|
+
return formatSingleSource(view);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Table format (default)
|
|
302
|
+
return formatTable(view);
|
|
303
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Questions presentation helpers
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for formatting question data across output formats.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Skill levels in order
|
|
9
|
+
*/
|
|
10
|
+
export const SKILL_LEVELS = [
|
|
11
|
+
"awareness",
|
|
12
|
+
"foundational",
|
|
13
|
+
"working",
|
|
14
|
+
"practitioner",
|
|
15
|
+
"expert",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Behaviour maturities in order
|
|
20
|
+
*/
|
|
21
|
+
export const BEHAVIOUR_MATURITIES = [
|
|
22
|
+
"emerging",
|
|
23
|
+
"developing",
|
|
24
|
+
"practicing",
|
|
25
|
+
"role_modeling",
|
|
26
|
+
"exemplifying",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} QuestionsFilter
|
|
31
|
+
* @property {string|null} level - Skill level filter
|
|
32
|
+
* @property {string|null} maturity - Behaviour maturity filter
|
|
33
|
+
* @property {string[]|null} skills - Skill IDs to include
|
|
34
|
+
* @property {string[]|null} behaviours - Behaviour IDs to include
|
|
35
|
+
* @property {string|null} capability - Capability filter for skills
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} FlattenedQuestion
|
|
40
|
+
* @property {string} source - Source skill/behaviour ID
|
|
41
|
+
* @property {string} sourceName - Source skill/behaviour name
|
|
42
|
+
* @property {string} sourceType - 'skill' or 'behaviour'
|
|
43
|
+
* @property {string} level - Skill level or behaviour maturity
|
|
44
|
+
* @property {string} id - Question ID
|
|
45
|
+
* @property {string} text - Question text
|
|
46
|
+
* @property {string[]} lookingFor - Expected answer indicators
|
|
47
|
+
* @property {number} expectedDurationMinutes - Time estimate
|
|
48
|
+
* @property {string[]} [followUps] - Follow-up questions
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} QuestionsView
|
|
53
|
+
* @property {QuestionsFilter} filter - Applied filters
|
|
54
|
+
* @property {FlattenedQuestion[]} questions - Flattened questions
|
|
55
|
+
* @property {Object} stats - Question statistics
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse filter options from CLI args
|
|
60
|
+
* @param {Object} options - CLI options
|
|
61
|
+
* @returns {QuestionsFilter}
|
|
62
|
+
*/
|
|
63
|
+
export function parseFilters(options) {
|
|
64
|
+
return {
|
|
65
|
+
level: options.level || null,
|
|
66
|
+
maturity: options.maturity || null,
|
|
67
|
+
skills: options.skill ? options.skill.split(",") : null,
|
|
68
|
+
behaviours: options.behaviour ? options.behaviour.split(",") : null,
|
|
69
|
+
capability: options.capability || null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get skill name by ID
|
|
75
|
+
* @param {string} skillId
|
|
76
|
+
* @param {Array} skills
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function getSkillName(skillId, skills) {
|
|
80
|
+
const skill = skills.find((s) => s.id === skillId);
|
|
81
|
+
return skill ? skill.name : skillId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get behaviour name by ID
|
|
86
|
+
* @param {string} behaviourId
|
|
87
|
+
* @param {Array} behaviours
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
function getBehaviourName(behaviourId, behaviours) {
|
|
91
|
+
const behaviour = behaviours.find((b) => b.id === behaviourId);
|
|
92
|
+
return behaviour ? behaviour.name : behaviourId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get skill capability by ID
|
|
97
|
+
* @param {string} skillId
|
|
98
|
+
* @param {Array} skills
|
|
99
|
+
* @returns {string|null}
|
|
100
|
+
*/
|
|
101
|
+
function getSkillCapability(skillId, skills) {
|
|
102
|
+
const skill = skills.find((s) => s.id === skillId);
|
|
103
|
+
return skill ? skill.capability : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Flatten all questions from question bank
|
|
108
|
+
* @param {Object} questionBank
|
|
109
|
+
* @param {Array} skills
|
|
110
|
+
* @param {Array} behaviours
|
|
111
|
+
* @param {QuestionsFilter} filter
|
|
112
|
+
* @returns {FlattenedQuestion[]}
|
|
113
|
+
*/
|
|
114
|
+
export function flattenQuestions(questionBank, skills, behaviours, filter) {
|
|
115
|
+
const questions = [];
|
|
116
|
+
|
|
117
|
+
// Process skill questions
|
|
118
|
+
for (const [skillId, levels] of Object.entries(
|
|
119
|
+
questionBank.skillLevels || {},
|
|
120
|
+
)) {
|
|
121
|
+
const skillName = getSkillName(skillId, skills);
|
|
122
|
+
const capability = getSkillCapability(skillId, skills);
|
|
123
|
+
|
|
124
|
+
// Filter by skill IDs
|
|
125
|
+
if (filter.skills && !filter.skills.includes(skillId)) continue;
|
|
126
|
+
|
|
127
|
+
// Skip skills if filtering by specific behaviours
|
|
128
|
+
if (filter.behaviours) continue;
|
|
129
|
+
|
|
130
|
+
// Filter by capability
|
|
131
|
+
if (filter.capability && capability !== filter.capability) continue;
|
|
132
|
+
|
|
133
|
+
for (const [level, levelQuestions] of Object.entries(levels)) {
|
|
134
|
+
// Filter by level
|
|
135
|
+
if (filter.level && level !== filter.level) continue;
|
|
136
|
+
|
|
137
|
+
// Skip if filtering by maturity (behaviour-only filter)
|
|
138
|
+
if (filter.maturity) continue;
|
|
139
|
+
|
|
140
|
+
for (const q of levelQuestions) {
|
|
141
|
+
questions.push({
|
|
142
|
+
source: skillId,
|
|
143
|
+
sourceName: skillName,
|
|
144
|
+
sourceType: "skill",
|
|
145
|
+
level,
|
|
146
|
+
id: q.id,
|
|
147
|
+
text: q.text,
|
|
148
|
+
lookingFor: q.lookingFor || [],
|
|
149
|
+
expectedDurationMinutes: q.expectedDurationMinutes || 5,
|
|
150
|
+
followUps: q.followUps || [],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Process behaviour questions
|
|
157
|
+
for (const [behaviourId, maturities] of Object.entries(
|
|
158
|
+
questionBank.behaviourMaturities || {},
|
|
159
|
+
)) {
|
|
160
|
+
const behaviourName = getBehaviourName(behaviourId, behaviours);
|
|
161
|
+
|
|
162
|
+
// Filter by behaviour IDs
|
|
163
|
+
if (filter.behaviours && !filter.behaviours.includes(behaviourId)) continue;
|
|
164
|
+
|
|
165
|
+
// Skip behaviours if filtering by capability (skill-only filter)
|
|
166
|
+
if (filter.capability) continue;
|
|
167
|
+
|
|
168
|
+
// Skip behaviours if filtering by specific skills
|
|
169
|
+
if (filter.skills) continue;
|
|
170
|
+
|
|
171
|
+
for (const [maturity, maturityQuestions] of Object.entries(maturities)) {
|
|
172
|
+
// Filter by maturity
|
|
173
|
+
if (filter.maturity && maturity !== filter.maturity) continue;
|
|
174
|
+
|
|
175
|
+
// Skip if filtering by level (skill-only filter)
|
|
176
|
+
if (filter.level) continue;
|
|
177
|
+
|
|
178
|
+
for (const q of maturityQuestions) {
|
|
179
|
+
questions.push({
|
|
180
|
+
source: behaviourId,
|
|
181
|
+
sourceName: behaviourName,
|
|
182
|
+
sourceType: "behaviour",
|
|
183
|
+
level: maturity,
|
|
184
|
+
id: q.id,
|
|
185
|
+
text: q.text,
|
|
186
|
+
lookingFor: q.lookingFor || [],
|
|
187
|
+
expectedDurationMinutes: q.expectedDurationMinutes || 5,
|
|
188
|
+
followUps: q.followUps || [],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return questions;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Calculate question statistics
|
|
199
|
+
* @param {FlattenedQuestion[]} questions
|
|
200
|
+
* @param {Object} questionBank
|
|
201
|
+
* @returns {Object}
|
|
202
|
+
*/
|
|
203
|
+
export function calculateStats(questions, questionBank) {
|
|
204
|
+
const bySource = {};
|
|
205
|
+
const byLevel = {};
|
|
206
|
+
|
|
207
|
+
for (const q of questions) {
|
|
208
|
+
bySource[q.source] = (bySource[q.source] || 0) + 1;
|
|
209
|
+
byLevel[q.level] = (byLevel[q.level] || 0) + 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Calculate full stats for skills and behaviours
|
|
213
|
+
const skillStats = {};
|
|
214
|
+
for (const [skillId, levels] of Object.entries(
|
|
215
|
+
questionBank.skillLevels || {},
|
|
216
|
+
)) {
|
|
217
|
+
skillStats[skillId] = {};
|
|
218
|
+
for (const level of SKILL_LEVELS) {
|
|
219
|
+
skillStats[skillId][level] = (levels[level] || []).length;
|
|
220
|
+
}
|
|
221
|
+
skillStats[skillId].total = Object.values(skillStats[skillId]).reduce(
|
|
222
|
+
(a, b) => a + b,
|
|
223
|
+
0,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const behaviourStats = {};
|
|
228
|
+
for (const [behaviourId, maturities] of Object.entries(
|
|
229
|
+
questionBank.behaviourMaturities || {},
|
|
230
|
+
)) {
|
|
231
|
+
behaviourStats[behaviourId] = {};
|
|
232
|
+
for (const maturity of BEHAVIOUR_MATURITIES) {
|
|
233
|
+
behaviourStats[behaviourId][maturity] = (
|
|
234
|
+
maturities[maturity] || []
|
|
235
|
+
).length;
|
|
236
|
+
}
|
|
237
|
+
behaviourStats[behaviourId].total = Object.values(
|
|
238
|
+
behaviourStats[behaviourId],
|
|
239
|
+
).reduce((a, b) => a + b, 0);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
totalQuestions: questions.length,
|
|
244
|
+
bySource,
|
|
245
|
+
byLevel,
|
|
246
|
+
skillStats,
|
|
247
|
+
behaviourStats,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Prepare questions view for rendering
|
|
253
|
+
* @param {Object} params
|
|
254
|
+
* @param {Object} params.questionBank
|
|
255
|
+
* @param {Array} params.skills
|
|
256
|
+
* @param {Array} params.behaviours
|
|
257
|
+
* @param {QuestionsFilter} params.filter
|
|
258
|
+
* @returns {QuestionsView}
|
|
259
|
+
*/
|
|
260
|
+
export function prepareQuestionsView({
|
|
261
|
+
questionBank,
|
|
262
|
+
skills,
|
|
263
|
+
behaviours,
|
|
264
|
+
filter,
|
|
265
|
+
}) {
|
|
266
|
+
const questions = flattenQuestions(questionBank, skills, behaviours, filter);
|
|
267
|
+
const stats = calculateStats(questions, questionBank);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
filter,
|
|
271
|
+
questions,
|
|
272
|
+
stats,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Questions YAML Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats questions as YAML for export and bulk editing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { stringify } from "yaml";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format questions as YAML
|
|
11
|
+
* @param {Object} view - Questions view from prepareQuestionsView
|
|
12
|
+
* @param {Object} options - Format options
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function questionsToYaml(view, _options = {}) {
|
|
16
|
+
const { filter, questions } = view;
|
|
17
|
+
|
|
18
|
+
// Build header comment
|
|
19
|
+
const filterParts = [];
|
|
20
|
+
if (filter.level) filterParts.push(`--level=${filter.level}`);
|
|
21
|
+
if (filter.maturity) filterParts.push(`--maturity=${filter.maturity}`);
|
|
22
|
+
if (filter.skills) filterParts.push(`--skill=${filter.skills.join(",")}`);
|
|
23
|
+
if (filter.behaviours)
|
|
24
|
+
filterParts.push(`--behaviour=${filter.behaviours.join(",")}`);
|
|
25
|
+
if (filter.capability) filterParts.push(`--capability=${filter.capability}`);
|
|
26
|
+
|
|
27
|
+
const filterStr =
|
|
28
|
+
filterParts.length > 0 ? filterParts.join(" ") : "(no filters)";
|
|
29
|
+
const header = `# Generated by: npx pathway questions ${filterStr}\n# Questions: ${questions.length}\n\n`;
|
|
30
|
+
|
|
31
|
+
// Group questions by source
|
|
32
|
+
const bySource = {};
|
|
33
|
+
for (const q of questions) {
|
|
34
|
+
if (!bySource[q.source]) {
|
|
35
|
+
bySource[q.source] = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build question object for YAML
|
|
39
|
+
const yamlQuestion = {
|
|
40
|
+
id: q.id,
|
|
41
|
+
text: q.text,
|
|
42
|
+
lookingFor: q.lookingFor,
|
|
43
|
+
expectedDurationMinutes: q.expectedDurationMinutes,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (q.followUps.length > 0) {
|
|
47
|
+
yamlQuestion.followUps = q.followUps;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
bySource[q.source].push({
|
|
51
|
+
level: q.level,
|
|
52
|
+
question: yamlQuestion,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build output structure grouped by source then level
|
|
57
|
+
const output = {};
|
|
58
|
+
for (const [sourceId, items] of Object.entries(bySource).sort()) {
|
|
59
|
+
output[sourceId] = {};
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
if (!output[sourceId][item.level]) {
|
|
62
|
+
output[sourceId][item.level] = [];
|
|
63
|
+
}
|
|
64
|
+
output[sourceId][item.level].push(item.question);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Stringify with nice formatting
|
|
69
|
+
const yamlContent = stringify(output, {
|
|
70
|
+
lineWidth: 80,
|
|
71
|
+
defaultKeyType: "PLAIN",
|
|
72
|
+
defaultStringType: "PLAIN",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return header + yamlContent;
|
|
76
|
+
}
|