@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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Output Formatting Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent formatting for terminal output including colors,
|
|
5
|
+
* tables, headers, and level formatting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ANSI color codes
|
|
9
|
+
export const colors = {
|
|
10
|
+
reset: "\x1b[0m",
|
|
11
|
+
bold: "\x1b[1m",
|
|
12
|
+
dim: "\x1b[2m",
|
|
13
|
+
italic: "\x1b[3m",
|
|
14
|
+
underline: "\x1b[4m",
|
|
15
|
+
red: "\x1b[31m",
|
|
16
|
+
green: "\x1b[32m",
|
|
17
|
+
yellow: "\x1b[33m",
|
|
18
|
+
blue: "\x1b[34m",
|
|
19
|
+
magenta: "\x1b[35m",
|
|
20
|
+
cyan: "\x1b[36m",
|
|
21
|
+
white: "\x1b[37m",
|
|
22
|
+
gray: "\x1b[90m",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if stdout supports colors
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
export function supportsColor() {
|
|
30
|
+
if (process.env.NO_COLOR) return false;
|
|
31
|
+
if (process.env.FORCE_COLOR) return true;
|
|
32
|
+
return process.stdout.isTTY;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wrap text with color if supported
|
|
37
|
+
* @param {string} text
|
|
38
|
+
* @param {string} color
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function colorize(text, color) {
|
|
42
|
+
if (!supportsColor()) return text;
|
|
43
|
+
return `${color}${text}${colors.reset}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format a header
|
|
48
|
+
* @param {string} text
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function formatHeader(text) {
|
|
52
|
+
return colorize(text, colors.bold + colors.cyan);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a subheader
|
|
57
|
+
* @param {string} text
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function formatSubheader(text) {
|
|
61
|
+
return colorize(text, colors.bold);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format a list item
|
|
66
|
+
* @param {string} label
|
|
67
|
+
* @param {string} value
|
|
68
|
+
* @param {number} [indent=0]
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export function formatListItem(label, value, indent = 0) {
|
|
72
|
+
const padding = " ".repeat(indent);
|
|
73
|
+
const bullet = colorize("•", colors.dim);
|
|
74
|
+
return `${padding}${bullet} ${label}: ${value}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format a bullet item (no label)
|
|
79
|
+
* @param {string} text
|
|
80
|
+
* @param {number} [indent=0]
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
export function formatBullet(text, indent = 0) {
|
|
84
|
+
const padding = " ".repeat(indent);
|
|
85
|
+
const bullet = colorize("•", colors.dim);
|
|
86
|
+
return `${padding}${bullet} ${text}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Format a table
|
|
91
|
+
* @param {string[]} headers
|
|
92
|
+
* @param {Array<Array<string|number>>} rows
|
|
93
|
+
* @param {Object} [options]
|
|
94
|
+
* @param {boolean} [options.compact=false]
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function formatTable(headers, rows, options = {}) {
|
|
98
|
+
const { compact = false } = options;
|
|
99
|
+
|
|
100
|
+
// Calculate column widths
|
|
101
|
+
const widths = headers.map((h, i) =>
|
|
102
|
+
Math.max(String(h).length, ...rows.map((r) => String(r[i] || "").length)),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const lines = [];
|
|
106
|
+
|
|
107
|
+
// Header
|
|
108
|
+
const headerLine = headers
|
|
109
|
+
.map((h, i) => String(h).padEnd(widths[i]))
|
|
110
|
+
.join(" ");
|
|
111
|
+
lines.push(colorize(headerLine, colors.bold));
|
|
112
|
+
|
|
113
|
+
// Separator
|
|
114
|
+
if (!compact) {
|
|
115
|
+
lines.push(widths.map((w) => "─".repeat(w)).join("──"));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Rows
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
lines.push(
|
|
121
|
+
row.map((cell, i) => String(cell || "").padEnd(widths[i])).join(" "),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Format skill level with color
|
|
130
|
+
* @param {string} level
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
export function formatSkillLevel(level) {
|
|
134
|
+
const levelColors = {
|
|
135
|
+
awareness: colors.gray,
|
|
136
|
+
foundational: colors.blue,
|
|
137
|
+
working: colors.green,
|
|
138
|
+
practitioner: colors.yellow,
|
|
139
|
+
expert: colors.magenta,
|
|
140
|
+
};
|
|
141
|
+
const color = levelColors[level] || colors.reset;
|
|
142
|
+
return colorize(level, color);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Format behaviour maturity with color
|
|
147
|
+
* @param {string} maturity
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function formatBehaviourMaturity(maturity) {
|
|
151
|
+
const maturityColors = {
|
|
152
|
+
emerging: colors.gray,
|
|
153
|
+
developing: colors.blue,
|
|
154
|
+
practicing: colors.green,
|
|
155
|
+
role_modeling: colors.yellow,
|
|
156
|
+
exemplifying: colors.magenta,
|
|
157
|
+
};
|
|
158
|
+
const color = maturityColors[maturity] || colors.reset;
|
|
159
|
+
const displayName = maturity.replace(/_/g, " ");
|
|
160
|
+
return colorize(displayName, color);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format a modifier value (+1, 0, -1)
|
|
165
|
+
* @param {number} modifier
|
|
166
|
+
* @returns {string}
|
|
167
|
+
*/
|
|
168
|
+
export function formatModifier(modifier) {
|
|
169
|
+
if (modifier > 0) {
|
|
170
|
+
return colorize(`+${modifier}`, colors.green);
|
|
171
|
+
} else if (modifier < 0) {
|
|
172
|
+
return colorize(String(modifier), colors.red);
|
|
173
|
+
}
|
|
174
|
+
return colorize("0", colors.dim);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format a percentage
|
|
179
|
+
* @param {number} value - Value between 0 and 1
|
|
180
|
+
* @returns {string}
|
|
181
|
+
*/
|
|
182
|
+
export function formatPercent(value) {
|
|
183
|
+
const percent = Math.round(value * 100);
|
|
184
|
+
let color;
|
|
185
|
+
if (percent >= 80) {
|
|
186
|
+
color = colors.green;
|
|
187
|
+
} else if (percent >= 50) {
|
|
188
|
+
color = colors.yellow;
|
|
189
|
+
} else {
|
|
190
|
+
color = colors.red;
|
|
191
|
+
}
|
|
192
|
+
return colorize(`${percent}%`, color);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Format a change indicator (↑, ↓, →)
|
|
197
|
+
* @param {number} change
|
|
198
|
+
* @returns {string}
|
|
199
|
+
*/
|
|
200
|
+
export function formatChange(change) {
|
|
201
|
+
if (change > 0) {
|
|
202
|
+
return colorize(`↑${change}`, colors.green);
|
|
203
|
+
} else if (change < 0) {
|
|
204
|
+
return colorize(`↓${Math.abs(change)}`, colors.red);
|
|
205
|
+
}
|
|
206
|
+
return colorize("→", colors.dim);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Format an error message
|
|
211
|
+
* @param {string} message
|
|
212
|
+
* @returns {string}
|
|
213
|
+
*/
|
|
214
|
+
export function formatError(message) {
|
|
215
|
+
return colorize(`Error: ${message}`, colors.red);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Format a success message
|
|
220
|
+
* @param {string} message
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
export function formatSuccess(message) {
|
|
224
|
+
return colorize(message, colors.green);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Format a warning message
|
|
229
|
+
* @param {string} message
|
|
230
|
+
/**
|
|
231
|
+
* Format a warning message
|
|
232
|
+
* @param {string} message
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
export function formatWarning(message) {
|
|
236
|
+
return colorize(`Warning: ${message}`, colors.yellow);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a horizontal rule
|
|
241
|
+
* @param {number} [width=60]
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
export function horizontalRule(width = 60) {
|
|
245
|
+
return colorize("─".repeat(width), colors.dim);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Format a section with title and content
|
|
250
|
+
* @param {string} title
|
|
251
|
+
* @param {string} content
|
|
252
|
+
* @returns {string}
|
|
253
|
+
*/
|
|
254
|
+
export function formatSection(title, content) {
|
|
255
|
+
return `${formatHeader(title)}\n\n${content}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Indent all lines of text
|
|
260
|
+
* @param {string} text
|
|
261
|
+
* @param {number} [spaces=2]
|
|
262
|
+
* @returns {string}
|
|
263
|
+
*/
|
|
264
|
+
export function indent(text, spaces = 2) {
|
|
265
|
+
const padding = " ".repeat(spaces);
|
|
266
|
+
return text
|
|
267
|
+
.split("\n")
|
|
268
|
+
.map((line) => padding + line)
|
|
269
|
+
.join("\n");
|
|
270
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error boundary wrapper for page rendering
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderNotFound, renderError } from "../components/error-page.js";
|
|
6
|
+
import { NotFoundError, InvalidCombinationError } from "./errors.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} ErrorBoundaryOptions
|
|
10
|
+
* @property {(error: Error) => void} [onError] - Error callback for logging
|
|
11
|
+
* @property {string} [backPath] - Default back path
|
|
12
|
+
* @property {string} [backText] - Default back text
|
|
13
|
+
* @property {(title: string, message: string) => void} [renderErrorFn] - Custom error renderer
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wrap a render function with error handling
|
|
18
|
+
* @param {Function} renderFn - Page render function
|
|
19
|
+
* @param {ErrorBoundaryOptions} [options]
|
|
20
|
+
* @returns {Function}
|
|
21
|
+
*/
|
|
22
|
+
export function withErrorBoundary(renderFn, options = {}) {
|
|
23
|
+
const errorRenderer =
|
|
24
|
+
options.renderErrorFn ||
|
|
25
|
+
((title, message) => {
|
|
26
|
+
renderError({
|
|
27
|
+
title,
|
|
28
|
+
message,
|
|
29
|
+
backPath: options.backPath || "/",
|
|
30
|
+
backText: options.backText || "← Back to Home",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return (...args) => {
|
|
35
|
+
try {
|
|
36
|
+
return renderFn(...args);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Page render error:", error);
|
|
39
|
+
|
|
40
|
+
options.onError?.(error);
|
|
41
|
+
|
|
42
|
+
if (error instanceof NotFoundError) {
|
|
43
|
+
if (options.renderErrorFn) {
|
|
44
|
+
errorRenderer(
|
|
45
|
+
`${error.entityType} Not Found`,
|
|
46
|
+
`No ${error.entityType.toLowerCase()} found with ID: ${error.entityId}`,
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
renderNotFound({
|
|
50
|
+
entityType: error.entityType,
|
|
51
|
+
entityId: error.entityId,
|
|
52
|
+
backPath: error.backPath,
|
|
53
|
+
backText: `← Back to ${error.entityType}s`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (error instanceof InvalidCombinationError) {
|
|
60
|
+
errorRenderer("Invalid Combination", error.message);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
errorRenderer(
|
|
65
|
+
"Something Went Wrong",
|
|
66
|
+
error.message || "An unexpected error occurred.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error types for the application
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Entity not found error
|
|
7
|
+
*/
|
|
8
|
+
export class NotFoundError extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} entityType - Type of entity (e.g., 'Skill', 'Behaviour')
|
|
11
|
+
* @param {string} entityId - ID that was not found
|
|
12
|
+
* @param {string} backPath - Path to navigate back to
|
|
13
|
+
*/
|
|
14
|
+
constructor(entityType, entityId, backPath) {
|
|
15
|
+
super(`${entityType} not found: ${entityId}`);
|
|
16
|
+
this.name = "NotFoundError";
|
|
17
|
+
this.entityType = entityType;
|
|
18
|
+
this.entityId = entityId;
|
|
19
|
+
this.backPath = backPath;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Invalid combination error (e.g., invalid job configuration)
|
|
25
|
+
*/
|
|
26
|
+
export class InvalidCombinationError extends Error {
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} message - Error message
|
|
29
|
+
* @param {string} backPath - Path to navigate back to
|
|
30
|
+
*/
|
|
31
|
+
constructor(message, backPath) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "InvalidCombinationError";
|
|
34
|
+
this.backPath = backPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Data loading error
|
|
40
|
+
*/
|
|
41
|
+
export class DataLoadError extends Error {
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} message - Error message
|
|
44
|
+
*/
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "DataLoadError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable form control components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { select, option } from "./render.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a select element with initial value and change handler
|
|
9
|
+
* @param {Object} options - Configuration options
|
|
10
|
+
* @param {string} options.id - Element ID
|
|
11
|
+
* @param {Array} options.items - Array of items to display
|
|
12
|
+
* @param {string} options.initialValue - Initial selected value
|
|
13
|
+
* @param {string} options.placeholder - Placeholder text for empty option
|
|
14
|
+
* @param {Function} options.onChange - Callback when selection changes
|
|
15
|
+
* @param {Function} [options.getDisplayName] - Optional function to get display name from item
|
|
16
|
+
* @returns {HTMLElement}
|
|
17
|
+
*/
|
|
18
|
+
export function createSelectWithValue({
|
|
19
|
+
id,
|
|
20
|
+
items,
|
|
21
|
+
initialValue,
|
|
22
|
+
placeholder,
|
|
23
|
+
onChange,
|
|
24
|
+
getDisplayName,
|
|
25
|
+
}) {
|
|
26
|
+
const displayFn = getDisplayName || ((item) => item.name);
|
|
27
|
+
const selectEl = select(
|
|
28
|
+
{
|
|
29
|
+
className: "form-select",
|
|
30
|
+
id,
|
|
31
|
+
},
|
|
32
|
+
option({ value: "" }, placeholder),
|
|
33
|
+
...items.map((item) => {
|
|
34
|
+
const opt = option({ value: item.id }, displayFn(item));
|
|
35
|
+
if (item.id === initialValue) {
|
|
36
|
+
opt.selected = true;
|
|
37
|
+
}
|
|
38
|
+
return opt;
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
selectEl.addEventListener("change", (e) => {
|
|
43
|
+
onChange(e.target.value);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return selectEl;
|
|
47
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Cache
|
|
3
|
+
*
|
|
4
|
+
* Centralized caching for generated job definitions.
|
|
5
|
+
* Provides consistent key generation and get-or-create pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { deriveJob } from "../model/derivation.js";
|
|
9
|
+
|
|
10
|
+
/** @type {Map<string, Object>} */
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a consistent cache key from job parameters
|
|
15
|
+
* @param {string} disciplineId
|
|
16
|
+
* @param {string} trackId
|
|
17
|
+
* @param {string} gradeId
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function makeJobKey(disciplineId, trackId, gradeId) {
|
|
21
|
+
return `${disciplineId}_${trackId}_${gradeId}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get or create a cached job definition
|
|
26
|
+
* @param {Object} params
|
|
27
|
+
* @param {Object} params.discipline
|
|
28
|
+
* @param {Object} params.grade
|
|
29
|
+
* @param {Object} params.track
|
|
30
|
+
* @param {Array} params.skills
|
|
31
|
+
* @param {Array} params.behaviours
|
|
32
|
+
* @param {Array} [params.capabilities]
|
|
33
|
+
* @returns {Object|null}
|
|
34
|
+
*/
|
|
35
|
+
export function getOrCreateJob({
|
|
36
|
+
discipline,
|
|
37
|
+
grade,
|
|
38
|
+
track,
|
|
39
|
+
skills,
|
|
40
|
+
behaviours,
|
|
41
|
+
capabilities,
|
|
42
|
+
}) {
|
|
43
|
+
const key = makeJobKey(discipline.id, track.id, grade.id);
|
|
44
|
+
|
|
45
|
+
if (!cache.has(key)) {
|
|
46
|
+
const job = deriveJob({
|
|
47
|
+
discipline,
|
|
48
|
+
grade,
|
|
49
|
+
track,
|
|
50
|
+
skills,
|
|
51
|
+
behaviours,
|
|
52
|
+
capabilities,
|
|
53
|
+
});
|
|
54
|
+
if (job) {
|
|
55
|
+
cache.set(key, job);
|
|
56
|
+
}
|
|
57
|
+
return job;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return cache.get(key);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Clear all cached jobs
|
|
65
|
+
*/
|
|
66
|
+
export function clearJobCache() {
|
|
67
|
+
cache.clear();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Invalidate a specific job from the cache
|
|
72
|
+
* @param {string} disciplineId
|
|
73
|
+
* @param {string} trackId
|
|
74
|
+
* @param {string} gradeId
|
|
75
|
+
*/
|
|
76
|
+
export function invalidateJob(disciplineId, trackId, gradeId) {
|
|
77
|
+
cache.delete(makeJobKey(disciplineId, trackId, gradeId));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the number of cached jobs (for testing/debugging)
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
export function getCacheSize() {
|
|
85
|
+
return cache.size;
|
|
86
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Markdown to HTML converter
|
|
3
|
+
*
|
|
4
|
+
* Converts common markdown syntax to HTML. Designed for job descriptions
|
|
5
|
+
* with headings, lists, bold text, and paragraphs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert markdown text to HTML
|
|
10
|
+
* @param {string} markdown - The markdown text to convert
|
|
11
|
+
* @returns {string} HTML string
|
|
12
|
+
*/
|
|
13
|
+
export function markdownToHtml(markdown) {
|
|
14
|
+
const lines = markdown.split("\n");
|
|
15
|
+
const htmlLines = [];
|
|
16
|
+
let inList = false;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
let line = lines[i];
|
|
20
|
+
|
|
21
|
+
// Skip empty lines but close list if open
|
|
22
|
+
if (line.trim() === "") {
|
|
23
|
+
if (inList) {
|
|
24
|
+
htmlLines.push("</ul>");
|
|
25
|
+
inList = false;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// H1 heading
|
|
31
|
+
if (line.startsWith("# ")) {
|
|
32
|
+
if (inList) {
|
|
33
|
+
htmlLines.push("</ul>");
|
|
34
|
+
inList = false;
|
|
35
|
+
}
|
|
36
|
+
htmlLines.push(`<h1>${escapeHtml(line.slice(2))}</h1>`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// H2 heading
|
|
41
|
+
if (line.startsWith("## ")) {
|
|
42
|
+
if (inList) {
|
|
43
|
+
htmlLines.push("</ul>");
|
|
44
|
+
inList = false;
|
|
45
|
+
}
|
|
46
|
+
htmlLines.push(`<h2>${escapeHtml(line.slice(3))}</h2>`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// H3 heading
|
|
51
|
+
if (line.startsWith("### ")) {
|
|
52
|
+
if (inList) {
|
|
53
|
+
htmlLines.push("</ul>");
|
|
54
|
+
inList = false;
|
|
55
|
+
}
|
|
56
|
+
htmlLines.push(`<h3>${escapeHtml(line.slice(4))}</h3>`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// List item
|
|
61
|
+
if (line.startsWith("- ")) {
|
|
62
|
+
if (!inList) {
|
|
63
|
+
htmlLines.push("<ul>");
|
|
64
|
+
inList = true;
|
|
65
|
+
}
|
|
66
|
+
const content = formatInlineMarkdown(line.slice(2));
|
|
67
|
+
htmlLines.push(`<li>${content}</li>`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Regular paragraph
|
|
72
|
+
if (inList) {
|
|
73
|
+
htmlLines.push("</ul>");
|
|
74
|
+
inList = false;
|
|
75
|
+
}
|
|
76
|
+
htmlLines.push(`<p>${formatInlineMarkdown(line)}</p>`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Close any open list
|
|
80
|
+
if (inList) {
|
|
81
|
+
htmlLines.push("</ul>");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return htmlLines.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Escape HTML special characters
|
|
89
|
+
* @param {string} text - Text to escape
|
|
90
|
+
* @returns {string} Escaped text
|
|
91
|
+
*/
|
|
92
|
+
function escapeHtml(text) {
|
|
93
|
+
return text
|
|
94
|
+
.replace(/&/g, "&")
|
|
95
|
+
.replace(/</g, "<")
|
|
96
|
+
.replace(/>/g, ">")
|
|
97
|
+
.replace(/"/g, """)
|
|
98
|
+
.replace(/'/g, "'");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Format inline markdown (bold text)
|
|
103
|
+
* @param {string} text - Text to format
|
|
104
|
+
* @returns {string} HTML formatted text
|
|
105
|
+
*/
|
|
106
|
+
function formatInlineMarkdown(text) {
|
|
107
|
+
// First escape HTML
|
|
108
|
+
let result = escapeHtml(text);
|
|
109
|
+
|
|
110
|
+
// Convert **bold** to <strong>bold</strong>
|
|
111
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|