@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,317 @@
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;
161
+
162
+ // =============================================================================
163
+ // Agent Profile Limits
164
+ // =============================================================================
165
+
166
+ /**
167
+ * Maximum number of skills shown in agent profile body
168
+ *
169
+ * Limits the skill index table and before-handoff checklist to keep
170
+ * agent context focused. All skills are still exported as SKILL.md files
171
+ * and listed via --skills.
172
+ *
173
+ * @see agent.js:buildStageProfileBodyData
174
+ */
175
+ export const LIMIT_AGENT_PROFILE_SKILLS = 5;
176
+
177
+ /**
178
+ * Maximum number of working style entries from top behaviours
179
+ * in agent profiles.
180
+ *
181
+ * @see agent.js:buildWorkingStyleFromBehaviours
182
+ */
183
+ export const LIMIT_AGENT_WORKING_STYLES = 3;
184
+
185
+ // =============================================================================
186
+ // Interview Time Defaults
187
+ // =============================================================================
188
+
189
+ /**
190
+ * Default expected duration for a standard interview question (minutes)
191
+ */
192
+ export const DEFAULT_INTERVIEW_QUESTION_MINUTES = 5;
193
+
194
+ /**
195
+ * Default expected duration for a decomposition question (minutes)
196
+ */
197
+ export const DEFAULT_DECOMPOSITION_QUESTION_MINUTES = 15;
198
+
199
+ /**
200
+ * Default expected duration for a stakeholder simulation question (minutes)
201
+ */
202
+ export const DEFAULT_SIMULATION_QUESTION_MINUTES = 20;
203
+
204
+ /**
205
+ * Tolerance above target interview budget before stopping selection (minutes)
206
+ *
207
+ * Interview question selection allows exceeding the time budget by this amount
208
+ * to avoid under-filling interviews.
209
+ */
210
+ export const TOLERANCE_INTERVIEW_BUDGET_MINUTES = 5;
211
+
212
+ // =============================================================================
213
+ // Decomposition Capability Weights
214
+ // =============================================================================
215
+
216
+ /**
217
+ * Capability priority weights for decomposition interviews
218
+ *
219
+ * Delivery and scale capabilities are typically more important for
220
+ * system decomposition questions.
221
+ *
222
+ * @see interview.js:calculateCapabilityPriority
223
+ */
224
+ export const WEIGHT_CAPABILITY_DECOMP_DELIVERY = 10;
225
+ export const WEIGHT_CAPABILITY_DECOMP_SCALE = 8;
226
+ export const WEIGHT_CAPABILITY_DECOMP_RELIABILITY = 6;
227
+
228
+ /**
229
+ * Priority boost applied to focus-area questions in focused interviews
230
+ *
231
+ * @see interview.js:deriveFocusedInterview
232
+ */
233
+ export const WEIGHT_FOCUS_BOOST = 10;
234
+
235
+ // =============================================================================
236
+ // Senior Grade Threshold
237
+ // =============================================================================
238
+
239
+ /**
240
+ * Minimum ordinalRank for a grade to be considered "senior" (Staff+)
241
+ *
242
+ * Used to determine when additional expectations scoring applies
243
+ * in job matching.
244
+ *
245
+ * @see derivation.js:isSeniorGrade
246
+ */
247
+ export const THRESHOLD_SENIOR_GRADE = 5;
248
+
249
+ // =============================================================================
250
+ // Assessment Weights
251
+ // =============================================================================
252
+
253
+ /**
254
+ * Default skill weight when track does not specify assessment weights
255
+ */
256
+ export const WEIGHT_ASSESSMENT_SKILL_DEFAULT = 0.5;
257
+
258
+ /**
259
+ * Default behaviour weight when track does not specify assessment weights
260
+ */
261
+ export const WEIGHT_ASSESSMENT_BEHAVIOUR_DEFAULT = 0.5;
262
+
263
+ /**
264
+ * Base weight for overall score in senior role matching (non-expectations portion)
265
+ */
266
+ export const WEIGHT_SENIOR_BASE = 0.9;
267
+
268
+ /**
269
+ * Weight for expectations score bonus in senior role matching
270
+ */
271
+ export const WEIGHT_SENIOR_EXPECTATIONS = 0.1;
272
+
273
+ // =============================================================================
274
+ // Match Result Limits
275
+ // =============================================================================
276
+
277
+ /**
278
+ * Number of top-priority gaps to surface in match analysis
279
+ */
280
+ export const LIMIT_PRIORITY_GAPS = 3;
281
+
282
+ /**
283
+ * Score bonus for same-track candidates in next-step job matching
284
+ */
285
+ export const WEIGHT_SAME_TRACK_BONUS = 0.1;
286
+
287
+ // =============================================================================
288
+ // Realistic Match Filtering
289
+ // =============================================================================
290
+
291
+ /**
292
+ * Grade offset (±) from best-fit grade for realistic match filtering
293
+ */
294
+ export const RANGE_GRADE_OFFSET = 1;
295
+
296
+ /**
297
+ * Grade offset below highest strong/good match for ready-tier filtering
298
+ *
299
+ * Strong and Good matches are shown up to this many levels below the
300
+ * highest matched grade. Stretch and Aspirational matches are only shown
301
+ * at or above the highest matched grade.
302
+ */
303
+ export const RANGE_READY_GRADE_OFFSET = 2;
304
+
305
+ // =============================================================================
306
+ // Driver Coverage Thresholds
307
+ // =============================================================================
308
+
309
+ /**
310
+ * Minimum skill level for a skill to count as "covered" in driver analysis
311
+ */
312
+ export const THRESHOLD_DRIVER_SKILL_LEVEL = "working";
313
+
314
+ /**
315
+ * Minimum behaviour maturity for a behaviour to count as "covered" in driver analysis
316
+ */
317
+ export const THRESHOLD_DRIVER_BEHAVIOUR_MATURITY = "practicing";
package/src/profile.js ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Unified Profile Derivation
3
+ *
4
+ * Shared functions for deriving skill and behaviour profiles for both
5
+ * human jobs and AI agents.
6
+ *
7
+ * - prepareBaseProfile() - core derivation (skills, behaviours, responsibilities)
8
+ * - prepareAgentProfile() - agent-specific derivation using composed policies
9
+ *
10
+ * @see policies/composed.js - Agent filtering and sorting policies
11
+ */
12
+
13
+ import {
14
+ deriveSkillMatrix,
15
+ deriveBehaviourProfile,
16
+ deriveResponsibilities,
17
+ } from "./derivation.js";
18
+
19
+ import {
20
+ prepareAgentSkillMatrix,
21
+ prepareAgentBehaviourProfile,
22
+ } from "./policies/composed.js";
23
+
24
+ // =============================================================================
25
+ // Utility Functions
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Build set of capabilities with positive track modifiers
30
+ * @param {Object} track - Track definition
31
+ * @returns {Set<string>} Set of capability IDs with positive modifiers
32
+ */
33
+ export function getPositiveTrackCapabilities(track) {
34
+ return new Set(
35
+ Object.entries(track.skillModifiers || {})
36
+ .filter(([_, modifier]) => modifier > 0)
37
+ .map(([capability]) => capability),
38
+ );
39
+ }
40
+
41
+ // =============================================================================
42
+ // Profile Derivation
43
+ // =============================================================================
44
+
45
+ /**
46
+ * @typedef {Object} BaseProfile
47
+ * @property {Array} skillMatrix - Derived skill matrix
48
+ * @property {Array} behaviourProfile - Derived behaviour profile
49
+ * @property {Array} derivedResponsibilities - Derived responsibilities (if capabilities provided)
50
+ * @property {Object} discipline - The discipline
51
+ * @property {Object} track - The track
52
+ * @property {Object} grade - The grade
53
+ */
54
+
55
+ /**
56
+ * Prepare a base profile with raw derivation
57
+ *
58
+ * Core derivation entry point shared by jobs and agents. Produces the
59
+ * raw skill matrix, behaviour profile, and responsibilities without
60
+ * any filtering or sorting. Consumers apply policies as needed:
61
+ *
62
+ * - Human jobs: use raw output directly (sorted by type in derivation)
63
+ * - AI agents: use prepareAgentProfile() which applies composed policies
64
+ *
65
+ * @param {Object} params
66
+ * @param {Object} params.discipline - The discipline
67
+ * @param {Object} params.track - The track
68
+ * @param {Object} params.grade - The grade
69
+ * @param {Array} params.skills - All available skills
70
+ * @param {Array} params.behaviours - All available behaviours
71
+ * @param {Array} [params.capabilities] - Optional capabilities for responsibility derivation
72
+ * @returns {BaseProfile} The prepared profile
73
+ */
74
+ export function prepareBaseProfile({
75
+ discipline,
76
+ track,
77
+ grade,
78
+ skills,
79
+ behaviours,
80
+ capabilities,
81
+ }) {
82
+ // Core derivation
83
+ const skillMatrix = deriveSkillMatrix({ discipline, grade, track, skills });
84
+ const behaviourProfile = deriveBehaviourProfile({
85
+ discipline,
86
+ grade,
87
+ track,
88
+ behaviours,
89
+ });
90
+
91
+ // Derive responsibilities if capabilities provided
92
+ let derivedResponsibilities = [];
93
+ if (capabilities && capabilities.length > 0) {
94
+ derivedResponsibilities = deriveResponsibilities({
95
+ skillMatrix,
96
+ capabilities,
97
+ track,
98
+ });
99
+ }
100
+
101
+ return {
102
+ skillMatrix,
103
+ behaviourProfile,
104
+ derivedResponsibilities,
105
+ discipline,
106
+ track,
107
+ grade,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Prepare a profile optimized for agent generation
113
+ *
114
+ * Applies agent-specific policies from composed.js:
115
+ * - Excludes human-only skills
116
+ * - Keeps only skills at the highest derived level
117
+ * - Sorts skills by level descending
118
+ * - Sorts behaviours by maturity descending
119
+ *
120
+ * @param {Object} params - Same as prepareBaseProfile
121
+ * @returns {BaseProfile} The prepared profile with agent policies applied
122
+ */
123
+ export function prepareAgentProfile({
124
+ discipline,
125
+ track,
126
+ grade,
127
+ skills,
128
+ behaviours,
129
+ capabilities,
130
+ }) {
131
+ const base = prepareBaseProfile({
132
+ discipline,
133
+ track,
134
+ grade,
135
+ skills,
136
+ behaviours,
137
+ capabilities,
138
+ });
139
+
140
+ return {
141
+ ...base,
142
+ skillMatrix: prepareAgentSkillMatrix(base.skillMatrix),
143
+ behaviourProfile: prepareAgentBehaviourProfile(base.behaviourProfile),
144
+ };
145
+ }
@@ -11,6 +11,10 @@ import {
11
11
  getBehaviourMaturityIndex,
12
12
  } from "@forwardimpact/schema/levels";
13
13
  import { deriveJob, isValidJobCombination } from "./derivation.js";
14
+ import {
15
+ compareBySkillChange,
16
+ compareByBehaviourChange,
17
+ } from "./policies/orderings.js";
14
18
 
15
19
  /**
16
20
  * @typedef {Object} SkillChange
@@ -128,14 +132,8 @@ export function calculateSkillChanges(currentMatrix, targetMatrix) {
128
132
  }
129
133
  }
130
134
 
131
- // Sort by change (largest first), then by type, then by name
132
- const typeOrder = { primary: 0, secondary: 1, broad: 2 };
133
- changes.sort((a, b) => {
134
- if (b.change !== a.change) return b.change - a.change;
135
- if (typeOrder[a.type] !== typeOrder[b.type])
136
- return typeOrder[a.type] - typeOrder[b.type];
137
- return a.name.localeCompare(b.name);
138
- });
135
+ // Sort using policy comparator
136
+ changes.sort(compareBySkillChange);
139
137
 
140
138
  return changes;
141
139
  }
@@ -172,11 +170,8 @@ export function calculateBehaviourChanges(currentProfile, targetProfile) {
172
170
  }
173
171
  }
174
172
 
175
- // Sort by change (largest first), then by name
176
- changes.sort((a, b) => {
177
- if (b.change !== a.change) return b.change - a.change;
178
- return a.name.localeCompare(b.name);
179
- });
173
+ // Sort using policy comparator
174
+ changes.sort(compareByBehaviourChange);
180
175
 
181
176
  return changes;
182
177
  }
@@ -6,7 +6,7 @@
6
6
  * level contribute tools, ensuring focused toolkits for both jobs and agents.
7
7
  */
8
8
 
9
- import { filterByHighestLevel } from "./profile.js";
9
+ import { filterToolkitSkills } from "./policies/composed.js";
10
10
 
11
11
  /**
12
12
  * @typedef {Object} ToolkitEntry
@@ -30,8 +30,8 @@ import { filterByHighestLevel } from "./profile.js";
30
30
  * @returns {ToolkitEntry[]} De-duplicated toolkit sorted by name
31
31
  */
32
32
  export function deriveToolkit({ skillMatrix, skills }) {
33
- // Filter to highest level skills only
34
- const sourceMatrix = filterByHighestLevel(skillMatrix);
33
+ // Filter to highest level skills only using policy
34
+ const sourceMatrix = filterToolkitSkills(skillMatrix);
35
35
 
36
36
  // Build skill lookup map for O(1) access
37
37
  const skillMap = new Map(skills.map((s) => [s.id, s]));