@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.
- package/README.md +2 -2
- package/package.json +17 -15
- package/{lib → src}/agent.js +25 -19
- package/{lib → src}/checklist.js +2 -10
- package/{lib → src}/derivation.js +17 -15
- package/{lib → src}/index.js +88 -17
- package/src/interview.js +1001 -0
- package/{lib → src}/job-cache.js +7 -7
- package/{lib → src}/matching.js +65 -41
- package/{lib → src}/modifiers.js +4 -4
- package/src/policies/composed.js +135 -0
- package/src/policies/filters.js +104 -0
- package/src/policies/index.js +160 -0
- package/src/policies/orderings.js +312 -0
- package/src/policies/predicates.js +177 -0
- package/src/policies/thresholds.js +317 -0
- package/src/profile.js +145 -0
- package/{lib → src}/progression.js +8 -13
- package/{lib → src}/toolkit.js +3 -3
- package/lib/interview.js +0 -539
- package/lib/profile.js +0 -262
- /package/{lib → src}/job.js +0 -0
package/{lib → src}/job-cache.js
RENAMED
|
@@ -11,13 +11,13 @@ import { deriveJob } from "./derivation.js";
|
|
|
11
11
|
const cache = new Map();
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Build a consistent cache key from job parameters
|
|
15
15
|
* @param {string} disciplineId
|
|
16
16
|
* @param {string} gradeId
|
|
17
17
|
* @param {string} [trackId] - Optional track ID
|
|
18
18
|
* @returns {string}
|
|
19
19
|
*/
|
|
20
|
-
export function
|
|
20
|
+
export function buildJobKey(disciplineId, gradeId, trackId = null) {
|
|
21
21
|
if (trackId) {
|
|
22
22
|
return `${disciplineId}_${gradeId}_${trackId}`;
|
|
23
23
|
}
|
|
@@ -43,7 +43,7 @@ export function getOrCreateJob({
|
|
|
43
43
|
behaviours,
|
|
44
44
|
capabilities,
|
|
45
45
|
}) {
|
|
46
|
-
const key =
|
|
46
|
+
const key = buildJobKey(discipline.id, grade.id, track?.id);
|
|
47
47
|
|
|
48
48
|
if (!cache.has(key)) {
|
|
49
49
|
const job = deriveJob({
|
|
@@ -66,7 +66,7 @@ export function getOrCreateJob({
|
|
|
66
66
|
/**
|
|
67
67
|
* Clear all cached jobs
|
|
68
68
|
*/
|
|
69
|
-
export function
|
|
69
|
+
export function clearCache() {
|
|
70
70
|
cache.clear();
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -76,14 +76,14 @@ export function clearJobCache() {
|
|
|
76
76
|
* @param {string} gradeId
|
|
77
77
|
* @param {string} [trackId] - Optional track ID
|
|
78
78
|
*/
|
|
79
|
-
export function
|
|
80
|
-
cache.delete(
|
|
79
|
+
export function invalidateCachedJob(disciplineId, gradeId, trackId = null) {
|
|
80
|
+
cache.delete(buildJobKey(disciplineId, gradeId, trackId));
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* Get the number of cached jobs (for testing/debugging)
|
|
85
85
|
* @returns {number}
|
|
86
86
|
*/
|
|
87
|
-
export function
|
|
87
|
+
export function getCachedJobCount() {
|
|
88
88
|
return cache.size;
|
|
89
89
|
}
|
package/{lib → src}/matching.js
RENAMED
|
@@ -16,6 +16,25 @@ import {
|
|
|
16
16
|
isSeniorGrade,
|
|
17
17
|
} from "./derivation.js";
|
|
18
18
|
|
|
19
|
+
import {
|
|
20
|
+
THRESHOLD_MATCH_STRONG,
|
|
21
|
+
THRESHOLD_MATCH_GOOD,
|
|
22
|
+
THRESHOLD_MATCH_STRETCH,
|
|
23
|
+
SCORE_GAP,
|
|
24
|
+
WEIGHT_DEV_TYPE_PRIMARY,
|
|
25
|
+
WEIGHT_DEV_TYPE_SECONDARY,
|
|
26
|
+
WEIGHT_DEV_TYPE_BROAD,
|
|
27
|
+
WEIGHT_DEV_AI_BOOST,
|
|
28
|
+
WEIGHT_ASSESSMENT_SKILL_DEFAULT,
|
|
29
|
+
WEIGHT_ASSESSMENT_BEHAVIOUR_DEFAULT,
|
|
30
|
+
WEIGHT_SENIOR_BASE,
|
|
31
|
+
WEIGHT_SENIOR_EXPECTATIONS,
|
|
32
|
+
LIMIT_PRIORITY_GAPS,
|
|
33
|
+
WEIGHT_SAME_TRACK_BONUS,
|
|
34
|
+
RANGE_GRADE_OFFSET,
|
|
35
|
+
RANGE_READY_GRADE_OFFSET,
|
|
36
|
+
} from "./policies/thresholds.js";
|
|
37
|
+
|
|
19
38
|
// ============================================================================
|
|
20
39
|
// Match Tier Types and Constants
|
|
21
40
|
// ============================================================================
|
|
@@ -34,25 +53,26 @@ export const MatchTier = {
|
|
|
34
53
|
|
|
35
54
|
/**
|
|
36
55
|
* Match tier configuration with thresholds and display properties
|
|
56
|
+
* Uses threshold constants from policies/thresholds.js
|
|
37
57
|
* @type {Object<number, {label: string, color: string, minScore: number, description: string}>}
|
|
38
58
|
*/
|
|
39
|
-
export const
|
|
59
|
+
export const CONFIG_MATCH_TIER = {
|
|
40
60
|
[MatchTier.STRONG]: {
|
|
41
61
|
label: "Strong Match",
|
|
42
62
|
color: "green",
|
|
43
|
-
minScore:
|
|
63
|
+
minScore: THRESHOLD_MATCH_STRONG,
|
|
44
64
|
description: "Ready for this role now",
|
|
45
65
|
},
|
|
46
66
|
[MatchTier.GOOD]: {
|
|
47
67
|
label: "Good Match",
|
|
48
68
|
color: "blue",
|
|
49
|
-
minScore:
|
|
69
|
+
minScore: THRESHOLD_MATCH_GOOD,
|
|
50
70
|
description: "Ready within 6-12 months of focused growth",
|
|
51
71
|
},
|
|
52
72
|
[MatchTier.STRETCH]: {
|
|
53
73
|
label: "Stretch Role",
|
|
54
74
|
color: "amber",
|
|
55
|
-
minScore:
|
|
75
|
+
minScore: THRESHOLD_MATCH_STRETCH,
|
|
56
76
|
description: "Ambitious but achievable with dedicated development",
|
|
57
77
|
},
|
|
58
78
|
[MatchTier.ASPIRATIONAL]: {
|
|
@@ -76,19 +96,19 @@ export const MATCH_TIER_CONFIG = {
|
|
|
76
96
|
* @param {number} score - Match score from 0 to 1
|
|
77
97
|
* @returns {MatchTierInfo} Tier classification
|
|
78
98
|
*/
|
|
79
|
-
export function
|
|
80
|
-
if (score >=
|
|
81
|
-
return { tier: MatchTier.STRONG, ...
|
|
99
|
+
export function classifyMatch(score) {
|
|
100
|
+
if (score >= CONFIG_MATCH_TIER[MatchTier.STRONG].minScore) {
|
|
101
|
+
return { tier: MatchTier.STRONG, ...CONFIG_MATCH_TIER[MatchTier.STRONG] };
|
|
82
102
|
}
|
|
83
|
-
if (score >=
|
|
84
|
-
return { tier: MatchTier.GOOD, ...
|
|
103
|
+
if (score >= CONFIG_MATCH_TIER[MatchTier.GOOD].minScore) {
|
|
104
|
+
return { tier: MatchTier.GOOD, ...CONFIG_MATCH_TIER[MatchTier.GOOD] };
|
|
85
105
|
}
|
|
86
|
-
if (score >=
|
|
87
|
-
return { tier: MatchTier.STRETCH, ...
|
|
106
|
+
if (score >= CONFIG_MATCH_TIER[MatchTier.STRETCH].minScore) {
|
|
107
|
+
return { tier: MatchTier.STRETCH, ...CONFIG_MATCH_TIER[MatchTier.STRETCH] };
|
|
88
108
|
}
|
|
89
109
|
return {
|
|
90
110
|
tier: MatchTier.ASPIRATIONAL,
|
|
91
|
-
...
|
|
111
|
+
...CONFIG_MATCH_TIER[MatchTier.ASPIRATIONAL],
|
|
92
112
|
};
|
|
93
113
|
}
|
|
94
114
|
|
|
@@ -98,16 +118,10 @@ export function classifyMatchTier(score) {
|
|
|
98
118
|
|
|
99
119
|
/**
|
|
100
120
|
* Score values for different gap sizes
|
|
101
|
-
*
|
|
121
|
+
* Re-exported from policies/thresholds.js for backward compatibility
|
|
102
122
|
* @type {Object<number, number>}
|
|
103
123
|
*/
|
|
104
|
-
export const GAP_SCORES =
|
|
105
|
-
0: 1.0, // Meets or exceeds
|
|
106
|
-
1: 0.7, // Minor development needed
|
|
107
|
-
2: 0.4, // Significant but achievable gap
|
|
108
|
-
3: 0.15, // Major development required
|
|
109
|
-
4: 0.05, // Aspirational only
|
|
110
|
-
};
|
|
124
|
+
export const GAP_SCORES = SCORE_GAP;
|
|
111
125
|
|
|
112
126
|
/**
|
|
113
127
|
* Calculate gap score with smooth decay
|
|
@@ -115,11 +129,11 @@ export const GAP_SCORES = {
|
|
|
115
129
|
* @returns {number} Score from 0 to 1
|
|
116
130
|
*/
|
|
117
131
|
export function calculateGapScore(gap) {
|
|
118
|
-
if (gap <= 0) return
|
|
119
|
-
if (gap === 1) return
|
|
120
|
-
if (gap === 2) return
|
|
121
|
-
if (gap === 3) return
|
|
122
|
-
return
|
|
132
|
+
if (gap <= 0) return SCORE_GAP[0]; // Meets or exceeds
|
|
133
|
+
if (gap === 1) return SCORE_GAP[1];
|
|
134
|
+
if (gap === 2) return SCORE_GAP[2];
|
|
135
|
+
if (gap === 3) return SCORE_GAP[3];
|
|
136
|
+
return SCORE_GAP[4]; // 4+ levels below
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
/**
|
|
@@ -279,8 +293,12 @@ function calculateExpectationsScore(selfExpectations, jobExpectations) {
|
|
|
279
293
|
*/
|
|
280
294
|
export function calculateJobMatch(selfAssessment, job) {
|
|
281
295
|
// Get weights from track or use defaults (track may be null for trackless jobs)
|
|
282
|
-
const skillWeight =
|
|
283
|
-
|
|
296
|
+
const skillWeight =
|
|
297
|
+
job.track?.assessmentWeights?.skillWeight ??
|
|
298
|
+
WEIGHT_ASSESSMENT_SKILL_DEFAULT;
|
|
299
|
+
const behaviourWeight =
|
|
300
|
+
job.track?.assessmentWeights?.behaviourWeight ??
|
|
301
|
+
WEIGHT_ASSESSMENT_BEHAVIOUR_DEFAULT;
|
|
284
302
|
|
|
285
303
|
// Calculate skill score
|
|
286
304
|
const skillResult = calculateSkillScore(
|
|
@@ -306,7 +324,9 @@ export function calculateJobMatch(selfAssessment, job) {
|
|
|
306
324
|
job.expectations,
|
|
307
325
|
);
|
|
308
326
|
// Add up to 10% bonus for expectations match
|
|
309
|
-
overallScore =
|
|
327
|
+
overallScore =
|
|
328
|
+
overallScore * WEIGHT_SENIOR_BASE +
|
|
329
|
+
expectationsScore * WEIGHT_SENIOR_EXPECTATIONS;
|
|
310
330
|
}
|
|
311
331
|
|
|
312
332
|
// Combine all gaps
|
|
@@ -316,10 +336,10 @@ export function calculateJobMatch(selfAssessment, job) {
|
|
|
316
336
|
allGaps.sort((a, b) => b.gap - a.gap);
|
|
317
337
|
|
|
318
338
|
// Classify match into tier
|
|
319
|
-
const tier =
|
|
339
|
+
const tier = classifyMatch(overallScore);
|
|
320
340
|
|
|
321
|
-
// Identify top priority gaps
|
|
322
|
-
const priorityGaps = allGaps.slice(0,
|
|
341
|
+
// Identify top priority gaps
|
|
342
|
+
const priorityGaps = allGaps.slice(0, LIMIT_PRIORITY_GAPS);
|
|
323
343
|
|
|
324
344
|
const result = {
|
|
325
345
|
overallScore,
|
|
@@ -538,11 +558,11 @@ export function findRealisticMatches({
|
|
|
538
558
|
skills,
|
|
539
559
|
});
|
|
540
560
|
|
|
541
|
-
// Determine grade range (±
|
|
561
|
+
// Determine grade range (±RANGE_GRADE_OFFSET levels)
|
|
542
562
|
const bestFitLevel = estimatedGrade.grade.ordinalRank;
|
|
543
563
|
const gradeRange = {
|
|
544
|
-
min: bestFitLevel -
|
|
545
|
-
max: bestFitLevel +
|
|
564
|
+
min: bestFitLevel - RANGE_GRADE_OFFSET,
|
|
565
|
+
max: bestFitLevel + RANGE_GRADE_OFFSET,
|
|
546
566
|
};
|
|
547
567
|
|
|
548
568
|
// Find all matches
|
|
@@ -602,11 +622,11 @@ export function findRealisticMatches({
|
|
|
602
622
|
}
|
|
603
623
|
|
|
604
624
|
// Filter each tier to only show grades within reasonable range of highest match
|
|
605
|
-
// For Strong/Good matches: show up to
|
|
625
|
+
// For Strong/Good matches: show up to RANGE_READY_GRADE_OFFSET levels below highest match
|
|
606
626
|
// For Stretch/Aspirational: show only at or above highest match (growth opportunities)
|
|
607
627
|
if (highestMatchedLevel > 0) {
|
|
608
|
-
const minLevelForReady = highestMatchedLevel -
|
|
609
|
-
const minLevelForStretch = highestMatchedLevel;
|
|
628
|
+
const minLevelForReady = highestMatchedLevel - RANGE_READY_GRADE_OFFSET;
|
|
629
|
+
const minLevelForStretch = highestMatchedLevel;
|
|
610
630
|
|
|
611
631
|
matchesByTier[1] = matchesByTier[1].filter(
|
|
612
632
|
(m) => m.job.grade.ordinalRank >= minLevelForReady,
|
|
@@ -667,8 +687,12 @@ export function deriveDevelopmentPath({ selfAssessment, targetJob }) {
|
|
|
667
687
|
// - AI skills get a boost for "AI-era focus"
|
|
668
688
|
const gapSize = targetIndex - selfIndex;
|
|
669
689
|
const typeMultiplier =
|
|
670
|
-
jobSkill.type === "primary"
|
|
671
|
-
|
|
690
|
+
jobSkill.type === "primary"
|
|
691
|
+
? WEIGHT_DEV_TYPE_PRIMARY
|
|
692
|
+
: jobSkill.type === "secondary"
|
|
693
|
+
? WEIGHT_DEV_TYPE_SECONDARY
|
|
694
|
+
: WEIGHT_DEV_TYPE_BROAD;
|
|
695
|
+
const aiBoost = jobSkill.capability === "ai" ? WEIGHT_DEV_AI_BOOST : 1;
|
|
672
696
|
const priority = gapSize * typeMultiplier * aiBoost;
|
|
673
697
|
|
|
674
698
|
items.push({
|
|
@@ -789,8 +813,8 @@ export function findNextStepJob({
|
|
|
789
813
|
|
|
790
814
|
if (job) {
|
|
791
815
|
const analysis = calculateJobMatch(selfAssessment, job);
|
|
792
|
-
|
|
793
|
-
|
|
816
|
+
const trackBonus =
|
|
817
|
+
track.id === currentJob.track.id ? WEIGHT_SAME_TRACK_BONUS : 0;
|
|
794
818
|
candidates.push({
|
|
795
819
|
job,
|
|
796
820
|
analysis,
|
package/{lib → src}/modifiers.js
RENAMED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* (e.g., "delivery: 1", "scale: -1") - individual skill modifiers are not allowed.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { Capability } from "@forwardimpact/schema/levels";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Valid skill capability names for modifier expansion
|
|
13
13
|
* @type {Set<string>}
|
|
14
14
|
*/
|
|
15
|
-
const VALID_CAPABILITIES = new Set(
|
|
15
|
+
const VALID_CAPABILITIES = new Set(Object.values(Capability));
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Check if a key is a skill capability
|
|
@@ -66,7 +66,7 @@ export function buildCapabilityToSkillsMap(skills) {
|
|
|
66
66
|
* @param {import('./levels.js').Skill[]} skills - Array of all skills (for capability lookup)
|
|
67
67
|
* @returns {Object<string, number>} Expanded skill modifiers with individual skill IDs
|
|
68
68
|
*/
|
|
69
|
-
export function
|
|
69
|
+
export function expandModifiersToSkills(skillModifiers, skills) {
|
|
70
70
|
if (!skillModifiers) {
|
|
71
71
|
return {};
|
|
72
72
|
}
|
|
@@ -113,7 +113,7 @@ export function extractCapabilityModifiers(skillModifiers) {
|
|
|
113
113
|
* @param {Object<string, number>} skillModifiers - The skill modifiers
|
|
114
114
|
* @returns {Object<string, number>} Only the individual skill modifiers
|
|
115
115
|
*/
|
|
116
|
-
export function
|
|
116
|
+
export function extractSkillModifiers(skillModifiers) {
|
|
117
117
|
if (!skillModifiers) {
|
|
118
118
|
return {};
|
|
119
119
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composed Policy Definitions
|
|
3
|
+
*
|
|
4
|
+
* Named policy compositions for specific use cases.
|
|
5
|
+
* Each POLICY_* export defines a complete filtering/sorting strategy.
|
|
6
|
+
*
|
|
7
|
+
* These are the high-level policies used by consuming code.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { isAgentEligible } from "./predicates.js";
|
|
11
|
+
import { filterHighestLevel, composeFilters } from "./filters.js";
|
|
12
|
+
import {
|
|
13
|
+
compareByLevelDesc,
|
|
14
|
+
compareByMaturityDesc,
|
|
15
|
+
compareByTypeAndName,
|
|
16
|
+
compareBySkillPriority,
|
|
17
|
+
} from "./orderings.js";
|
|
18
|
+
import { LIMIT_AGENT_PROFILE_SKILLS } from "./thresholds.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Agent Skill Policies
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Filter for agent-eligible skills at highest derived level
|
|
26
|
+
*
|
|
27
|
+
* Agents receive skills after:
|
|
28
|
+
* 1. Excluding human-only skills (isAgentEligible)
|
|
29
|
+
* 2. Keeping only skills at the highest derived level
|
|
30
|
+
*
|
|
31
|
+
* This ensures agents focus on their peak competencies and
|
|
32
|
+
* respects track modifiers (a broad skill boosted to the same
|
|
33
|
+
* level as primary skills will be included).
|
|
34
|
+
*/
|
|
35
|
+
export const filterAgentSkills = composeFilters(
|
|
36
|
+
isAgentEligible,
|
|
37
|
+
filterHighestLevel,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Toolkit Extraction Policy
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Filter for toolkit extraction
|
|
46
|
+
*
|
|
47
|
+
* Tools are extracted only from highest-level skills,
|
|
48
|
+
* keeping the toolkit focused on core competencies.
|
|
49
|
+
*/
|
|
50
|
+
export const filterToolkitSkills = composeFilters(filterHighestLevel);
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Sorting Policies
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sort skills for agent profiles (level descending)
|
|
58
|
+
* @param {Array} skills - Skill matrix entries
|
|
59
|
+
* @returns {Array} Sorted skills (new array)
|
|
60
|
+
*/
|
|
61
|
+
export function sortAgentSkills(skills) {
|
|
62
|
+
return [...skills].sort(compareByLevelDesc);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sort behaviours for agent profiles (maturity descending)
|
|
67
|
+
* @param {Array} behaviours - Behaviour profile entries
|
|
68
|
+
* @returns {Array} Sorted behaviours (new array)
|
|
69
|
+
*/
|
|
70
|
+
export function sortAgentBehaviours(behaviours) {
|
|
71
|
+
return [...behaviours].sort(compareByMaturityDesc);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sort skills for job display (type ascending, then name)
|
|
76
|
+
* @param {Array} skills - Skill matrix entries
|
|
77
|
+
* @returns {Array} Sorted skills (new array)
|
|
78
|
+
*/
|
|
79
|
+
export function sortJobSkills(skills) {
|
|
80
|
+
return [...skills].sort(compareByTypeAndName);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Agent Profile Focus Policy
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Select the focused subset of agent skills for profile body
|
|
89
|
+
*
|
|
90
|
+
* Agent profiles include a limited skill index to avoid context bloat.
|
|
91
|
+
* Skills are ranked by priority (level desc, type asc, name asc) and
|
|
92
|
+
* the top N are returned, where N = LIMIT_AGENT_PROFILE_SKILLS.
|
|
93
|
+
*
|
|
94
|
+
* All skills are still exported as SKILL.md files and listed via --skills.
|
|
95
|
+
*
|
|
96
|
+
* @param {Array} skills - Agent-eligible skills (already filtered and sorted)
|
|
97
|
+
* @returns {Array} Top N skills by priority
|
|
98
|
+
*/
|
|
99
|
+
export function focusAgentSkills(skills) {
|
|
100
|
+
return [...skills]
|
|
101
|
+
.sort(compareBySkillPriority)
|
|
102
|
+
.slice(0, LIMIT_AGENT_PROFILE_SKILLS);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// =============================================================================
|
|
106
|
+
// Combined Filter + Sort Policies
|
|
107
|
+
// =============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Prepare skills for agent profile generation
|
|
111
|
+
*
|
|
112
|
+
* Complete pipeline:
|
|
113
|
+
* 1. Filter to agent-eligible skills
|
|
114
|
+
* 2. Keep only highest-level skills
|
|
115
|
+
* 3. Sort by level descending
|
|
116
|
+
*
|
|
117
|
+
* @param {Array} skillMatrix - Full skill matrix
|
|
118
|
+
* @returns {Array} Filtered and sorted skills
|
|
119
|
+
*/
|
|
120
|
+
export function prepareAgentSkillMatrix(skillMatrix) {
|
|
121
|
+
const filtered = filterAgentSkills(skillMatrix);
|
|
122
|
+
return sortAgentSkills(filtered);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Prepare behaviours for agent profile generation
|
|
127
|
+
*
|
|
128
|
+
* Sorts by maturity descending (highest first).
|
|
129
|
+
*
|
|
130
|
+
* @param {Array} behaviourProfile - Full behaviour profile
|
|
131
|
+
* @returns {Array} Sorted behaviours
|
|
132
|
+
*/
|
|
133
|
+
export function prepareAgentBehaviourProfile(behaviourProfile) {
|
|
134
|
+
return sortAgentBehaviours(behaviourProfile);
|
|
135
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matrix-Level Filter Functions
|
|
3
|
+
*
|
|
4
|
+
* Filters that operate on entire arrays of skill/behaviour entries.
|
|
5
|
+
* Unlike predicates (single entry → boolean), these transform arrays.
|
|
6
|
+
*
|
|
7
|
+
* Naming convention: filter* for functions that reduce/transform arrays.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getSkillLevelIndex } from "@forwardimpact/schema/levels";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Level-Based Filters
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Filter matrix to keep only skills at the highest derived level
|
|
18
|
+
*
|
|
19
|
+
* After track modifiers are applied, some skills will be at higher levels
|
|
20
|
+
* than others. This filter keeps only the skills at the maximum level.
|
|
21
|
+
*
|
|
22
|
+
* @param {Array} matrix - Skill matrix entries with derived levels
|
|
23
|
+
* @returns {Array} Filtered matrix with only max-level skills
|
|
24
|
+
*/
|
|
25
|
+
export function filterHighestLevel(matrix) {
|
|
26
|
+
if (matrix.length === 0) return [];
|
|
27
|
+
|
|
28
|
+
const maxIndex = Math.max(
|
|
29
|
+
...matrix.map((entry) => getSkillLevelIndex(entry.level)),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return matrix.filter((entry) => getSkillLevelIndex(entry.level) === maxIndex);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Filter matrix to exclude skills at awareness level
|
|
37
|
+
*
|
|
38
|
+
* Skills at awareness level are typically too basic for certain outputs
|
|
39
|
+
* like responsibilities derivation.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array} matrix - Skill matrix entries
|
|
42
|
+
* @returns {Array} Filtered matrix excluding awareness skills
|
|
43
|
+
*/
|
|
44
|
+
export function filterAboveAwareness(matrix) {
|
|
45
|
+
return matrix.filter((entry) => entry.level !== "awareness");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Predicate Application
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Apply a predicate to filter a matrix
|
|
54
|
+
*
|
|
55
|
+
* Convenience wrapper around Array.filter() for consistency with
|
|
56
|
+
* the policy API.
|
|
57
|
+
*
|
|
58
|
+
* @param {Function} predicate - Predicate function (entry → boolean)
|
|
59
|
+
* @returns {(matrix: Array) => Array} Curried filter function
|
|
60
|
+
*/
|
|
61
|
+
export function filterBy(predicate) {
|
|
62
|
+
return (matrix) => matrix.filter(predicate);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Pipeline Application
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply multiple filter operations in sequence
|
|
71
|
+
*
|
|
72
|
+
* Each operation can be either:
|
|
73
|
+
* - A predicate function (entry → boolean): used with Array.filter()
|
|
74
|
+
* - A matrix filter (array → array): applied directly
|
|
75
|
+
*
|
|
76
|
+
* Detection: if the function returns an array when given [{}], it's a matrix filter.
|
|
77
|
+
*
|
|
78
|
+
* @param {Array} matrix - Initial items
|
|
79
|
+
* @param {...Function} operations - Predicates or matrix filters
|
|
80
|
+
* @returns {Array} Transformed items
|
|
81
|
+
*/
|
|
82
|
+
export function applyFilters(matrix, ...operations) {
|
|
83
|
+
return operations.reduce((acc, op) => {
|
|
84
|
+
// Detect matrix filter by checking return type
|
|
85
|
+
// Matrix filters always return arrays; predicates return booleans
|
|
86
|
+
const testResult = op([{}]);
|
|
87
|
+
if (Array.isArray(testResult)) {
|
|
88
|
+
return op(acc);
|
|
89
|
+
}
|
|
90
|
+
return acc.filter(op);
|
|
91
|
+
}, matrix);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compose filter operations into a single filter function
|
|
96
|
+
*
|
|
97
|
+
* Useful for creating reusable composed policies.
|
|
98
|
+
*
|
|
99
|
+
* @param {...Function} operations - Predicates or matrix filters
|
|
100
|
+
* @returns {(matrix: Array) => Array} Composed filter function
|
|
101
|
+
*/
|
|
102
|
+
export function composeFilters(...operations) {
|
|
103
|
+
return (matrix) => applyFilters(matrix, ...operations);
|
|
104
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy Module Index
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all policy constants, predicates, filters, and orderings.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { THRESHOLD_MATCH_STRONG, isAgentEligible, filterHighestLevel }
|
|
8
|
+
* from "@forwardimpact/model/policies";
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Thresholds, scores, and weights
|
|
12
|
+
export {
|
|
13
|
+
// Match tier thresholds
|
|
14
|
+
THRESHOLD_MATCH_STRONG,
|
|
15
|
+
THRESHOLD_MATCH_GOOD,
|
|
16
|
+
THRESHOLD_MATCH_STRETCH,
|
|
17
|
+
THRESHOLD_MATCH_ASPIRATIONAL,
|
|
18
|
+
// Gap scores
|
|
19
|
+
SCORE_GAP_MEETS,
|
|
20
|
+
SCORE_GAP_MINOR,
|
|
21
|
+
SCORE_GAP_SIGNIFICANT,
|
|
22
|
+
SCORE_GAP_MAJOR,
|
|
23
|
+
SCORE_GAP_ASPIRATIONAL,
|
|
24
|
+
SCORE_GAP,
|
|
25
|
+
// Skill type weights
|
|
26
|
+
WEIGHT_SKILL_TYPE_PRIMARY,
|
|
27
|
+
WEIGHT_SKILL_TYPE_SECONDARY,
|
|
28
|
+
WEIGHT_SKILL_TYPE_BROAD,
|
|
29
|
+
WEIGHT_SKILL_TYPE_TRACK,
|
|
30
|
+
WEIGHT_SKILL_TYPE,
|
|
31
|
+
// Capability boosts
|
|
32
|
+
WEIGHT_CAPABILITY_AI,
|
|
33
|
+
WEIGHT_CAPABILITY_DELIVERY,
|
|
34
|
+
WEIGHT_CAPABILITY_BOOST,
|
|
35
|
+
// Behaviour weights
|
|
36
|
+
WEIGHT_BEHAVIOUR_BASE,
|
|
37
|
+
WEIGHT_BEHAVIOUR_MATURITY,
|
|
38
|
+
// Interview ratios
|
|
39
|
+
RATIO_SKILL_BEHAVIOUR,
|
|
40
|
+
// Level multipliers
|
|
41
|
+
WEIGHT_SKILL_LEVEL,
|
|
42
|
+
WEIGHT_BELOW_LEVEL_PENALTY,
|
|
43
|
+
// Development path weights
|
|
44
|
+
WEIGHT_DEV_TYPE_PRIMARY,
|
|
45
|
+
WEIGHT_DEV_TYPE_SECONDARY,
|
|
46
|
+
WEIGHT_DEV_TYPE_BROAD,
|
|
47
|
+
WEIGHT_DEV_AI_BOOST,
|
|
48
|
+
// Agent profile limits
|
|
49
|
+
LIMIT_AGENT_PROFILE_SKILLS,
|
|
50
|
+
LIMIT_AGENT_WORKING_STYLES,
|
|
51
|
+
// Interview time defaults
|
|
52
|
+
DEFAULT_INTERVIEW_QUESTION_MINUTES,
|
|
53
|
+
DEFAULT_DECOMPOSITION_QUESTION_MINUTES,
|
|
54
|
+
DEFAULT_SIMULATION_QUESTION_MINUTES,
|
|
55
|
+
TOLERANCE_INTERVIEW_BUDGET_MINUTES,
|
|
56
|
+
// Decomposition capability weights
|
|
57
|
+
WEIGHT_CAPABILITY_DECOMP_DELIVERY,
|
|
58
|
+
WEIGHT_CAPABILITY_DECOMP_SCALE,
|
|
59
|
+
WEIGHT_CAPABILITY_DECOMP_RELIABILITY,
|
|
60
|
+
WEIGHT_FOCUS_BOOST,
|
|
61
|
+
// Senior grade threshold
|
|
62
|
+
THRESHOLD_SENIOR_GRADE,
|
|
63
|
+
// Assessment weights
|
|
64
|
+
WEIGHT_ASSESSMENT_SKILL_DEFAULT,
|
|
65
|
+
WEIGHT_ASSESSMENT_BEHAVIOUR_DEFAULT,
|
|
66
|
+
WEIGHT_SENIOR_BASE,
|
|
67
|
+
WEIGHT_SENIOR_EXPECTATIONS,
|
|
68
|
+
// Match result limits
|
|
69
|
+
LIMIT_PRIORITY_GAPS,
|
|
70
|
+
WEIGHT_SAME_TRACK_BONUS,
|
|
71
|
+
// Realistic match filtering
|
|
72
|
+
RANGE_GRADE_OFFSET,
|
|
73
|
+
RANGE_READY_GRADE_OFFSET,
|
|
74
|
+
// Driver coverage thresholds
|
|
75
|
+
THRESHOLD_DRIVER_SKILL_LEVEL,
|
|
76
|
+
THRESHOLD_DRIVER_BEHAVIOUR_MATURITY,
|
|
77
|
+
} from "./thresholds.js";
|
|
78
|
+
|
|
79
|
+
// Predicates
|
|
80
|
+
export {
|
|
81
|
+
// Identity
|
|
82
|
+
isAny,
|
|
83
|
+
isNone,
|
|
84
|
+
// Human-only
|
|
85
|
+
isHumanOnly,
|
|
86
|
+
isAgentEligible,
|
|
87
|
+
// Skill types
|
|
88
|
+
isPrimary,
|
|
89
|
+
isSecondary,
|
|
90
|
+
isBroad,
|
|
91
|
+
isTrack,
|
|
92
|
+
isCore,
|
|
93
|
+
isSupporting,
|
|
94
|
+
// Skill levels
|
|
95
|
+
hasMinLevel,
|
|
96
|
+
hasLevel,
|
|
97
|
+
hasBelowLevel,
|
|
98
|
+
// Capabilities
|
|
99
|
+
isInCapability,
|
|
100
|
+
isInAnyCapability,
|
|
101
|
+
// Combinators
|
|
102
|
+
allOf,
|
|
103
|
+
anyOf,
|
|
104
|
+
not,
|
|
105
|
+
} from "./predicates.js";
|
|
106
|
+
|
|
107
|
+
// Filters
|
|
108
|
+
export {
|
|
109
|
+
filterHighestLevel,
|
|
110
|
+
filterAboveAwareness,
|
|
111
|
+
filterBy,
|
|
112
|
+
applyFilters,
|
|
113
|
+
composeFilters,
|
|
114
|
+
} from "./filters.js";
|
|
115
|
+
|
|
116
|
+
// Orderings
|
|
117
|
+
export {
|
|
118
|
+
// Canonical orders
|
|
119
|
+
ORDER_SKILL_TYPE,
|
|
120
|
+
ORDER_STAGE,
|
|
121
|
+
ORDER_AGENT_STAGE,
|
|
122
|
+
CHECKLIST_STAGE_MAP,
|
|
123
|
+
// Skill comparators
|
|
124
|
+
compareByLevelDesc,
|
|
125
|
+
compareByLevelAsc,
|
|
126
|
+
compareByType,
|
|
127
|
+
compareByName,
|
|
128
|
+
compareBySkillPriority,
|
|
129
|
+
compareByTypeAndName,
|
|
130
|
+
// Capability comparators
|
|
131
|
+
compareByCapability,
|
|
132
|
+
sortSkillsByCapability,
|
|
133
|
+
// Behaviour comparators
|
|
134
|
+
compareByMaturityDesc,
|
|
135
|
+
compareByMaturityAsc,
|
|
136
|
+
compareByBehaviourName,
|
|
137
|
+
compareByBehaviourPriority,
|
|
138
|
+
// Generic comparators
|
|
139
|
+
compareByOrder,
|
|
140
|
+
chainComparators,
|
|
141
|
+
// Change comparators
|
|
142
|
+
compareBySkillChange,
|
|
143
|
+
compareByBehaviourChange,
|
|
144
|
+
} from "./orderings.js";
|
|
145
|
+
|
|
146
|
+
// Composed policies
|
|
147
|
+
export {
|
|
148
|
+
// Agent skill filtering
|
|
149
|
+
filterAgentSkills,
|
|
150
|
+
filterToolkitSkills,
|
|
151
|
+
// Agent profile focus
|
|
152
|
+
focusAgentSkills,
|
|
153
|
+
// Sorting
|
|
154
|
+
sortAgentSkills,
|
|
155
|
+
sortAgentBehaviours,
|
|
156
|
+
sortJobSkills,
|
|
157
|
+
// Combined filter + sort
|
|
158
|
+
prepareAgentSkillMatrix,
|
|
159
|
+
prepareAgentBehaviourProfile,
|
|
160
|
+
} from "./composed.js";
|