@forwardimpact/model 0.4.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.
@@ -16,6 +16,17 @@ 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
+ } from "./policies/thresholds.js";
29
+
19
30
  // ============================================================================
20
31
  // Match Tier Types and Constants
21
32
  // ============================================================================
@@ -34,25 +45,26 @@ export const MatchTier = {
34
45
 
35
46
  /**
36
47
  * Match tier configuration with thresholds and display properties
48
+ * Uses threshold constants from policies/thresholds.js
37
49
  * @type {Object<number, {label: string, color: string, minScore: number, description: string}>}
38
50
  */
39
- export const MATCH_TIER_CONFIG = {
51
+ export const CONFIG_MATCH_TIER = {
40
52
  [MatchTier.STRONG]: {
41
53
  label: "Strong Match",
42
54
  color: "green",
43
- minScore: 0.85,
55
+ minScore: THRESHOLD_MATCH_STRONG,
44
56
  description: "Ready for this role now",
45
57
  },
46
58
  [MatchTier.GOOD]: {
47
59
  label: "Good Match",
48
60
  color: "blue",
49
- minScore: 0.7,
61
+ minScore: THRESHOLD_MATCH_GOOD,
50
62
  description: "Ready within 6-12 months of focused growth",
51
63
  },
52
64
  [MatchTier.STRETCH]: {
53
65
  label: "Stretch Role",
54
66
  color: "amber",
55
- minScore: 0.55,
67
+ minScore: THRESHOLD_MATCH_STRETCH,
56
68
  description: "Ambitious but achievable with dedicated development",
57
69
  },
58
70
  [MatchTier.ASPIRATIONAL]: {
@@ -76,19 +88,19 @@ export const MATCH_TIER_CONFIG = {
76
88
  * @param {number} score - Match score from 0 to 1
77
89
  * @returns {MatchTierInfo} Tier classification
78
90
  */
79
- export function classifyMatchTier(score) {
80
- if (score >= MATCH_TIER_CONFIG[MatchTier.STRONG].minScore) {
81
- return { tier: MatchTier.STRONG, ...MATCH_TIER_CONFIG[MatchTier.STRONG] };
91
+ export function classifyMatch(score) {
92
+ if (score >= CONFIG_MATCH_TIER[MatchTier.STRONG].minScore) {
93
+ return { tier: MatchTier.STRONG, ...CONFIG_MATCH_TIER[MatchTier.STRONG] };
82
94
  }
83
- if (score >= MATCH_TIER_CONFIG[MatchTier.GOOD].minScore) {
84
- return { tier: MatchTier.GOOD, ...MATCH_TIER_CONFIG[MatchTier.GOOD] };
95
+ if (score >= CONFIG_MATCH_TIER[MatchTier.GOOD].minScore) {
96
+ return { tier: MatchTier.GOOD, ...CONFIG_MATCH_TIER[MatchTier.GOOD] };
85
97
  }
86
- if (score >= MATCH_TIER_CONFIG[MatchTier.STRETCH].minScore) {
87
- return { tier: MatchTier.STRETCH, ...MATCH_TIER_CONFIG[MatchTier.STRETCH] };
98
+ if (score >= CONFIG_MATCH_TIER[MatchTier.STRETCH].minScore) {
99
+ return { tier: MatchTier.STRETCH, ...CONFIG_MATCH_TIER[MatchTier.STRETCH] };
88
100
  }
89
101
  return {
90
102
  tier: MatchTier.ASPIRATIONAL,
91
- ...MATCH_TIER_CONFIG[MatchTier.ASPIRATIONAL],
103
+ ...CONFIG_MATCH_TIER[MatchTier.ASPIRATIONAL],
92
104
  };
93
105
  }
94
106
 
@@ -98,16 +110,10 @@ export function classifyMatchTier(score) {
98
110
 
99
111
  /**
100
112
  * Score values for different gap sizes
101
- * Uses a smooth decay that reflects real-world readiness
113
+ * Re-exported from policies/thresholds.js for backward compatibility
102
114
  * @type {Object<number, number>}
103
115
  */
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
- };
116
+ export const GAP_SCORES = SCORE_GAP;
111
117
 
112
118
  /**
113
119
  * Calculate gap score with smooth decay
@@ -115,11 +121,11 @@ export const GAP_SCORES = {
115
121
  * @returns {number} Score from 0 to 1
116
122
  */
117
123
  export function calculateGapScore(gap) {
118
- if (gap <= 0) return GAP_SCORES[0]; // Meets or exceeds
119
- if (gap === 1) return GAP_SCORES[1];
120
- if (gap === 2) return GAP_SCORES[2];
121
- if (gap === 3) return GAP_SCORES[3];
122
- return GAP_SCORES[4]; // 4+ levels below
124
+ if (gap <= 0) return SCORE_GAP[0]; // Meets or exceeds
125
+ if (gap === 1) return SCORE_GAP[1];
126
+ if (gap === 2) return SCORE_GAP[2];
127
+ if (gap === 3) return SCORE_GAP[3];
128
+ return SCORE_GAP[4]; // 4+ levels below
123
129
  }
124
130
 
125
131
  /**
@@ -316,7 +322,7 @@ export function calculateJobMatch(selfAssessment, job) {
316
322
  allGaps.sort((a, b) => b.gap - a.gap);
317
323
 
318
324
  // Classify match into tier
319
- const tier = classifyMatchTier(overallScore);
325
+ const tier = classifyMatch(overallScore);
320
326
 
321
327
  // Identify top priority gaps (top 3 by gap size)
322
328
  const priorityGaps = allGaps.slice(0, 3);
@@ -667,8 +673,12 @@ export function deriveDevelopmentPath({ selfAssessment, targetJob }) {
667
673
  // - AI skills get a boost for "AI-era focus"
668
674
  const gapSize = targetIndex - selfIndex;
669
675
  const typeMultiplier =
670
- jobSkill.type === "primary" ? 3 : jobSkill.type === "secondary" ? 2 : 1;
671
- const aiBoost = jobSkill.capability === "ai" ? 1.5 : 1;
676
+ jobSkill.type === "primary"
677
+ ? WEIGHT_DEV_TYPE_PRIMARY
678
+ : jobSkill.type === "secondary"
679
+ ? WEIGHT_DEV_TYPE_SECONDARY
680
+ : WEIGHT_DEV_TYPE_BROAD;
681
+ const aiBoost = jobSkill.capability === "ai" ? WEIGHT_DEV_AI_BOOST : 1;
672
682
  const priority = gapSize * typeMultiplier * aiBoost;
673
683
 
674
684
  items.push({
@@ -6,13 +6,13 @@
6
6
  * (e.g., "delivery: 1", "scale: -1") - individual skill modifiers are not allowed.
7
7
  */
8
8
 
9
- import { CAPABILITY_ORDER } from "@forwardimpact/schema/levels";
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(CAPABILITY_ORDER);
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 expandSkillModifiers(skillModifiers, skills) {
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 extractIndividualModifiers(skillModifiers) {
116
+ export function extractSkillModifiers(skillModifiers) {
117
117
  if (!skillModifiers) {
118
118
  return {};
119
119
  }
@@ -0,0 +1,111 @@
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
+ } from "./orderings.js";
17
+
18
+ // =============================================================================
19
+ // Agent Skill Policies
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Filter for agent-eligible skills at highest derived level
24
+ *
25
+ * Agents receive skills after:
26
+ * 1. Excluding human-only skills (isAgentEligible)
27
+ * 2. Keeping only skills at the highest derived level
28
+ *
29
+ * This ensures agents focus on their peak competencies and
30
+ * respects track modifiers (a broad skill boosted to the same
31
+ * level as primary skills will be included).
32
+ */
33
+ export const filterAgentSkills = composeFilters(
34
+ isAgentEligible,
35
+ filterHighestLevel,
36
+ );
37
+
38
+ // =============================================================================
39
+ // Toolkit Extraction Policy
40
+ // =============================================================================
41
+
42
+ /**
43
+ * Filter for toolkit extraction
44
+ *
45
+ * Tools are extracted only from highest-level skills,
46
+ * keeping the toolkit focused on core competencies.
47
+ */
48
+ export const filterToolkitSkills = composeFilters(filterHighestLevel);
49
+
50
+ // =============================================================================
51
+ // Sorting Policies
52
+ // =============================================================================
53
+
54
+ /**
55
+ * Sort skills for agent profiles (level descending)
56
+ * @param {Array} skills - Skill matrix entries
57
+ * @returns {Array} Sorted skills (new array)
58
+ */
59
+ export function sortAgentSkills(skills) {
60
+ return [...skills].sort(compareByLevelDesc);
61
+ }
62
+
63
+ /**
64
+ * Sort behaviours for agent profiles (maturity descending)
65
+ * @param {Array} behaviours - Behaviour profile entries
66
+ * @returns {Array} Sorted behaviours (new array)
67
+ */
68
+ export function sortAgentBehaviours(behaviours) {
69
+ return [...behaviours].sort(compareByMaturityDesc);
70
+ }
71
+
72
+ /**
73
+ * Sort skills for job display (type ascending, then name)
74
+ * @param {Array} skills - Skill matrix entries
75
+ * @returns {Array} Sorted skills (new array)
76
+ */
77
+ export function sortJobSkills(skills) {
78
+ return [...skills].sort(compareByTypeAndName);
79
+ }
80
+
81
+ // =============================================================================
82
+ // Combined Filter + Sort Policies
83
+ // =============================================================================
84
+
85
+ /**
86
+ * Prepare skills for agent profile generation
87
+ *
88
+ * Complete pipeline:
89
+ * 1. Filter to agent-eligible skills
90
+ * 2. Keep only highest-level skills
91
+ * 3. Sort by level descending
92
+ *
93
+ * @param {Array} skillMatrix - Full skill matrix
94
+ * @returns {Array} Filtered and sorted skills
95
+ */
96
+ export function prepareAgentSkillMatrix(skillMatrix) {
97
+ const filtered = filterAgentSkills(skillMatrix);
98
+ return sortAgentSkills(filtered);
99
+ }
100
+
101
+ /**
102
+ * Prepare behaviours for agent profile generation
103
+ *
104
+ * Sorts by maturity descending (highest first).
105
+ *
106
+ * @param {Array} behaviourProfile - Full behaviour profile
107
+ * @returns {Array} Sorted behaviours
108
+ */
109
+ export function prepareAgentBehaviourProfile(behaviourProfile) {
110
+ return sortAgentBehaviours(behaviourProfile);
111
+ }
@@ -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,128 @@
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
+ } from "./thresholds.js";
49
+
50
+ // Predicates
51
+ export {
52
+ // Identity
53
+ isAny,
54
+ isNone,
55
+ // Human-only
56
+ isHumanOnly,
57
+ isAgentEligible,
58
+ // Skill types
59
+ isPrimary,
60
+ isSecondary,
61
+ isBroad,
62
+ isTrack,
63
+ isCore,
64
+ isSupporting,
65
+ // Skill levels
66
+ hasMinLevel,
67
+ hasLevel,
68
+ hasBelowLevel,
69
+ // Capabilities
70
+ isInCapability,
71
+ isInAnyCapability,
72
+ // Combinators
73
+ allOf,
74
+ anyOf,
75
+ not,
76
+ } from "./predicates.js";
77
+
78
+ // Filters
79
+ export {
80
+ filterHighestLevel,
81
+ filterAboveAwareness,
82
+ filterBy,
83
+ applyFilters,
84
+ composeFilters,
85
+ } from "./filters.js";
86
+
87
+ // Orderings
88
+ export {
89
+ // Canonical orders
90
+ ORDER_SKILL_TYPE,
91
+ ORDER_STAGE,
92
+ ORDER_AGENT_STAGE,
93
+ // Skill comparators
94
+ compareByLevelDesc,
95
+ compareByLevelAsc,
96
+ compareByType,
97
+ compareByName,
98
+ compareBySkillPriority,
99
+ compareByTypeAndName,
100
+ // Capability comparators
101
+ compareByCapability,
102
+ sortSkillsByCapability,
103
+ // Behaviour comparators
104
+ compareByMaturityDesc,
105
+ compareByMaturityAsc,
106
+ compareByBehaviourName,
107
+ compareByBehaviourPriority,
108
+ // Generic comparators
109
+ compareByOrder,
110
+ chainComparators,
111
+ // Change comparators
112
+ compareBySkillChange,
113
+ compareByBehaviourChange,
114
+ } from "./orderings.js";
115
+
116
+ // Composed policies
117
+ export {
118
+ // Agent skill filtering
119
+ filterAgentSkills,
120
+ filterToolkitSkills,
121
+ // Sorting
122
+ sortAgentSkills,
123
+ sortAgentBehaviours,
124
+ sortJobSkills,
125
+ // Combined filter + sort
126
+ prepareAgentSkillMatrix,
127
+ prepareAgentBehaviourProfile,
128
+ } from "./composed.js";