@forwardimpact/model 0.1.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.
- package/lib/agent.js +754 -0
- package/lib/checklist.js +103 -0
- package/lib/derivation.js +766 -0
- package/lib/index.js +121 -0
- package/lib/interview.js +539 -0
- package/lib/job-cache.js +89 -0
- package/lib/job.js +228 -0
- package/lib/matching.js +891 -0
- package/lib/modifiers.js +158 -0
- package/lib/profile.js +262 -0
- package/lib/progression.js +510 -0
- package/package.json +35 -0
package/lib/modifiers.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Modifier Expansion Functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides pure functions for expanding capability-based skill modifiers
|
|
5
|
+
* to individual skill modifiers. Tracks define modifiers by capability only
|
|
6
|
+
* (e.g., "delivery: 1", "scale: -1") - individual skill modifiers are not allowed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CAPABILITY_ORDER } from "@forwardimpact/schema/levels";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Valid skill capability names for modifier expansion
|
|
13
|
+
* @type {Set<string>}
|
|
14
|
+
*/
|
|
15
|
+
const VALID_CAPABILITIES = new Set(CAPABILITY_ORDER);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if a key is a skill capability
|
|
19
|
+
* @param {string} key - The key to check
|
|
20
|
+
* @returns {boolean} True if the key is a valid skill capability
|
|
21
|
+
*/
|
|
22
|
+
export function isCapability(key) {
|
|
23
|
+
return VALID_CAPABILITIES.has(key);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get skills by capability from a skills array
|
|
28
|
+
* @param {import('./levels.js').Skill[]} skills - Array of all skills
|
|
29
|
+
* @param {string} capability - The capability to filter by
|
|
30
|
+
* @returns {import('./levels.js').Skill[]} Skills in the specified capability
|
|
31
|
+
*/
|
|
32
|
+
export function getSkillsByCapability(skills, capability) {
|
|
33
|
+
return skills.filter((skill) => skill.capability === capability);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a map of capability to skill IDs
|
|
38
|
+
* @param {import('./levels.js').Skill[]} skills - Array of all skills
|
|
39
|
+
* @returns {Object<string, string[]>} Map of capability to array of skill IDs
|
|
40
|
+
*/
|
|
41
|
+
export function buildCapabilityToSkillsMap(skills) {
|
|
42
|
+
const capabilityMap = {};
|
|
43
|
+
|
|
44
|
+
for (const capability of VALID_CAPABILITIES) {
|
|
45
|
+
capabilityMap[capability] = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const skill of skills) {
|
|
49
|
+
if (skill.capability && capabilityMap[skill.capability]) {
|
|
50
|
+
capabilityMap[skill.capability].push(skill.id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return capabilityMap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Expand capability-based skill modifiers to individual skill modifiers
|
|
59
|
+
*
|
|
60
|
+
* Takes a skillModifiers object containing capability-based modifiers only
|
|
61
|
+
* (e.g., { delivery: 1, scale: -1 }). Individual skill modifiers are not allowed.
|
|
62
|
+
*
|
|
63
|
+
* Returns an object with individual skill modifiers expanded from capabilities.
|
|
64
|
+
*
|
|
65
|
+
* @param {Object<string, number>} skillModifiers - The capability skill modifiers
|
|
66
|
+
* @param {import('./levels.js').Skill[]} skills - Array of all skills (for capability lookup)
|
|
67
|
+
* @returns {Object<string, number>} Expanded skill modifiers with individual skill IDs
|
|
68
|
+
*/
|
|
69
|
+
export function expandSkillModifiers(skillModifiers, skills) {
|
|
70
|
+
if (!skillModifiers) {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const capabilityMap = buildCapabilityToSkillsMap(skills);
|
|
75
|
+
const expanded = {};
|
|
76
|
+
|
|
77
|
+
// Expand capability modifiers to individual skills
|
|
78
|
+
for (const [key, modifier] of Object.entries(skillModifiers)) {
|
|
79
|
+
if (isCapability(key)) {
|
|
80
|
+
// This is a capability - expand to all skills in that capability
|
|
81
|
+
const skillIds = capabilityMap[key] || [];
|
|
82
|
+
for (const skillId of skillIds) {
|
|
83
|
+
expanded[skillId] = modifier;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Non-capability keys are ignored (validation should catch these)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return expanded;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract capability modifiers from a skillModifiers object
|
|
94
|
+
* @param {Object<string, number>} skillModifiers - The skill modifiers
|
|
95
|
+
* @returns {Object<string, number>} Only the capability-based modifiers
|
|
96
|
+
*/
|
|
97
|
+
export function extractCapabilityModifiers(skillModifiers) {
|
|
98
|
+
if (!skillModifiers) {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = {};
|
|
103
|
+
for (const [key, modifier] of Object.entries(skillModifiers)) {
|
|
104
|
+
if (isCapability(key)) {
|
|
105
|
+
result[key] = modifier;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract individual skill modifiers from a skillModifiers object
|
|
113
|
+
* @param {Object<string, number>} skillModifiers - The skill modifiers
|
|
114
|
+
* @returns {Object<string, number>} Only the individual skill modifiers
|
|
115
|
+
*/
|
|
116
|
+
export function extractIndividualModifiers(skillModifiers) {
|
|
117
|
+
if (!skillModifiers) {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = {};
|
|
122
|
+
for (const [key, modifier] of Object.entries(skillModifiers)) {
|
|
123
|
+
if (!isCapability(key)) {
|
|
124
|
+
result[key] = modifier;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the effective skill modifier for a specific skill
|
|
132
|
+
*
|
|
133
|
+
* Looks up the capability modifier for the skill's capability.
|
|
134
|
+
* Returns 0 if no modifier applies.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} skillId - The skill ID to get modifier for
|
|
137
|
+
* @param {Object<string, number>} skillModifiers - The capability skill modifiers
|
|
138
|
+
* @param {import('./levels.js').Skill[]} skills - Array of all skills
|
|
139
|
+
* @returns {number} The effective modifier for this skill
|
|
140
|
+
*/
|
|
141
|
+
export function resolveSkillModifier(skillId, skillModifiers, skills) {
|
|
142
|
+
if (!skillModifiers) {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Find the skill's capability
|
|
147
|
+
const skill = skills.find((s) => s.id === skillId);
|
|
148
|
+
if (!skill || !skill.capability) {
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for capability modifier
|
|
153
|
+
if (skill.capability in skillModifiers) {
|
|
154
|
+
return skillModifiers[skill.capability];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
package/lib/profile.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Profile Derivation
|
|
3
|
+
*
|
|
4
|
+
* Shared functions for deriving skill and behaviour profiles for both
|
|
5
|
+
* human jobs and AI agents. This module provides:
|
|
6
|
+
*
|
|
7
|
+
* 1. Filtering functions - reusable filters for skills and behaviours
|
|
8
|
+
* 2. Sorting functions - sort by level/maturity for display
|
|
9
|
+
* 3. prepareBaseProfile() - shared profile derivation used by both job.js and agent.js
|
|
10
|
+
*
|
|
11
|
+
* The core derivation (deriveSkillMatrix, deriveBehaviourProfile) remains in
|
|
12
|
+
* derivation.js. This module adds post-processing for specific use cases.
|
|
13
|
+
*
|
|
14
|
+
* Agent filtering keeps only skills at the highest derived level. This ensures
|
|
15
|
+
* track modifiers are respected—a broad skill boosted by a +1 track modifier
|
|
16
|
+
* may reach the same level as primary skills and thus be included.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
SKILL_LEVEL_ORDER,
|
|
21
|
+
BEHAVIOUR_MATURITY_ORDER,
|
|
22
|
+
} from "@forwardimpact/schema/levels";
|
|
23
|
+
import {
|
|
24
|
+
deriveSkillMatrix,
|
|
25
|
+
deriveBehaviourProfile,
|
|
26
|
+
deriveResponsibilities,
|
|
27
|
+
} from "./derivation.js";
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Skill Filters
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build set of capabilities with positive track modifiers
|
|
35
|
+
* @param {Object} track - Track definition
|
|
36
|
+
* @returns {Set<string>} Set of capability IDs with positive modifiers
|
|
37
|
+
*/
|
|
38
|
+
export function getPositiveTrackCapabilities(track) {
|
|
39
|
+
return new Set(
|
|
40
|
+
Object.entries(track.skillModifiers || {})
|
|
41
|
+
.filter(([_, modifier]) => modifier > 0)
|
|
42
|
+
.map(([capability]) => capability),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Filter out human-only skills
|
|
48
|
+
* Human-only skills are those requiring human presence/experience
|
|
49
|
+
* @param {Array} skillMatrix - Skill matrix entries
|
|
50
|
+
* @returns {Array} Filtered skill matrix
|
|
51
|
+
*/
|
|
52
|
+
export function filterHumanOnlySkills(skillMatrix) {
|
|
53
|
+
return skillMatrix.filter((entry) => !entry.isHumanOnly);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Filter skills to keep only those at the highest derived level
|
|
58
|
+
* After track modifiers are applied, some skills will be at higher levels
|
|
59
|
+
* than others. This filter keeps only the skills at the maximum level.
|
|
60
|
+
* @param {Array} skillMatrix - Skill matrix entries with derived levels
|
|
61
|
+
* @returns {Array} Filtered skill matrix containing only highest-level skills
|
|
62
|
+
*/
|
|
63
|
+
export function filterByHighestLevel(skillMatrix) {
|
|
64
|
+
if (skillMatrix.length === 0) return [];
|
|
65
|
+
|
|
66
|
+
// Find the highest level index in the matrix
|
|
67
|
+
const maxLevelIndex = Math.max(
|
|
68
|
+
...skillMatrix.map((entry) => SKILL_LEVEL_ORDER.indexOf(entry.level)),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Keep only skills at that level
|
|
72
|
+
return skillMatrix.filter(
|
|
73
|
+
(entry) => SKILL_LEVEL_ORDER.indexOf(entry.level) === maxLevelIndex,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Apply agent-specific skill filters
|
|
79
|
+
* Filters to human-only skills and keeps only skills at the highest derived level.
|
|
80
|
+
* This approach respects track modifiers—a broad skill boosted to the same level
|
|
81
|
+
* as primary skills will be included.
|
|
82
|
+
* @param {Array} skillMatrix - Skill matrix entries with derived levels
|
|
83
|
+
* @returns {Array} Filtered skill matrix
|
|
84
|
+
*/
|
|
85
|
+
export function filterSkillsForAgent(skillMatrix) {
|
|
86
|
+
// First exclude human-only skills
|
|
87
|
+
const withoutHumanOnly = filterHumanOnlySkills(skillMatrix);
|
|
88
|
+
|
|
89
|
+
// Then keep only skills at the highest level
|
|
90
|
+
return filterByHighestLevel(withoutHumanOnly);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Sorting Functions
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Sort skills by level (highest first)
|
|
99
|
+
* Used for agent profiles where top skills should appear first
|
|
100
|
+
* @param {Array} skillMatrix - Skill matrix entries
|
|
101
|
+
* @returns {Array} Sorted skill matrix (new array)
|
|
102
|
+
*/
|
|
103
|
+
export function sortByLevelDescending(skillMatrix) {
|
|
104
|
+
return [...skillMatrix].sort((a, b) => {
|
|
105
|
+
const aIndex = SKILL_LEVEL_ORDER.indexOf(a.level);
|
|
106
|
+
const bIndex = SKILL_LEVEL_ORDER.indexOf(b.level);
|
|
107
|
+
return bIndex - aIndex;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Sort behaviours by maturity (highest first)
|
|
113
|
+
* Used for agent profiles where top behaviours should appear first
|
|
114
|
+
* @param {Array} behaviourProfile - Behaviour profile entries
|
|
115
|
+
* @returns {Array} Sorted behaviour profile (new array)
|
|
116
|
+
*/
|
|
117
|
+
export function sortByMaturityDescending(behaviourProfile) {
|
|
118
|
+
return [...behaviourProfile].sort((a, b) => {
|
|
119
|
+
const aIndex = BEHAVIOUR_MATURITY_ORDER.indexOf(a.maturity);
|
|
120
|
+
const bIndex = BEHAVIOUR_MATURITY_ORDER.indexOf(b.maturity);
|
|
121
|
+
return bIndex - aIndex;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// Profile Derivation
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @typedef {Object} ProfileOptions
|
|
131
|
+
* @property {boolean} [excludeHumanOnly=false] - Filter out human-only skills
|
|
132
|
+
* @property {boolean} [keepHighestLevelOnly=false] - Keep only skills at the highest derived level
|
|
133
|
+
* @property {boolean} [sortByLevel=false] - Sort skills by level descending
|
|
134
|
+
* @property {boolean} [sortByMaturity=false] - Sort behaviours by maturity descending
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @typedef {Object} BaseProfile
|
|
139
|
+
* @property {Array} skillMatrix - Derived skill matrix
|
|
140
|
+
* @property {Array} behaviourProfile - Derived behaviour profile
|
|
141
|
+
* @property {Array} derivedResponsibilities - Derived responsibilities (if capabilities provided)
|
|
142
|
+
* @property {Object} discipline - The discipline
|
|
143
|
+
* @property {Object} track - The track
|
|
144
|
+
* @property {Object} grade - The grade
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Prepare a base profile shared by jobs and agents
|
|
149
|
+
*
|
|
150
|
+
* This is the unified entry point for profile derivation. Both human jobs
|
|
151
|
+
* and AI agents use this function, with different options:
|
|
152
|
+
*
|
|
153
|
+
* - Human jobs: No filtering, default sorting by type
|
|
154
|
+
* - AI agents: Filter humanOnly, keep only highest-level skills, sort by level
|
|
155
|
+
*
|
|
156
|
+
* @param {Object} params
|
|
157
|
+
* @param {Object} params.discipline - The discipline
|
|
158
|
+
* @param {Object} params.track - The track
|
|
159
|
+
* @param {Object} params.grade - The grade
|
|
160
|
+
* @param {Array} params.skills - All available skills
|
|
161
|
+
* @param {Array} params.behaviours - All available behaviours
|
|
162
|
+
* @param {Array} [params.capabilities] - Optional capabilities for responsibility derivation
|
|
163
|
+
* @param {ProfileOptions} [params.options={}] - Filtering and sorting options
|
|
164
|
+
* @returns {BaseProfile} The prepared profile
|
|
165
|
+
*/
|
|
166
|
+
export function prepareBaseProfile({
|
|
167
|
+
discipline,
|
|
168
|
+
track,
|
|
169
|
+
grade,
|
|
170
|
+
skills,
|
|
171
|
+
behaviours,
|
|
172
|
+
capabilities,
|
|
173
|
+
options = {},
|
|
174
|
+
}) {
|
|
175
|
+
const {
|
|
176
|
+
excludeHumanOnly = false,
|
|
177
|
+
keepHighestLevelOnly = false,
|
|
178
|
+
sortByLevel = false,
|
|
179
|
+
sortByMaturity = false,
|
|
180
|
+
} = options;
|
|
181
|
+
|
|
182
|
+
// Core derivation
|
|
183
|
+
let skillMatrix = deriveSkillMatrix({ discipline, grade, track, skills });
|
|
184
|
+
let behaviourProfile = deriveBehaviourProfile({
|
|
185
|
+
discipline,
|
|
186
|
+
grade,
|
|
187
|
+
track,
|
|
188
|
+
behaviours,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Apply skill filters
|
|
192
|
+
if (excludeHumanOnly) {
|
|
193
|
+
skillMatrix = filterHumanOnlySkills(skillMatrix);
|
|
194
|
+
}
|
|
195
|
+
if (keepHighestLevelOnly) {
|
|
196
|
+
skillMatrix = filterByHighestLevel(skillMatrix);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Apply sorting
|
|
200
|
+
if (sortByLevel) {
|
|
201
|
+
skillMatrix = sortByLevelDescending(skillMatrix);
|
|
202
|
+
}
|
|
203
|
+
if (sortByMaturity) {
|
|
204
|
+
behaviourProfile = sortByMaturityDescending(behaviourProfile);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Derive responsibilities if capabilities provided
|
|
208
|
+
let derivedResponsibilities = [];
|
|
209
|
+
if (capabilities && capabilities.length > 0) {
|
|
210
|
+
derivedResponsibilities = deriveResponsibilities({
|
|
211
|
+
skillMatrix,
|
|
212
|
+
capabilities,
|
|
213
|
+
track,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
skillMatrix,
|
|
219
|
+
behaviourProfile,
|
|
220
|
+
derivedResponsibilities,
|
|
221
|
+
discipline,
|
|
222
|
+
track,
|
|
223
|
+
grade,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Preset options for agent profile derivation
|
|
229
|
+
* Excludes human-only skills, keeps only skills at the highest derived level,
|
|
230
|
+
* and sorts by level/maturity descending
|
|
231
|
+
*/
|
|
232
|
+
export const AGENT_PROFILE_OPTIONS = {
|
|
233
|
+
excludeHumanOnly: true,
|
|
234
|
+
keepHighestLevelOnly: true,
|
|
235
|
+
sortByLevel: true,
|
|
236
|
+
sortByMaturity: true,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Prepare a profile optimized for agent generation
|
|
241
|
+
* Convenience function that applies AGENT_PROFILE_OPTIONS
|
|
242
|
+
* @param {Object} params - Same as prepareBaseProfile, without options
|
|
243
|
+
* @returns {BaseProfile} The prepared profile
|
|
244
|
+
*/
|
|
245
|
+
export function prepareAgentProfile({
|
|
246
|
+
discipline,
|
|
247
|
+
track,
|
|
248
|
+
grade,
|
|
249
|
+
skills,
|
|
250
|
+
behaviours,
|
|
251
|
+
capabilities,
|
|
252
|
+
}) {
|
|
253
|
+
return prepareBaseProfile({
|
|
254
|
+
discipline,
|
|
255
|
+
track,
|
|
256
|
+
grade,
|
|
257
|
+
skills,
|
|
258
|
+
behaviours,
|
|
259
|
+
capabilities,
|
|
260
|
+
options: AGENT_PROFILE_OPTIONS,
|
|
261
|
+
});
|
|
262
|
+
}
|