@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/job-cache.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Cache
|
|
3
|
+
*
|
|
4
|
+
* Centralized caching for generated job definitions.
|
|
5
|
+
* Provides consistent key generation and get-or-create pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { deriveJob } from "./derivation.js";
|
|
9
|
+
|
|
10
|
+
/** @type {Map<string, Object>} */
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a consistent cache key from job parameters
|
|
15
|
+
* @param {string} disciplineId
|
|
16
|
+
* @param {string} gradeId
|
|
17
|
+
* @param {string} [trackId] - Optional track ID
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function makeJobKey(disciplineId, gradeId, trackId = null) {
|
|
21
|
+
if (trackId) {
|
|
22
|
+
return `${disciplineId}_${gradeId}_${trackId}`;
|
|
23
|
+
}
|
|
24
|
+
return `${disciplineId}_${gradeId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get or create a cached job definition
|
|
29
|
+
* @param {Object} params
|
|
30
|
+
* @param {Object} params.discipline
|
|
31
|
+
* @param {Object} params.grade
|
|
32
|
+
* @param {Object} [params.track] - Optional track
|
|
33
|
+
* @param {Array} params.skills
|
|
34
|
+
* @param {Array} params.behaviours
|
|
35
|
+
* @param {Array} [params.capabilities]
|
|
36
|
+
* @returns {Object|null}
|
|
37
|
+
*/
|
|
38
|
+
export function getOrCreateJob({
|
|
39
|
+
discipline,
|
|
40
|
+
grade,
|
|
41
|
+
track = null,
|
|
42
|
+
skills,
|
|
43
|
+
behaviours,
|
|
44
|
+
capabilities,
|
|
45
|
+
}) {
|
|
46
|
+
const key = makeJobKey(discipline.id, grade.id, track?.id);
|
|
47
|
+
|
|
48
|
+
if (!cache.has(key)) {
|
|
49
|
+
const job = deriveJob({
|
|
50
|
+
discipline,
|
|
51
|
+
grade,
|
|
52
|
+
track,
|
|
53
|
+
skills,
|
|
54
|
+
behaviours,
|
|
55
|
+
capabilities,
|
|
56
|
+
});
|
|
57
|
+
if (job) {
|
|
58
|
+
cache.set(key, job);
|
|
59
|
+
}
|
|
60
|
+
return job;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return cache.get(key);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Clear all cached jobs
|
|
68
|
+
*/
|
|
69
|
+
export function clearJobCache() {
|
|
70
|
+
cache.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Invalidate a specific job from the cache
|
|
75
|
+
* @param {string} disciplineId
|
|
76
|
+
* @param {string} gradeId
|
|
77
|
+
* @param {string} [trackId] - Optional track ID
|
|
78
|
+
*/
|
|
79
|
+
export function invalidateJob(disciplineId, gradeId, trackId = null) {
|
|
80
|
+
cache.delete(makeJobKey(disciplineId, gradeId, trackId));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the number of cached jobs (for testing/debugging)
|
|
85
|
+
* @returns {number}
|
|
86
|
+
*/
|
|
87
|
+
export function getCacheSize() {
|
|
88
|
+
return cache.size;
|
|
89
|
+
}
|
package/lib/job.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Model
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for preparing job data for display.
|
|
5
|
+
* Parallels model/agent.js in structure.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
calculateDriverCoverage,
|
|
10
|
+
generateJobTitle,
|
|
11
|
+
isValidJobCombination,
|
|
12
|
+
getDisciplineSkillIds,
|
|
13
|
+
} from "./derivation.js";
|
|
14
|
+
import { deriveChecklist } from "./checklist.js";
|
|
15
|
+
import { getOrCreateJob } from "./job-cache.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} JobDetailView
|
|
19
|
+
* @property {string} title
|
|
20
|
+
* @property {string} disciplineId
|
|
21
|
+
* @property {string} disciplineName
|
|
22
|
+
* @property {string} gradeId
|
|
23
|
+
* @property {string} gradeName
|
|
24
|
+
* @property {string} trackId
|
|
25
|
+
* @property {string} trackName
|
|
26
|
+
* @property {Object} expectations
|
|
27
|
+
* @property {Array} skillMatrix
|
|
28
|
+
* @property {Array} behaviourProfile
|
|
29
|
+
* @property {Array} derivedResponsibilities
|
|
30
|
+
* @property {Array} driverCoverage
|
|
31
|
+
* @property {Object} checklists - Handoff checklists keyed by handoff type
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prepare a job for detail view
|
|
36
|
+
* @param {Object} params
|
|
37
|
+
* @param {Object} params.discipline
|
|
38
|
+
* @param {Object} params.grade
|
|
39
|
+
* @param {Object} params.track
|
|
40
|
+
* @param {Array} params.skills
|
|
41
|
+
* @param {Array} params.behaviours
|
|
42
|
+
* @param {Array} params.drivers
|
|
43
|
+
* @param {Array} [params.capabilities]
|
|
44
|
+
* @returns {JobDetailView|null}
|
|
45
|
+
*/
|
|
46
|
+
export function prepareJobDetail({
|
|
47
|
+
discipline,
|
|
48
|
+
grade,
|
|
49
|
+
track,
|
|
50
|
+
skills,
|
|
51
|
+
behaviours,
|
|
52
|
+
drivers,
|
|
53
|
+
capabilities,
|
|
54
|
+
}) {
|
|
55
|
+
// Track is optional (null = generalist)
|
|
56
|
+
if (!discipline || !grade) return null;
|
|
57
|
+
|
|
58
|
+
const job = getOrCreateJob({
|
|
59
|
+
discipline,
|
|
60
|
+
grade,
|
|
61
|
+
track,
|
|
62
|
+
skills,
|
|
63
|
+
behaviours,
|
|
64
|
+
capabilities,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!job) return null;
|
|
68
|
+
|
|
69
|
+
const driverCoverage = calculateDriverCoverage({
|
|
70
|
+
job,
|
|
71
|
+
drivers,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Derive checklists for each stage
|
|
75
|
+
const checklists = {};
|
|
76
|
+
if (capabilities) {
|
|
77
|
+
const stageIds = ["plan", "code"];
|
|
78
|
+
for (const stageId of stageIds) {
|
|
79
|
+
checklists[stageId] = deriveChecklist({
|
|
80
|
+
stageId,
|
|
81
|
+
skillMatrix: job.skillMatrix,
|
|
82
|
+
skills,
|
|
83
|
+
capabilities,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
title: job.title,
|
|
90
|
+
disciplineId: discipline.id,
|
|
91
|
+
disciplineName: discipline.specialization || discipline.name,
|
|
92
|
+
gradeId: grade.id,
|
|
93
|
+
gradeName: grade.professionalTitle || grade.id,
|
|
94
|
+
trackId: track?.id || null,
|
|
95
|
+
trackName: track?.name || null,
|
|
96
|
+
expectations: job.expectations || {},
|
|
97
|
+
// Raw model data for components that need the original shape
|
|
98
|
+
skillMatrix: job.skillMatrix,
|
|
99
|
+
behaviourProfile: job.behaviourProfile,
|
|
100
|
+
derivedResponsibilities: job.derivedResponsibilities || [],
|
|
101
|
+
// Transformed driver coverage for display
|
|
102
|
+
driverCoverage: driverCoverage.map((d) => ({
|
|
103
|
+
id: d.driverId,
|
|
104
|
+
name: d.driverName,
|
|
105
|
+
coverage: d.overallScore,
|
|
106
|
+
skillsCovered: d.coveredSkills?.length || 0,
|
|
107
|
+
skillsTotal:
|
|
108
|
+
(d.coveredSkills?.length || 0) + (d.missingSkills?.length || 0),
|
|
109
|
+
behavioursCovered: d.coveredBehaviours?.length || 0,
|
|
110
|
+
behavioursTotal:
|
|
111
|
+
(d.coveredBehaviours?.length || 0) + (d.missingBehaviours?.length || 0),
|
|
112
|
+
})),
|
|
113
|
+
// Derived checklists by handoff type
|
|
114
|
+
checklists,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Prepare a job for list view (summary only)
|
|
120
|
+
* @param {Object} params
|
|
121
|
+
* @param {Object} params.discipline
|
|
122
|
+
* @param {Object} params.grade
|
|
123
|
+
* @param {Object} params.track
|
|
124
|
+
* @param {Array} params.skills
|
|
125
|
+
* @param {Array} params.behaviours
|
|
126
|
+
* @returns {Object|null}
|
|
127
|
+
*/
|
|
128
|
+
export function prepareJobSummary({
|
|
129
|
+
discipline,
|
|
130
|
+
grade,
|
|
131
|
+
track,
|
|
132
|
+
skills,
|
|
133
|
+
behaviours,
|
|
134
|
+
}) {
|
|
135
|
+
if (!discipline || !grade) return null;
|
|
136
|
+
|
|
137
|
+
const job = getOrCreateJob({
|
|
138
|
+
discipline,
|
|
139
|
+
grade,
|
|
140
|
+
track,
|
|
141
|
+
skills,
|
|
142
|
+
behaviours,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!job) return null;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
title: job.title,
|
|
149
|
+
disciplineId: discipline.id,
|
|
150
|
+
disciplineName: discipline.specialization || discipline.name,
|
|
151
|
+
gradeId: grade.id,
|
|
152
|
+
trackId: track?.id || null,
|
|
153
|
+
trackName: track?.name || null,
|
|
154
|
+
skillCount: job.skillMatrix.length,
|
|
155
|
+
behaviourCount: job.behaviourProfile.length,
|
|
156
|
+
primarySkillCount: job.skillMatrix.filter((s) => s.type === "primary")
|
|
157
|
+
.length,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @typedef {Object} JobBuilderPreview
|
|
163
|
+
* @property {boolean} isValid
|
|
164
|
+
* @property {string|null} title
|
|
165
|
+
* @property {number} totalSkills
|
|
166
|
+
* @property {number} totalBehaviours
|
|
167
|
+
* @property {string|null} invalidReason
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Prepare job builder preview for form validation
|
|
172
|
+
* @param {Object} params
|
|
173
|
+
* @param {Object|null} params.discipline
|
|
174
|
+
* @param {Object|null} params.grade
|
|
175
|
+
* @param {Object|null} params.track
|
|
176
|
+
* @param {number} params.behaviourCount - Total behaviours in the system
|
|
177
|
+
* @param {Array} [params.grades] - All grades for validation
|
|
178
|
+
* @returns {JobBuilderPreview}
|
|
179
|
+
*/
|
|
180
|
+
export function prepareJobBuilderPreview({
|
|
181
|
+
discipline,
|
|
182
|
+
grade,
|
|
183
|
+
track,
|
|
184
|
+
behaviourCount,
|
|
185
|
+
grades,
|
|
186
|
+
}) {
|
|
187
|
+
// Track is optional (null = generalist)
|
|
188
|
+
if (!discipline || !grade) {
|
|
189
|
+
return {
|
|
190
|
+
isValid: false,
|
|
191
|
+
title: null,
|
|
192
|
+
totalSkills: 0,
|
|
193
|
+
totalBehaviours: 0,
|
|
194
|
+
invalidReason: null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const validCombination = isValidJobCombination({
|
|
199
|
+
discipline,
|
|
200
|
+
grade,
|
|
201
|
+
track,
|
|
202
|
+
grades,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!validCombination) {
|
|
206
|
+
const reason = track
|
|
207
|
+
? `The ${track.name} track is not available for ${discipline.specialization}.`
|
|
208
|
+
: `${discipline.specialization} requires a track specialization.`;
|
|
209
|
+
return {
|
|
210
|
+
isValid: false,
|
|
211
|
+
title: null,
|
|
212
|
+
totalSkills: 0,
|
|
213
|
+
totalBehaviours: 0,
|
|
214
|
+
invalidReason: reason,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const title = generateJobTitle(discipline, grade, track);
|
|
219
|
+
const totalSkills = getDisciplineSkillIds(discipline).length;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
isValid: true,
|
|
223
|
+
title,
|
|
224
|
+
totalSkills,
|
|
225
|
+
totalBehaviours: behaviourCount,
|
|
226
|
+
invalidReason: null,
|
|
227
|
+
};
|
|
228
|
+
}
|