@forwardimpact/model 0.5.0 → 0.7.1

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,312 @@
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
+ * Stage-to-handoff mapping for checklist derivation
39
+ *
40
+ * Maps stage IDs to the stage whose `.ready` criteria should be shown
41
+ * before leaving that stage.
42
+ */
43
+ export const CHECKLIST_STAGE_MAP = {
44
+ plan: "plan",
45
+ code: "code",
46
+ review: "review",
47
+ };
48
+
49
+ // =============================================================================
50
+ // Skill Comparators
51
+ // =============================================================================
52
+
53
+ /**
54
+ * Compare skills by level descending (higher level first)
55
+ * @param {Object} a - First skill entry
56
+ * @param {Object} b - Second skill entry
57
+ * @returns {number} Comparison result
58
+ */
59
+ export function compareByLevelDesc(a, b) {
60
+ return getSkillLevelIndex(b.level) - getSkillLevelIndex(a.level);
61
+ }
62
+
63
+ /**
64
+ * Compare skills by level ascending (lower level first)
65
+ * @param {Object} a - First skill entry
66
+ * @param {Object} b - Second skill entry
67
+ * @returns {number} Comparison result
68
+ */
69
+ export function compareByLevelAsc(a, b) {
70
+ return getSkillLevelIndex(a.level) - getSkillLevelIndex(b.level);
71
+ }
72
+
73
+ /**
74
+ * Compare skills by type (primary first)
75
+ * @param {Object} a - First skill entry
76
+ * @param {Object} b - Second skill entry
77
+ * @returns {number} Comparison result
78
+ */
79
+ export function compareByType(a, b) {
80
+ return ORDER_SKILL_TYPE.indexOf(a.type) - ORDER_SKILL_TYPE.indexOf(b.type);
81
+ }
82
+
83
+ /**
84
+ * Compare skills by name alphabetically
85
+ * @param {Object} a - First skill entry
86
+ * @param {Object} b - Second skill entry
87
+ * @returns {number} Comparison result
88
+ */
89
+ export function compareByName(a, b) {
90
+ const nameA = a.skillName || a.name;
91
+ const nameB = b.skillName || b.name;
92
+ return nameA.localeCompare(nameB);
93
+ }
94
+
95
+ /**
96
+ * Compare skills by level (desc), then type (asc), then name (asc)
97
+ *
98
+ * Standard priority ordering for skill display:
99
+ * - Higher levels first
100
+ * - Within same level, primary before secondary before broad
101
+ * - Within same type, alphabetical by name
102
+ *
103
+ * @param {Object} a - First skill entry
104
+ * @param {Object} b - Second skill entry
105
+ * @returns {number} Comparison result
106
+ */
107
+ export function compareBySkillPriority(a, b) {
108
+ // Level descending (higher level first)
109
+ const levelDiff = getSkillLevelIndex(b.level) - getSkillLevelIndex(a.level);
110
+ if (levelDiff !== 0) return levelDiff;
111
+
112
+ // Type ascending (primary first)
113
+ const typeA = ORDER_SKILL_TYPE.indexOf(a.type);
114
+ const typeB = ORDER_SKILL_TYPE.indexOf(b.type);
115
+ if (typeA !== typeB) return typeA - typeB;
116
+
117
+ // Name ascending (alphabetical)
118
+ const nameA = a.skillName || a.name;
119
+ const nameB = b.skillName || b.name;
120
+ return nameA.localeCompare(nameB);
121
+ }
122
+
123
+ /**
124
+ * Compare skills by type (asc), then name (asc)
125
+ *
126
+ * Standard ordering for job skill matrix display:
127
+ * - Primary skills first, then secondary, then broad, then track
128
+ * - Within same type, alphabetical by name
129
+ *
130
+ * @param {Object} a - First skill entry
131
+ * @param {Object} b - Second skill entry
132
+ * @returns {number} Comparison result
133
+ */
134
+ export function compareByTypeAndName(a, b) {
135
+ const typeCompare = compareByType(a, b);
136
+ if (typeCompare !== 0) return typeCompare;
137
+ return compareByName(a, b);
138
+ }
139
+
140
+ // =============================================================================
141
+ // Behaviour Comparators
142
+ // =============================================================================
143
+
144
+ /**
145
+ * Compare behaviours by maturity descending (higher maturity first)
146
+ * @param {Object} a - First behaviour entry
147
+ * @param {Object} b - Second behaviour entry
148
+ * @returns {number} Comparison result
149
+ */
150
+ export function compareByMaturityDesc(a, b) {
151
+ return (
152
+ getBehaviourMaturityIndex(b.maturity) -
153
+ getBehaviourMaturityIndex(a.maturity)
154
+ );
155
+ }
156
+
157
+ /**
158
+ * Compare behaviours by maturity ascending (lower maturity first)
159
+ * @param {Object} a - First behaviour entry
160
+ * @param {Object} b - Second behaviour entry
161
+ * @returns {number} Comparison result
162
+ */
163
+ export function compareByMaturityAsc(a, b) {
164
+ return (
165
+ getBehaviourMaturityIndex(a.maturity) -
166
+ getBehaviourMaturityIndex(b.maturity)
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Compare behaviours by name alphabetically
172
+ * @param {Object} a - First behaviour entry
173
+ * @param {Object} b - Second behaviour entry
174
+ * @returns {number} Comparison result
175
+ */
176
+ export function compareByBehaviourName(a, b) {
177
+ const nameA = a.behaviourName || a.name;
178
+ const nameB = b.behaviourName || b.name;
179
+ return nameA.localeCompare(nameB);
180
+ }
181
+
182
+ /**
183
+ * Compare behaviours by maturity (desc), then name (asc)
184
+ *
185
+ * Standard priority ordering for behaviour display:
186
+ * - Higher maturity first
187
+ * - Within same maturity, alphabetical by name
188
+ *
189
+ * @param {Object} a - First behaviour entry
190
+ * @param {Object} b - Second behaviour entry
191
+ * @returns {number} Comparison result
192
+ */
193
+ export function compareByBehaviourPriority(a, b) {
194
+ const maturityDiff =
195
+ getBehaviourMaturityIndex(b.maturity) -
196
+ getBehaviourMaturityIndex(a.maturity);
197
+ if (maturityDiff !== 0) return maturityDiff;
198
+ return compareByBehaviourName(a, b);
199
+ }
200
+
201
+ // =============================================================================
202
+ // Capability Comparators
203
+ // =============================================================================
204
+
205
+ /**
206
+ * Create a comparator for sorting by capability ordinal rank
207
+ *
208
+ * The returned comparator uses ordinalRank from loaded capability data,
209
+ * making the ordering data-driven rather than hardcoded.
210
+ *
211
+ * @param {Object[]} capabilities - Loaded capabilities array
212
+ * @returns {(a: Object, b: Object) => number} Comparator function
213
+ */
214
+ export function compareByCapability(capabilities) {
215
+ const order = getCapabilityOrder(capabilities);
216
+ return (a, b) => {
217
+ const capA = a.capability || "";
218
+ const capB = b.capability || "";
219
+ return order.indexOf(capA) - order.indexOf(capB);
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Sort skills by capability (display order), then by name
225
+ *
226
+ * @param {Object[]} skills - Array of skills to sort
227
+ * @param {Object[]} capabilities - Loaded capabilities array
228
+ * @returns {Object[]} Sorted array (new array, does not mutate input)
229
+ */
230
+ export function sortSkillsByCapability(skills, capabilities) {
231
+ const capabilityComparator = compareByCapability(capabilities);
232
+ return [...skills].sort((a, b) => {
233
+ const capCompare = capabilityComparator(a, b);
234
+ if (capCompare !== 0) return capCompare;
235
+ const nameA = a.skillName || a.name;
236
+ const nameB = b.skillName || b.name;
237
+ return nameA.localeCompare(nameB);
238
+ });
239
+ }
240
+
241
+ // =============================================================================
242
+ // Generic Comparator Factory
243
+ // =============================================================================
244
+
245
+ /**
246
+ * Create comparator from an ordering array
247
+ *
248
+ * @param {string[]} order - Canonical order
249
+ * @param {(item: Object) => string} accessor - Extract value to compare
250
+ * @returns {(a: Object, b: Object) => number}
251
+ */
252
+ export function compareByOrder(order, accessor) {
253
+ return (a, b) => order.indexOf(accessor(a)) - order.indexOf(accessor(b));
254
+ }
255
+
256
+ /**
257
+ * Chain multiple comparators together
258
+ *
259
+ * Returns first non-zero result, or 0 if all comparators return 0.
260
+ *
261
+ * @param {...Function} comparators - Comparator functions
262
+ * @returns {(a: Object, b: Object) => number}
263
+ */
264
+ export function chainComparators(...comparators) {
265
+ return (a, b) => {
266
+ for (const comparator of comparators) {
267
+ const result = comparator(a, b);
268
+ if (result !== 0) return result;
269
+ }
270
+ return 0;
271
+ };
272
+ }
273
+
274
+ // =============================================================================
275
+ // Skill Change Comparators (for progression)
276
+ // =============================================================================
277
+
278
+ /**
279
+ * Compare skill changes by change magnitude (largest first), then type, then name
280
+ *
281
+ * Used for career progression analysis where biggest changes are most important.
282
+ *
283
+ * @param {Object} a - First skill change
284
+ * @param {Object} b - Second skill change
285
+ * @returns {number} Comparison result
286
+ */
287
+ export function compareBySkillChange(a, b) {
288
+ // Change descending (largest improvement first)
289
+ if (b.change !== a.change) return b.change - a.change;
290
+
291
+ // Type ascending (primary first)
292
+ const typeA = ORDER_SKILL_TYPE.indexOf(a.type);
293
+ const typeB = ORDER_SKILL_TYPE.indexOf(b.type);
294
+ if (typeA !== typeB) return typeA - typeB;
295
+
296
+ // Name ascending
297
+ return a.name.localeCompare(b.name);
298
+ }
299
+
300
+ /**
301
+ * Compare behaviour changes by change magnitude (largest first), then name
302
+ *
303
+ * Used for career progression analysis.
304
+ *
305
+ * @param {Object} a - First behaviour change
306
+ * @param {Object} b - Second behaviour change
307
+ * @returns {number} Comparison result
308
+ */
309
+ export function compareByBehaviourChange(a, b) {
310
+ if (b.change !== a.change) return b.change - a.change;
311
+ return a.name.localeCompare(b.name);
312
+ }
@@ -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
+ }