@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.
@@ -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
+ }