@forwardimpact/model 0.5.0 → 0.7.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.
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Orderings and Comparator Functions
3
+ *
4
+ * Canonical orderings for entity types and comparator functions for sorting.
5
+ *
6
+ * Naming conventions:
7
+ * - ORDER_* - canonical ordering arrays
8
+ * - compareBy* - comparator functions for Array.sort()
9
+ */
10
+
11
+ import {
12
+ getSkillLevelIndex,
13
+ getBehaviourMaturityIndex,
14
+ getCapabilityOrder,
15
+ } from "@forwardimpact/schema/levels";
16
+
17
+ // =============================================================================
18
+ // Canonical Orderings
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Skill type ordering (T-shaped profile: core → broad)
23
+ * Primary skills first, then secondary, broad, and track-added skills.
24
+ */
25
+ export const ORDER_SKILL_TYPE = ["primary", "secondary", "broad", "track"];
26
+
27
+ /**
28
+ * Engineering lifecycle stages in execution order
29
+ */
30
+ export const ORDER_STAGE = ["specify", "plan", "code", "review", "deploy"];
31
+
32
+ /**
33
+ * Agent stage ordering (subset used for agent generation)
34
+ */
35
+ export const ORDER_AGENT_STAGE = ["plan", "code", "review"];
36
+
37
+ // =============================================================================
38
+ // Skill Comparators
39
+ // =============================================================================
40
+
41
+ /**
42
+ * Compare skills by level descending (higher level first)
43
+ * @param {Object} a - First skill entry
44
+ * @param {Object} b - Second skill entry
45
+ * @returns {number} Comparison result
46
+ */
47
+ export function compareByLevelDesc(a, b) {
48
+ return getSkillLevelIndex(b.level) - getSkillLevelIndex(a.level);
49
+ }
50
+
51
+ /**
52
+ * Compare skills by level ascending (lower level first)
53
+ * @param {Object} a - First skill entry
54
+ * @param {Object} b - Second skill entry
55
+ * @returns {number} Comparison result
56
+ */
57
+ export function compareByLevelAsc(a, b) {
58
+ return getSkillLevelIndex(a.level) - getSkillLevelIndex(b.level);
59
+ }
60
+
61
+ /**
62
+ * Compare skills by type (primary first)
63
+ * @param {Object} a - First skill entry
64
+ * @param {Object} b - Second skill entry
65
+ * @returns {number} Comparison result
66
+ */
67
+ export function compareByType(a, b) {
68
+ return ORDER_SKILL_TYPE.indexOf(a.type) - ORDER_SKILL_TYPE.indexOf(b.type);
69
+ }
70
+
71
+ /**
72
+ * Compare skills by name alphabetically
73
+ * @param {Object} a - First skill entry
74
+ * @param {Object} b - Second skill entry
75
+ * @returns {number} Comparison result
76
+ */
77
+ export function compareByName(a, b) {
78
+ const nameA = a.skillName || a.name;
79
+ const nameB = b.skillName || b.name;
80
+ return nameA.localeCompare(nameB);
81
+ }
82
+
83
+ /**
84
+ * Compare skills by level (desc), then type (asc), then name (asc)
85
+ *
86
+ * Standard priority ordering for skill display:
87
+ * - Higher levels first
88
+ * - Within same level, primary before secondary before broad
89
+ * - Within same type, alphabetical by name
90
+ *
91
+ * @param {Object} a - First skill entry
92
+ * @param {Object} b - Second skill entry
93
+ * @returns {number} Comparison result
94
+ */
95
+ export function compareBySkillPriority(a, b) {
96
+ // Level descending (higher level first)
97
+ const levelDiff = getSkillLevelIndex(b.level) - getSkillLevelIndex(a.level);
98
+ if (levelDiff !== 0) return levelDiff;
99
+
100
+ // Type ascending (primary first)
101
+ const typeA = ORDER_SKILL_TYPE.indexOf(a.type);
102
+ const typeB = ORDER_SKILL_TYPE.indexOf(b.type);
103
+ if (typeA !== typeB) return typeA - typeB;
104
+
105
+ // Name ascending (alphabetical)
106
+ const nameA = a.skillName || a.name;
107
+ const nameB = b.skillName || b.name;
108
+ return nameA.localeCompare(nameB);
109
+ }
110
+
111
+ /**
112
+ * Compare skills by type (asc), then name (asc)
113
+ *
114
+ * Standard ordering for job skill matrix display:
115
+ * - Primary skills first, then secondary, then broad, then track
116
+ * - Within same type, alphabetical by name
117
+ *
118
+ * @param {Object} a - First skill entry
119
+ * @param {Object} b - Second skill entry
120
+ * @returns {number} Comparison result
121
+ */
122
+ export function compareByTypeAndName(a, b) {
123
+ const typeCompare = compareByType(a, b);
124
+ if (typeCompare !== 0) return typeCompare;
125
+ return compareByName(a, b);
126
+ }
127
+
128
+ // =============================================================================
129
+ // Behaviour Comparators
130
+ // =============================================================================
131
+
132
+ /**
133
+ * Compare behaviours by maturity descending (higher maturity first)
134
+ * @param {Object} a - First behaviour entry
135
+ * @param {Object} b - Second behaviour entry
136
+ * @returns {number} Comparison result
137
+ */
138
+ export function compareByMaturityDesc(a, b) {
139
+ return (
140
+ getBehaviourMaturityIndex(b.maturity) -
141
+ getBehaviourMaturityIndex(a.maturity)
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Compare behaviours by maturity ascending (lower maturity first)
147
+ * @param {Object} a - First behaviour entry
148
+ * @param {Object} b - Second behaviour entry
149
+ * @returns {number} Comparison result
150
+ */
151
+ export function compareByMaturityAsc(a, b) {
152
+ return (
153
+ getBehaviourMaturityIndex(a.maturity) -
154
+ getBehaviourMaturityIndex(b.maturity)
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Compare behaviours by name alphabetically
160
+ * @param {Object} a - First behaviour entry
161
+ * @param {Object} b - Second behaviour entry
162
+ * @returns {number} Comparison result
163
+ */
164
+ export function compareByBehaviourName(a, b) {
165
+ const nameA = a.behaviourName || a.name;
166
+ const nameB = b.behaviourName || b.name;
167
+ return nameA.localeCompare(nameB);
168
+ }
169
+
170
+ /**
171
+ * Compare behaviours by maturity (desc), then name (asc)
172
+ *
173
+ * Standard priority ordering for behaviour display:
174
+ * - Higher maturity first
175
+ * - Within same maturity, alphabetical by name
176
+ *
177
+ * @param {Object} a - First behaviour entry
178
+ * @param {Object} b - Second behaviour entry
179
+ * @returns {number} Comparison result
180
+ */
181
+ export function compareByBehaviourPriority(a, b) {
182
+ const maturityDiff =
183
+ getBehaviourMaturityIndex(b.maturity) -
184
+ getBehaviourMaturityIndex(a.maturity);
185
+ if (maturityDiff !== 0) return maturityDiff;
186
+ return compareByBehaviourName(a, b);
187
+ }
188
+
189
+ // =============================================================================
190
+ // Capability Comparators
191
+ // =============================================================================
192
+
193
+ /**
194
+ * Create a comparator for sorting by capability ordinal rank
195
+ *
196
+ * The returned comparator uses ordinalRank from loaded capability data,
197
+ * making the ordering data-driven rather than hardcoded.
198
+ *
199
+ * @param {Object[]} capabilities - Loaded capabilities array
200
+ * @returns {(a: Object, b: Object) => number} Comparator function
201
+ */
202
+ export function compareByCapability(capabilities) {
203
+ const order = getCapabilityOrder(capabilities);
204
+ return (a, b) => {
205
+ const capA = a.capability || "";
206
+ const capB = b.capability || "";
207
+ return order.indexOf(capA) - order.indexOf(capB);
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Sort skills by capability (display order), then by name
213
+ *
214
+ * @param {Object[]} skills - Array of skills to sort
215
+ * @param {Object[]} capabilities - Loaded capabilities array
216
+ * @returns {Object[]} Sorted array (new array, does not mutate input)
217
+ */
218
+ export function sortSkillsByCapability(skills, capabilities) {
219
+ const capabilityComparator = compareByCapability(capabilities);
220
+ return [...skills].sort((a, b) => {
221
+ const capCompare = capabilityComparator(a, b);
222
+ if (capCompare !== 0) return capCompare;
223
+ const nameA = a.skillName || a.name;
224
+ const nameB = b.skillName || b.name;
225
+ return nameA.localeCompare(nameB);
226
+ });
227
+ }
228
+
229
+ // =============================================================================
230
+ // Generic Comparator Factory
231
+ // =============================================================================
232
+
233
+ /**
234
+ * Create comparator from an ordering array
235
+ *
236
+ * @param {string[]} order - Canonical order
237
+ * @param {(item: Object) => string} accessor - Extract value to compare
238
+ * @returns {(a: Object, b: Object) => number}
239
+ */
240
+ export function compareByOrder(order, accessor) {
241
+ return (a, b) => order.indexOf(accessor(a)) - order.indexOf(accessor(b));
242
+ }
243
+
244
+ /**
245
+ * Chain multiple comparators together
246
+ *
247
+ * Returns first non-zero result, or 0 if all comparators return 0.
248
+ *
249
+ * @param {...Function} comparators - Comparator functions
250
+ * @returns {(a: Object, b: Object) => number}
251
+ */
252
+ export function chainComparators(...comparators) {
253
+ return (a, b) => {
254
+ for (const comparator of comparators) {
255
+ const result = comparator(a, b);
256
+ if (result !== 0) return result;
257
+ }
258
+ return 0;
259
+ };
260
+ }
261
+
262
+ // =============================================================================
263
+ // Skill Change Comparators (for progression)
264
+ // =============================================================================
265
+
266
+ /**
267
+ * Compare skill changes by change magnitude (largest first), then type, then name
268
+ *
269
+ * Used for career progression analysis where biggest changes are most important.
270
+ *
271
+ * @param {Object} a - First skill change
272
+ * @param {Object} b - Second skill change
273
+ * @returns {number} Comparison result
274
+ */
275
+ export function compareBySkillChange(a, b) {
276
+ // Change descending (largest improvement first)
277
+ if (b.change !== a.change) return b.change - a.change;
278
+
279
+ // Type ascending (primary first)
280
+ const typeA = ORDER_SKILL_TYPE.indexOf(a.type);
281
+ const typeB = ORDER_SKILL_TYPE.indexOf(b.type);
282
+ if (typeA !== typeB) return typeA - typeB;
283
+
284
+ // Name ascending
285
+ return a.name.localeCompare(b.name);
286
+ }
287
+
288
+ /**
289
+ * Compare behaviour changes by change magnitude (largest first), then name
290
+ *
291
+ * Used for career progression analysis.
292
+ *
293
+ * @param {Object} a - First behaviour change
294
+ * @param {Object} b - Second behaviour change
295
+ * @returns {number} Comparison result
296
+ */
297
+ export function compareByBehaviourChange(a, b) {
298
+ if (b.change !== a.change) return b.change - a.change;
299
+ return a.name.localeCompare(b.name);
300
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Entry-Level Predicate Functions
3
+ *
4
+ * Pure predicates that operate on single skill/behaviour matrix entries.
5
+ * Each predicate takes one entry and returns boolean.
6
+ *
7
+ * Naming conventions:
8
+ * - is* - checks a boolean condition
9
+ * - has* - checks presence of a value
10
+ * - allOf/anyOf/not - combinators
11
+ */
12
+
13
+ import { getSkillLevelIndex } from "@forwardimpact/schema/levels";
14
+
15
+ // =============================================================================
16
+ // Identity Predicates
17
+ // =============================================================================
18
+
19
+ /** Always returns true (identity predicate for optional filtering) */
20
+ export const isAny = () => true;
21
+
22
+ /** Always returns false (null predicate) */
23
+ export const isNone = () => false;
24
+
25
+ // =============================================================================
26
+ // Human-Only Predicates
27
+ // =============================================================================
28
+
29
+ /**
30
+ * Returns true if skill is marked as human-only
31
+ * @param {Object} entry - Skill matrix entry
32
+ * @returns {boolean}
33
+ */
34
+ export const isHumanOnly = (entry) => entry.isHumanOnly === true;
35
+
36
+ /**
37
+ * Returns true if skill is NOT human-only (agent-eligible)
38
+ * @param {Object} entry - Skill matrix entry
39
+ * @returns {boolean}
40
+ */
41
+ export const isAgentEligible = (entry) => !entry.isHumanOnly;
42
+
43
+ // =============================================================================
44
+ // Skill Type Predicates
45
+ // =============================================================================
46
+
47
+ /**
48
+ * Returns true if skill type is primary
49
+ * @param {Object} entry - Skill matrix entry
50
+ * @returns {boolean}
51
+ */
52
+ export const isPrimary = (entry) => entry.type === "primary";
53
+
54
+ /**
55
+ * Returns true if skill type is secondary
56
+ * @param {Object} entry - Skill matrix entry
57
+ * @returns {boolean}
58
+ */
59
+ export const isSecondary = (entry) => entry.type === "secondary";
60
+
61
+ /**
62
+ * Returns true if skill type is broad
63
+ * @param {Object} entry - Skill matrix entry
64
+ * @returns {boolean}
65
+ */
66
+ export const isBroad = (entry) => entry.type === "broad";
67
+
68
+ /**
69
+ * Returns true if skill type is track
70
+ * @param {Object} entry - Skill matrix entry
71
+ * @returns {boolean}
72
+ */
73
+ export const isTrack = (entry) => entry.type === "track";
74
+
75
+ /**
76
+ * Returns true if skill is primary or secondary (core skills)
77
+ * @param {Object} entry - Skill matrix entry
78
+ * @returns {boolean}
79
+ */
80
+ export const isCore = (entry) =>
81
+ entry.type === "primary" || entry.type === "secondary";
82
+
83
+ /**
84
+ * Returns true if skill is broad or track (supporting skills)
85
+ * @param {Object} entry - Skill matrix entry
86
+ * @returns {boolean}
87
+ */
88
+ export const isSupporting = (entry) =>
89
+ entry.type === "broad" || entry.type === "track";
90
+
91
+ // =============================================================================
92
+ // Skill Level Predicates
93
+ // =============================================================================
94
+
95
+ /**
96
+ * Create predicate for skills at or above a minimum level
97
+ * @param {string} minLevel - Minimum skill level
98
+ * @returns {(entry: Object) => boolean}
99
+ */
100
+ export function hasMinLevel(minLevel) {
101
+ const minIndex = getSkillLevelIndex(minLevel);
102
+ return (entry) => getSkillLevelIndex(entry.level) >= minIndex;
103
+ }
104
+
105
+ /**
106
+ * Create predicate for skills at exactly a specific level
107
+ * @param {string} level - Exact skill level to match
108
+ * @returns {(entry: Object) => boolean}
109
+ */
110
+ export function hasLevel(level) {
111
+ const targetIndex = getSkillLevelIndex(level);
112
+ return (entry) => getSkillLevelIndex(entry.level) === targetIndex;
113
+ }
114
+
115
+ /**
116
+ * Create predicate for skills below a level threshold
117
+ * @param {string} maxLevel - Level that must NOT be reached
118
+ * @returns {(entry: Object) => boolean}
119
+ */
120
+ export function hasBelowLevel(maxLevel) {
121
+ const maxIndex = getSkillLevelIndex(maxLevel);
122
+ return (entry) => getSkillLevelIndex(entry.level) < maxIndex;
123
+ }
124
+
125
+ // =============================================================================
126
+ // Capability Predicates
127
+ // =============================================================================
128
+
129
+ /**
130
+ * Create predicate for skills in a specific capability
131
+ * @param {string} capability - Capability to match
132
+ * @returns {(entry: Object) => boolean}
133
+ */
134
+ export function isInCapability(capability) {
135
+ return (entry) => entry.capability === capability;
136
+ }
137
+
138
+ /**
139
+ * Create predicate for skills in any of the specified capabilities
140
+ * @param {string[]} capabilities - Capabilities to match
141
+ * @returns {(entry: Object) => boolean}
142
+ */
143
+ export function isInAnyCapability(capabilities) {
144
+ const set = new Set(capabilities);
145
+ return (entry) => set.has(entry.capability);
146
+ }
147
+
148
+ // =============================================================================
149
+ // Combinators
150
+ // =============================================================================
151
+
152
+ /**
153
+ * Compose predicates with AND logic (all must pass)
154
+ * @param {...Function} predicates - Predicates to combine
155
+ * @returns {(entry: Object) => boolean}
156
+ */
157
+ export function allOf(...predicates) {
158
+ return (entry) => predicates.every((p) => p(entry));
159
+ }
160
+
161
+ /**
162
+ * Compose predicates with OR logic (any must pass)
163
+ * @param {...Function} predicates - Predicates to combine
164
+ * @returns {(entry: Object) => boolean}
165
+ */
166
+ export function anyOf(...predicates) {
167
+ return (entry) => predicates.some((p) => p(entry));
168
+ }
169
+
170
+ /**
171
+ * Negate a predicate
172
+ * @param {Function} predicate - Predicate to negate
173
+ * @returns {(entry: Object) => boolean}
174
+ */
175
+ export function not(predicate) {
176
+ return (entry) => !predicate(entry);
177
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Policy Thresholds, Scores, and Weights
3
+ *
4
+ * Named constants for filter thresholds, scoring, and priority weights.
5
+ * Grep for THRESHOLD_, SCORE_, WEIGHT_ to find all policy values.
6
+ */
7
+
8
+ // =============================================================================
9
+ // Match Tier Thresholds
10
+ // =============================================================================
11
+
12
+ /**
13
+ * Match tier score thresholds
14
+ *
15
+ * These thresholds determine how candidate match scores are classified.
16
+ * Adjust these to change how lenient/strict match tiers are.
17
+ *
18
+ * @see matching.js:classifyMatch
19
+ */
20
+ export const THRESHOLD_MATCH_STRONG = 0.85;
21
+ export const THRESHOLD_MATCH_GOOD = 0.7;
22
+ export const THRESHOLD_MATCH_STRETCH = 0.55;
23
+ export const THRESHOLD_MATCH_ASPIRATIONAL = 0;
24
+
25
+ // =============================================================================
26
+ // Gap Score Decay
27
+ // =============================================================================
28
+
29
+ /**
30
+ * Gap score decay values
31
+ *
32
+ * When a candidate has a gap between their level and the job requirement,
33
+ * these scores penalize larger gaps more heavily.
34
+ *
35
+ * @see matching.js:calculateGapScore
36
+ */
37
+ export const SCORE_GAP_MEETS = 1.0;
38
+ export const SCORE_GAP_MINOR = 0.7;
39
+ export const SCORE_GAP_SIGNIFICANT = 0.4;
40
+ export const SCORE_GAP_MAJOR = 0.15;
41
+ export const SCORE_GAP_ASPIRATIONAL = 0.05;
42
+
43
+ /** Gap scores indexed by gap size (0-4+) */
44
+ export const SCORE_GAP = {
45
+ 0: SCORE_GAP_MEETS,
46
+ 1: SCORE_GAP_MINOR,
47
+ 2: SCORE_GAP_SIGNIFICANT,
48
+ 3: SCORE_GAP_MAJOR,
49
+ 4: SCORE_GAP_ASPIRATIONAL,
50
+ };
51
+
52
+ // =============================================================================
53
+ // Skill Priority Weights
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Skill priority weights by type
58
+ *
59
+ * Primary skills are core competencies and get highest priority.
60
+ * Used for interview question selection and development prioritization.
61
+ *
62
+ * @see interview.js:calculateSkillPriority
63
+ */
64
+ export const WEIGHT_SKILL_TYPE_PRIMARY = 30;
65
+ export const WEIGHT_SKILL_TYPE_SECONDARY = 20;
66
+ export const WEIGHT_SKILL_TYPE_BROAD = 10;
67
+ export const WEIGHT_SKILL_TYPE_TRACK = 5;
68
+
69
+ /** Skill type weights as object for lookup */
70
+ export const WEIGHT_SKILL_TYPE = {
71
+ primary: WEIGHT_SKILL_TYPE_PRIMARY,
72
+ secondary: WEIGHT_SKILL_TYPE_SECONDARY,
73
+ broad: WEIGHT_SKILL_TYPE_BROAD,
74
+ track: WEIGHT_SKILL_TYPE_TRACK,
75
+ };
76
+
77
+ // =============================================================================
78
+ // Capability Priority Boosts
79
+ // =============================================================================
80
+
81
+ /**
82
+ * Capability priority boosts
83
+ *
84
+ * Certain capabilities get additional priority in the AI-era engineering model.
85
+ *
86
+ * @see interview.js:calculateSkillPriority
87
+ */
88
+ export const WEIGHT_CAPABILITY_AI = 15;
89
+ export const WEIGHT_CAPABILITY_DELIVERY = 5;
90
+
91
+ /** Capability boosts as object for lookup */
92
+ export const WEIGHT_CAPABILITY_BOOST = {
93
+ ai: WEIGHT_CAPABILITY_AI,
94
+ delivery: WEIGHT_CAPABILITY_DELIVERY,
95
+ };
96
+
97
+ // =============================================================================
98
+ // Behaviour Priority Weights
99
+ // =============================================================================
100
+
101
+ /**
102
+ * Base behaviour priority weight
103
+ *
104
+ * @see interview.js:calculateBehaviourPriority
105
+ */
106
+ export const WEIGHT_BEHAVIOUR_BASE = 15;
107
+
108
+ /**
109
+ * Behaviour maturity multiplier
110
+ *
111
+ * Each maturity level adds this amount to the priority.
112
+ */
113
+ export const WEIGHT_BEHAVIOUR_MATURITY = 3;
114
+
115
+ // =============================================================================
116
+ // Interview Ratios
117
+ // =============================================================================
118
+
119
+ /**
120
+ * Default ratio of interview time allocated to skills vs behaviours
121
+ *
122
+ * 0.6 = 60% skills, 40% behaviours
123
+ */
124
+ export const RATIO_SKILL_BEHAVIOUR = 0.6;
125
+
126
+ // =============================================================================
127
+ // Skill Level Multipliers
128
+ // =============================================================================
129
+
130
+ /**
131
+ * Skill level multiplier for priority calculation
132
+ *
133
+ * Higher skill levels get proportionally more priority.
134
+ */
135
+ export const WEIGHT_SKILL_LEVEL = 2;
136
+
137
+ /**
138
+ * Priority penalty for below-level questions
139
+ */
140
+ export const WEIGHT_BELOW_LEVEL_PENALTY = -5;
141
+
142
+ // =============================================================================
143
+ // Development Path Weights
144
+ // =============================================================================
145
+
146
+ /**
147
+ * Type multipliers for development path prioritization
148
+ *
149
+ * Primary skills are more critical to develop first.
150
+ */
151
+ export const WEIGHT_DEV_TYPE_PRIMARY = 3;
152
+ export const WEIGHT_DEV_TYPE_SECONDARY = 2;
153
+ export const WEIGHT_DEV_TYPE_BROAD = 1;
154
+
155
+ /**
156
+ * AI capability boost for development paths
157
+ *
158
+ * AI skills get extra emphasis in development planning.
159
+ */
160
+ export const WEIGHT_DEV_AI_BOOST = 1.5;