@forwardimpact/model 0.7.0 → 0.8.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/package.json +1 -1
- package/src/agent.js +26 -22
- package/src/checklist.js +9 -20
- package/src/derivation.js +11 -6
- package/src/index.js +30 -2
- package/src/interview.js +79 -46
- package/src/job.js +6 -3
- package/src/matching.js +27 -13
- package/src/policies/composed.js +24 -0
- package/src/policies/index.js +34 -2
- package/src/policies/orderings.js +22 -6
- package/src/policies/thresholds.js +157 -0
- package/src/profile.js +24 -61
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -24,8 +24,10 @@ import {
|
|
|
24
24
|
filterAgentSkills,
|
|
25
25
|
sortAgentSkills,
|
|
26
26
|
sortAgentBehaviours,
|
|
27
|
+
focusAgentSkills,
|
|
27
28
|
} from "./policies/composed.js";
|
|
28
|
-
import {
|
|
29
|
+
import { compareByStageOrder } from "./policies/orderings.js";
|
|
30
|
+
import { LIMIT_AGENT_WORKING_STYLES } from "./policies/thresholds.js";
|
|
29
31
|
import { SkillLevel } from "@forwardimpact/schema/levels";
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -194,7 +196,7 @@ function findAgentBehaviour(agentBehaviours, id) {
|
|
|
194
196
|
function buildWorkingStyleFromBehaviours(
|
|
195
197
|
derivedBehaviours,
|
|
196
198
|
agentBehaviours,
|
|
197
|
-
topN =
|
|
199
|
+
topN = LIMIT_AGENT_WORKING_STYLES,
|
|
198
200
|
) {
|
|
199
201
|
const entries = [];
|
|
200
202
|
|
|
@@ -267,18 +269,15 @@ export function generateSkillMarkdown(skillData, stages) {
|
|
|
267
269
|
stageName,
|
|
268
270
|
nextStageName,
|
|
269
271
|
focus: stageData.focus,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
+
readChecklist: stageData.readChecklist || [],
|
|
273
|
+
confirmChecklist: stageData.confirmChecklist || [],
|
|
272
274
|
};
|
|
273
275
|
},
|
|
274
276
|
);
|
|
275
277
|
|
|
276
|
-
// Sort stages using canonical ordering from
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
ORDER_AGENT_STAGE.indexOf(a.stageId) -
|
|
280
|
-
ORDER_AGENT_STAGE.indexOf(b.stageId),
|
|
281
|
-
);
|
|
278
|
+
// Sort stages using canonical ordering from loaded stage data
|
|
279
|
+
const stageComparator = compareByStageOrder(stages);
|
|
280
|
+
stagesArray.sort(stageComparator);
|
|
282
281
|
|
|
283
282
|
return {
|
|
284
283
|
frontmatter: {
|
|
@@ -431,9 +430,9 @@ export function deriveHandoffs({ stage, discipline, track, stages }) {
|
|
|
431
430
|
const baseName = `${abbrev}-${toKebabCase(track.id)}`;
|
|
432
431
|
|
|
433
432
|
return stage.handoffs.map((handoff) => {
|
|
434
|
-
// Find the target stage to get its
|
|
433
|
+
// Find the target stage to get its confirmChecklist
|
|
435
434
|
const targetStage = stages.find((s) => s.id === handoff.targetStage);
|
|
436
|
-
const
|
|
435
|
+
const confirmChecklist = targetStage?.confirmChecklist || [];
|
|
437
436
|
|
|
438
437
|
// Build rich prompt - formatted for single-line display
|
|
439
438
|
const promptParts = [handoff.prompt];
|
|
@@ -443,9 +442,9 @@ export function deriveHandoffs({ stage, discipline, track, stages }) {
|
|
|
443
442
|
`Summarize what was completed in the ${stage.name} stage.`,
|
|
444
443
|
);
|
|
445
444
|
|
|
446
|
-
// Add
|
|
447
|
-
if (
|
|
448
|
-
const formattedCriteria =
|
|
445
|
+
// Add confirm checklist from target stage with inline numbered list
|
|
446
|
+
if (confirmChecklist.length > 0) {
|
|
447
|
+
const formattedCriteria = confirmChecklist
|
|
449
448
|
.map((item, index) => `(${index + 1}) ${item}`)
|
|
450
449
|
.join(", ");
|
|
451
450
|
promptParts.push(
|
|
@@ -518,7 +517,7 @@ function buildStageProfileBodyData({
|
|
|
518
517
|
? substituteTemplateVars(rawPriority, humanDiscipline)
|
|
519
518
|
: null;
|
|
520
519
|
|
|
521
|
-
// Build skill index from derived skills
|
|
520
|
+
// Build skill index from derived skills (already focused by deriveStageAgent)
|
|
522
521
|
const skillIndex = derivedSkills
|
|
523
522
|
.map((derived) => {
|
|
524
523
|
const skill = skills.find((s) => s.id === derived.skillId);
|
|
@@ -538,7 +537,6 @@ function buildStageProfileBodyData({
|
|
|
538
537
|
const workingStyles = buildWorkingStyleFromBehaviours(
|
|
539
538
|
derivedBehaviours,
|
|
540
539
|
agentBehaviours,
|
|
541
|
-
3,
|
|
542
540
|
);
|
|
543
541
|
|
|
544
542
|
// Constraints (stage + discipline + track)
|
|
@@ -559,12 +557,15 @@ function buildStageProfileBodyData({
|
|
|
559
557
|
return {
|
|
560
558
|
title: `${name} - ${stageName} Agent`,
|
|
561
559
|
stageDescription: stage.description,
|
|
560
|
+
stageId: stage.id,
|
|
561
|
+
stageName,
|
|
562
|
+
isOnboard: stage.id === "onboard",
|
|
562
563
|
identity: identity.trim(),
|
|
563
564
|
priority: priority ? priority.trim() : null,
|
|
564
565
|
skillIndex,
|
|
565
566
|
roleContext,
|
|
566
567
|
workingStyles,
|
|
567
|
-
|
|
568
|
+
confirmChecklist: checklist || [],
|
|
568
569
|
constraints,
|
|
569
570
|
agentIndex: filteredAgentIndex,
|
|
570
571
|
hasAgentIndex: filteredAgentIndex.length > 0,
|
|
@@ -602,13 +603,16 @@ export function deriveStageAgent({
|
|
|
602
603
|
stages,
|
|
603
604
|
}) {
|
|
604
605
|
// Derive skills and behaviours
|
|
605
|
-
const
|
|
606
|
+
const allSkills = deriveAgentSkills({
|
|
606
607
|
discipline,
|
|
607
608
|
track,
|
|
608
609
|
grade,
|
|
609
610
|
skills,
|
|
610
611
|
});
|
|
611
612
|
|
|
613
|
+
// Focus skills for profile body (limited set to reduce context bloat)
|
|
614
|
+
const focusedSkills = focusAgentSkills(allSkills);
|
|
615
|
+
|
|
612
616
|
const derivedBehaviours = deriveAgentBehaviours({
|
|
613
617
|
discipline,
|
|
614
618
|
track,
|
|
@@ -624,13 +628,13 @@ export function deriveStageAgent({
|
|
|
624
628
|
stages,
|
|
625
629
|
});
|
|
626
630
|
|
|
627
|
-
// Derive checklist
|
|
631
|
+
// Derive checklist from focused skills only
|
|
628
632
|
const checklistStage = getChecklistStage(stage.id);
|
|
629
633
|
let checklist = [];
|
|
630
634
|
if (checklistStage && capabilities) {
|
|
631
635
|
checklist = deriveChecklist({
|
|
632
636
|
stageId: checklistStage,
|
|
633
|
-
skillMatrix:
|
|
637
|
+
skillMatrix: focusedSkills,
|
|
634
638
|
skills,
|
|
635
639
|
capabilities,
|
|
636
640
|
});
|
|
@@ -640,7 +644,7 @@ export function deriveStageAgent({
|
|
|
640
644
|
stage,
|
|
641
645
|
discipline,
|
|
642
646
|
track,
|
|
643
|
-
derivedSkills,
|
|
647
|
+
derivedSkills: focusedSkills,
|
|
644
648
|
derivedBehaviours,
|
|
645
649
|
handoffs,
|
|
646
650
|
constraints: [
|
package/src/checklist.js
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checklist Derivation
|
|
3
3
|
*
|
|
4
|
-
* Checklists are derived from skills with agent.stages.{stage}.
|
|
4
|
+
* Checklists are derived from skills with agent.stages.{stage}.confirmChecklist criteria.
|
|
5
5
|
* Each skill defines its own readiness criteria for stage transitions.
|
|
6
6
|
*
|
|
7
|
-
* Checklist = Stage × Skill Matrix × Skill
|
|
7
|
+
* Checklist = Stage × Skill Matrix × Skill Confirm Checklist
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Map from stage ID to the stage whose ready criteria should be shown
|
|
12
|
-
* (i.e., what must be ready before leaving this stage)
|
|
13
|
-
*/
|
|
14
|
-
const STAGE_TO_HANDOFF = {
|
|
15
|
-
plan: "plan", // Show plan.ready before leaving plan
|
|
16
|
-
code: "code", // Show code.ready before leaving code
|
|
17
|
-
review: "review", // Show review.ready (completion criteria)
|
|
18
|
-
};
|
|
19
|
-
|
|
20
10
|
/**
|
|
21
11
|
* Derive checklist items for a specific stage
|
|
22
12
|
* Returns skills grouped by capability with their ready criteria
|
|
@@ -34,11 +24,6 @@ export function deriveChecklist({
|
|
|
34
24
|
skills,
|
|
35
25
|
capabilities,
|
|
36
26
|
}) {
|
|
37
|
-
const targetStage = STAGE_TO_HANDOFF[stageId];
|
|
38
|
-
if (!targetStage) {
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
27
|
// Build skill lookup
|
|
43
28
|
const skillById = new Map(skills.map((s) => [s.id, s]));
|
|
44
29
|
|
|
@@ -53,8 +38,12 @@ export function deriveChecklist({
|
|
|
53
38
|
continue;
|
|
54
39
|
}
|
|
55
40
|
|
|
56
|
-
const stageData = skill.agent.stages[
|
|
57
|
-
if (
|
|
41
|
+
const stageData = skill.agent.stages[stageId];
|
|
42
|
+
if (
|
|
43
|
+
!stageData ||
|
|
44
|
+
!stageData.confirmChecklist ||
|
|
45
|
+
stageData.confirmChecklist.length === 0
|
|
46
|
+
) {
|
|
58
47
|
continue;
|
|
59
48
|
}
|
|
60
49
|
|
|
@@ -74,7 +63,7 @@ export function deriveChecklist({
|
|
|
74
63
|
name: capability.name,
|
|
75
64
|
emojiIcon: capability.emojiIcon,
|
|
76
65
|
},
|
|
77
|
-
items: stageData.
|
|
66
|
+
items: stageData.confirmChecklist,
|
|
78
67
|
});
|
|
79
68
|
}
|
|
80
69
|
|
package/src/derivation.js
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
SkillType,
|
|
10
|
-
SkillLevel,
|
|
11
|
-
BehaviourMaturity,
|
|
12
10
|
SKILL_LEVEL_ORDER,
|
|
13
11
|
getSkillLevelIndex,
|
|
14
12
|
getBehaviourMaturityIndex,
|
|
@@ -19,6 +17,11 @@ import {
|
|
|
19
17
|
|
|
20
18
|
import { resolveSkillModifier } from "./modifiers.js";
|
|
21
19
|
import { ORDER_SKILL_TYPE } from "./policies/orderings.js";
|
|
20
|
+
import {
|
|
21
|
+
THRESHOLD_SENIOR_GRADE,
|
|
22
|
+
THRESHOLD_DRIVER_SKILL_LEVEL,
|
|
23
|
+
THRESHOLD_DRIVER_BEHAVIOUR_MATURITY,
|
|
24
|
+
} from "./policies/thresholds.js";
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Build a Map of skillId → skillType for a discipline
|
|
@@ -595,7 +598,10 @@ export function calculateDriverCoverage({ job, drivers }) {
|
|
|
595
598
|
|
|
596
599
|
for (const skillId of contributingSkills) {
|
|
597
600
|
const level = jobSkillLevels.get(skillId);
|
|
598
|
-
if (
|
|
601
|
+
if (
|
|
602
|
+
level &&
|
|
603
|
+
skillLevelMeetsRequirement(level, THRESHOLD_DRIVER_SKILL_LEVEL)
|
|
604
|
+
) {
|
|
599
605
|
coveredSkills.push(skillId);
|
|
600
606
|
} else {
|
|
601
607
|
missingSkills.push(skillId);
|
|
@@ -611,7 +617,7 @@ export function calculateDriverCoverage({ job, drivers }) {
|
|
|
611
617
|
const coveredBehaviours = [];
|
|
612
618
|
const missingBehaviours = [];
|
|
613
619
|
const practicingIndex = getBehaviourMaturityIndex(
|
|
614
|
-
|
|
620
|
+
THRESHOLD_DRIVER_BEHAVIOUR_MATURITY,
|
|
615
621
|
);
|
|
616
622
|
|
|
617
623
|
for (const behaviourId of contributingBehaviours) {
|
|
@@ -678,8 +684,7 @@ export function getGradeLevel(grade) {
|
|
|
678
684
|
* @returns {boolean} True if the grade is senior level
|
|
679
685
|
*/
|
|
680
686
|
export function isSeniorGrade(grade) {
|
|
681
|
-
|
|
682
|
-
return grade.ordinalRank >= 5;
|
|
687
|
+
return grade.ordinalRank >= THRESHOLD_SENIOR_GRADE;
|
|
683
688
|
}
|
|
684
689
|
|
|
685
690
|
/**
|
package/src/index.js
CHANGED
|
@@ -130,6 +130,33 @@ export {
|
|
|
130
130
|
SCORE_GAP,
|
|
131
131
|
WEIGHT_SKILL_TYPE,
|
|
132
132
|
WEIGHT_CAPABILITY_BOOST,
|
|
133
|
+
// Interview thresholds
|
|
134
|
+
DEFAULT_INTERVIEW_QUESTION_MINUTES,
|
|
135
|
+
DEFAULT_DECOMPOSITION_QUESTION_MINUTES,
|
|
136
|
+
DEFAULT_SIMULATION_QUESTION_MINUTES,
|
|
137
|
+
TOLERANCE_INTERVIEW_BUDGET_MINUTES,
|
|
138
|
+
WEIGHT_CAPABILITY_DECOMP_DELIVERY,
|
|
139
|
+
WEIGHT_CAPABILITY_DECOMP_SCALE,
|
|
140
|
+
WEIGHT_CAPABILITY_DECOMP_RELIABILITY,
|
|
141
|
+
WEIGHT_FOCUS_BOOST,
|
|
142
|
+
// Senior grade
|
|
143
|
+
THRESHOLD_SENIOR_GRADE,
|
|
144
|
+
// Assessment weights
|
|
145
|
+
WEIGHT_ASSESSMENT_SKILL_DEFAULT,
|
|
146
|
+
WEIGHT_ASSESSMENT_BEHAVIOUR_DEFAULT,
|
|
147
|
+
WEIGHT_SENIOR_BASE,
|
|
148
|
+
WEIGHT_SENIOR_EXPECTATIONS,
|
|
149
|
+
// Match limits
|
|
150
|
+
LIMIT_PRIORITY_GAPS,
|
|
151
|
+
WEIGHT_SAME_TRACK_BONUS,
|
|
152
|
+
RANGE_GRADE_OFFSET,
|
|
153
|
+
RANGE_READY_GRADE_OFFSET,
|
|
154
|
+
// Driver coverage
|
|
155
|
+
THRESHOLD_DRIVER_SKILL_LEVEL,
|
|
156
|
+
THRESHOLD_DRIVER_BEHAVIOUR_MATURITY,
|
|
157
|
+
// Agent limits
|
|
158
|
+
LIMIT_AGENT_PROFILE_SKILLS,
|
|
159
|
+
LIMIT_AGENT_WORKING_STYLES,
|
|
133
160
|
// Predicates
|
|
134
161
|
isHumanOnly,
|
|
135
162
|
isAgentEligible,
|
|
@@ -150,8 +177,8 @@ export {
|
|
|
150
177
|
composeFilters,
|
|
151
178
|
// Orderings
|
|
152
179
|
ORDER_SKILL_TYPE,
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
getStageOrder,
|
|
181
|
+
compareByStageOrder,
|
|
155
182
|
compareByLevelDesc,
|
|
156
183
|
compareByType,
|
|
157
184
|
compareBySkillPriority,
|
|
@@ -160,6 +187,7 @@ export {
|
|
|
160
187
|
// Composed policies
|
|
161
188
|
filterAgentSkills,
|
|
162
189
|
filterToolkitSkills,
|
|
190
|
+
focusAgentSkills,
|
|
163
191
|
sortAgentSkills,
|
|
164
192
|
sortAgentBehaviours,
|
|
165
193
|
prepareAgentSkillMatrix,
|
package/src/interview.js
CHANGED
|
@@ -20,22 +20,17 @@ import {
|
|
|
20
20
|
WEIGHT_SKILL_LEVEL,
|
|
21
21
|
WEIGHT_BELOW_LEVEL_PENALTY,
|
|
22
22
|
RATIO_SKILL_BEHAVIOUR,
|
|
23
|
+
DEFAULT_INTERVIEW_QUESTION_MINUTES,
|
|
24
|
+
DEFAULT_DECOMPOSITION_QUESTION_MINUTES,
|
|
25
|
+
DEFAULT_SIMULATION_QUESTION_MINUTES,
|
|
26
|
+
TOLERANCE_INTERVIEW_BUDGET_MINUTES,
|
|
27
|
+
WEIGHT_CAPABILITY_DECOMP_DELIVERY,
|
|
28
|
+
WEIGHT_CAPABILITY_DECOMP_SCALE,
|
|
29
|
+
WEIGHT_CAPABILITY_DECOMP_RELIABILITY,
|
|
30
|
+
WEIGHT_FOCUS_BOOST,
|
|
23
31
|
} from "./policies/thresholds.js";
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
* Default question time estimate if not specified
|
|
27
|
-
*/
|
|
28
|
-
const DEFAULT_QUESTION_MINUTES = 5;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Default decomposition question time estimate
|
|
32
|
-
*/
|
|
33
|
-
const DEFAULT_DECOMPOSITION_MINUTES = 15;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Default stakeholder simulation question time estimate
|
|
37
|
-
*/
|
|
38
|
-
const DEFAULT_SIMULATION_MINUTES = 20;
|
|
33
|
+
import { compareByMaturityDesc } from "./policies/orderings.js";
|
|
39
34
|
|
|
40
35
|
/**
|
|
41
36
|
* Get questions from the question bank for a specific skill and level
|
|
@@ -136,7 +131,7 @@ function calculateSkillPriority(skill, includeBelowLevel = false) {
|
|
|
136
131
|
priority += WEIGHT_CAPABILITY_BOOST.ai;
|
|
137
132
|
}
|
|
138
133
|
|
|
139
|
-
// Delivery skills
|
|
134
|
+
// Delivery skills get a core technical boost
|
|
140
135
|
if (skill.capability === Capability.DELIVERY) {
|
|
141
136
|
priority += WEIGHT_CAPABILITY_BOOST.delivery;
|
|
142
137
|
}
|
|
@@ -178,11 +173,11 @@ function calculateCapabilityPriority(capabilityId, levelIndex) {
|
|
|
178
173
|
|
|
179
174
|
// Delivery and scale capabilities are typically more important for decomposition
|
|
180
175
|
if (capabilityId === Capability.DELIVERY) {
|
|
181
|
-
priority +=
|
|
176
|
+
priority += WEIGHT_CAPABILITY_DECOMP_DELIVERY;
|
|
182
177
|
} else if (capabilityId === Capability.SCALE) {
|
|
183
|
-
priority +=
|
|
178
|
+
priority += WEIGHT_CAPABILITY_DECOMP_SCALE;
|
|
184
179
|
} else if (capabilityId === Capability.RELIABILITY) {
|
|
185
|
-
priority +=
|
|
180
|
+
priority += WEIGHT_CAPABILITY_DECOMP_RELIABILITY;
|
|
186
181
|
}
|
|
187
182
|
|
|
188
183
|
// Higher level = higher priority
|
|
@@ -339,8 +334,11 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
|
|
|
339
334
|
for (const q of allSkillQuestions) {
|
|
340
335
|
if (selectedSkillIds.has(q.targetId)) continue; // Skip if we already have this skill
|
|
341
336
|
const questionTime =
|
|
342
|
-
q.question.expectedDurationMinutes ||
|
|
343
|
-
if (
|
|
337
|
+
q.question.expectedDurationMinutes || DEFAULT_INTERVIEW_QUESTION_MINUTES;
|
|
338
|
+
if (
|
|
339
|
+
skillMinutes + questionTime <=
|
|
340
|
+
skillTimeBudget + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
341
|
+
) {
|
|
344
342
|
selectedQuestions.push(q);
|
|
345
343
|
selectedSkillIds.add(q.targetId);
|
|
346
344
|
coveredSkills.add(q.targetId);
|
|
@@ -352,8 +350,11 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
|
|
|
352
350
|
for (const q of allSkillQuestions) {
|
|
353
351
|
if (selectedQuestions.includes(q)) continue; // Skip already selected
|
|
354
352
|
const questionTime =
|
|
355
|
-
q.question.expectedDurationMinutes ||
|
|
356
|
-
if (
|
|
353
|
+
q.question.expectedDurationMinutes || DEFAULT_INTERVIEW_QUESTION_MINUTES;
|
|
354
|
+
if (
|
|
355
|
+
skillMinutes + questionTime <=
|
|
356
|
+
skillTimeBudget + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
357
|
+
) {
|
|
357
358
|
selectedQuestions.push(q);
|
|
358
359
|
coveredSkills.add(q.targetId);
|
|
359
360
|
skillMinutes += questionTime;
|
|
@@ -368,8 +369,11 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
|
|
|
368
369
|
for (const q of allBehaviourQuestions) {
|
|
369
370
|
if (selectedBehaviourIds.has(q.targetId)) continue; // Skip if we already have this behaviour
|
|
370
371
|
const questionTime =
|
|
371
|
-
q.question.expectedDurationMinutes ||
|
|
372
|
-
if (
|
|
372
|
+
q.question.expectedDurationMinutes || DEFAULT_INTERVIEW_QUESTION_MINUTES;
|
|
373
|
+
if (
|
|
374
|
+
behaviourMinutes + questionTime <=
|
|
375
|
+
behaviourTimeBudget + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
376
|
+
) {
|
|
373
377
|
selectedQuestions.push(q);
|
|
374
378
|
selectedBehaviourIds.add(q.targetId);
|
|
375
379
|
coveredBehaviours.add(q.targetId);
|
|
@@ -381,8 +385,11 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
|
|
|
381
385
|
for (const q of allBehaviourQuestions) {
|
|
382
386
|
if (selectedQuestions.includes(q)) continue; // Skip already selected
|
|
383
387
|
const questionTime =
|
|
384
|
-
q.question.expectedDurationMinutes ||
|
|
385
|
-
if (
|
|
388
|
+
q.question.expectedDurationMinutes || DEFAULT_INTERVIEW_QUESTION_MINUTES;
|
|
389
|
+
if (
|
|
390
|
+
behaviourMinutes + questionTime <=
|
|
391
|
+
behaviourTimeBudget + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
392
|
+
) {
|
|
386
393
|
selectedQuestions.push(q);
|
|
387
394
|
coveredBehaviours.add(q.targetId);
|
|
388
395
|
behaviourMinutes += questionTime;
|
|
@@ -395,7 +402,9 @@ export function deriveInterviewQuestions({ job, questionBank, options = {} }) {
|
|
|
395
402
|
// Calculate total time
|
|
396
403
|
const expectedDurationMinutes = selectedQuestions.reduce(
|
|
397
404
|
(sum, q) =>
|
|
398
|
-
sum +
|
|
405
|
+
sum +
|
|
406
|
+
(q.question.expectedDurationMinutes ||
|
|
407
|
+
DEFAULT_INTERVIEW_QUESTION_MINUTES),
|
|
399
408
|
0,
|
|
400
409
|
);
|
|
401
410
|
|
|
@@ -469,10 +478,14 @@ export function deriveShortInterview({
|
|
|
469
478
|
}
|
|
470
479
|
|
|
471
480
|
const questionTime =
|
|
472
|
-
nextQuestion.question.expectedDurationMinutes ||
|
|
481
|
+
nextQuestion.question.expectedDurationMinutes ||
|
|
482
|
+
DEFAULT_INTERVIEW_QUESTION_MINUTES;
|
|
473
483
|
|
|
474
484
|
// Don't exceed budget by too much
|
|
475
|
-
if (
|
|
485
|
+
if (
|
|
486
|
+
totalMinutes + questionTime >
|
|
487
|
+
targetMinutes + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
488
|
+
) {
|
|
476
489
|
break;
|
|
477
490
|
}
|
|
478
491
|
|
|
@@ -549,7 +562,9 @@ export function deriveBehaviourQuestions({
|
|
|
549
562
|
// Calculate total time
|
|
550
563
|
const expectedDurationMinutes = interviewQuestions.reduce(
|
|
551
564
|
(sum, q) =>
|
|
552
|
-
sum +
|
|
565
|
+
sum +
|
|
566
|
+
(q.question.expectedDurationMinutes ||
|
|
567
|
+
DEFAULT_INTERVIEW_QUESTION_MINUTES),
|
|
553
568
|
0,
|
|
554
569
|
);
|
|
555
570
|
|
|
@@ -603,7 +618,7 @@ export function deriveFocusedInterview({
|
|
|
603
618
|
targetName: skill.skillName,
|
|
604
619
|
targetType: "skill",
|
|
605
620
|
targetLevel: skill.level,
|
|
606
|
-
priority: calculateSkillPriority(skill) +
|
|
621
|
+
priority: calculateSkillPriority(skill) + WEIGHT_FOCUS_BOOST,
|
|
607
622
|
});
|
|
608
623
|
coveredSkills.add(skill.skillId);
|
|
609
624
|
}
|
|
@@ -627,7 +642,7 @@ export function deriveFocusedInterview({
|
|
|
627
642
|
targetName: behaviour.behaviourName,
|
|
628
643
|
targetType: "behaviour",
|
|
629
644
|
targetLevel: behaviour.maturity,
|
|
630
|
-
priority: calculateBehaviourPriority(behaviour) +
|
|
645
|
+
priority: calculateBehaviourPriority(behaviour) + WEIGHT_FOCUS_BOOST,
|
|
631
646
|
});
|
|
632
647
|
coveredBehaviours.add(behaviour.behaviourId);
|
|
633
648
|
}
|
|
@@ -638,7 +653,9 @@ export function deriveFocusedInterview({
|
|
|
638
653
|
|
|
639
654
|
const expectedDurationMinutes = interviewQuestions.reduce(
|
|
640
655
|
(sum, q) =>
|
|
641
|
-
sum +
|
|
656
|
+
sum +
|
|
657
|
+
(q.question.expectedDurationMinutes ||
|
|
658
|
+
DEFAULT_INTERVIEW_QUESTION_MINUTES),
|
|
642
659
|
0,
|
|
643
660
|
);
|
|
644
661
|
|
|
@@ -734,8 +751,11 @@ export function deriveMissionFitInterview({
|
|
|
734
751
|
for (const q of allSkillQuestions) {
|
|
735
752
|
if (selectedSkillIds.has(q.targetId)) continue;
|
|
736
753
|
const questionTime =
|
|
737
|
-
q.question.expectedDurationMinutes ||
|
|
738
|
-
if (
|
|
754
|
+
q.question.expectedDurationMinutes || DEFAULT_INTERVIEW_QUESTION_MINUTES;
|
|
755
|
+
if (
|
|
756
|
+
totalMinutes + questionTime <=
|
|
757
|
+
targetMinutes + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
758
|
+
) {
|
|
739
759
|
selectedQuestions.push(q);
|
|
740
760
|
selectedSkillIds.add(q.targetId);
|
|
741
761
|
coveredSkills.add(q.targetId);
|
|
@@ -747,8 +767,11 @@ export function deriveMissionFitInterview({
|
|
|
747
767
|
for (const q of allSkillQuestions) {
|
|
748
768
|
if (selectedQuestions.includes(q)) continue;
|
|
749
769
|
const questionTime =
|
|
750
|
-
q.question.expectedDurationMinutes ||
|
|
751
|
-
if (
|
|
770
|
+
q.question.expectedDurationMinutes || DEFAULT_INTERVIEW_QUESTION_MINUTES;
|
|
771
|
+
if (
|
|
772
|
+
totalMinutes + questionTime <=
|
|
773
|
+
targetMinutes + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
774
|
+
) {
|
|
752
775
|
selectedQuestions.push(q);
|
|
753
776
|
coveredSkills.add(q.targetId);
|
|
754
777
|
totalMinutes += questionTime;
|
|
@@ -854,8 +877,12 @@ export function deriveDecompositionInterview({
|
|
|
854
877
|
for (const q of allCapabilityQuestions) {
|
|
855
878
|
if (selectedCapabilityIds.has(q.targetId)) continue;
|
|
856
879
|
const questionTime =
|
|
857
|
-
q.question.expectedDurationMinutes ||
|
|
858
|
-
|
|
880
|
+
q.question.expectedDurationMinutes ||
|
|
881
|
+
DEFAULT_DECOMPOSITION_QUESTION_MINUTES;
|
|
882
|
+
if (
|
|
883
|
+
totalMinutes + questionTime <=
|
|
884
|
+
targetMinutes + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
885
|
+
) {
|
|
859
886
|
selectedQuestions.push(q);
|
|
860
887
|
selectedCapabilityIds.add(q.targetId);
|
|
861
888
|
coveredCapabilities.add(q.targetId);
|
|
@@ -867,8 +894,12 @@ export function deriveDecompositionInterview({
|
|
|
867
894
|
for (const q of allCapabilityQuestions) {
|
|
868
895
|
if (selectedQuestions.includes(q)) continue;
|
|
869
896
|
const questionTime =
|
|
870
|
-
q.question.expectedDurationMinutes ||
|
|
871
|
-
|
|
897
|
+
q.question.expectedDurationMinutes ||
|
|
898
|
+
DEFAULT_DECOMPOSITION_QUESTION_MINUTES;
|
|
899
|
+
if (
|
|
900
|
+
totalMinutes + questionTime <=
|
|
901
|
+
targetMinutes + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
902
|
+
) {
|
|
872
903
|
selectedQuestions.push(q);
|
|
873
904
|
coveredCapabilities.add(q.targetId);
|
|
874
905
|
totalMinutes += questionTime;
|
|
@@ -915,9 +946,7 @@ export function deriveStakeholderInterview({
|
|
|
915
946
|
|
|
916
947
|
// Sort behaviours by maturity (highest first) to prioritize the most emphasized
|
|
917
948
|
const sortedBehaviours = [...job.behaviourProfile].sort(
|
|
918
|
-
|
|
919
|
-
getBehaviourMaturityIndex(b.maturity) -
|
|
920
|
-
getBehaviourMaturityIndex(a.maturity),
|
|
949
|
+
compareByMaturityDesc,
|
|
921
950
|
);
|
|
922
951
|
|
|
923
952
|
// Select one question per behaviour, highest maturity first, within budget
|
|
@@ -940,8 +969,12 @@ export function deriveStakeholderInterview({
|
|
|
940
969
|
if (!question) continue;
|
|
941
970
|
|
|
942
971
|
const questionTime =
|
|
943
|
-
question.expectedDurationMinutes ||
|
|
944
|
-
if (
|
|
972
|
+
question.expectedDurationMinutes || DEFAULT_SIMULATION_QUESTION_MINUTES;
|
|
973
|
+
if (
|
|
974
|
+
totalMinutes + questionTime >
|
|
975
|
+
targetMinutes + TOLERANCE_INTERVIEW_BUDGET_MINUTES
|
|
976
|
+
)
|
|
977
|
+
break;
|
|
945
978
|
|
|
946
979
|
selectedQuestions.push({
|
|
947
980
|
question,
|
package/src/job.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { deriveChecklist } from "./checklist.js";
|
|
15
15
|
import { deriveToolkit } from "./toolkit.js";
|
|
16
16
|
import { getOrCreateJob } from "./job-cache.js";
|
|
17
|
+
import { getStageOrder } from "@forwardimpact/schema/levels";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* @typedef {Object} JobDetailView
|
|
@@ -43,6 +44,7 @@ import { getOrCreateJob } from "./job-cache.js";
|
|
|
43
44
|
* @param {Array} params.behaviours
|
|
44
45
|
* @param {Array} params.drivers
|
|
45
46
|
* @param {Array} [params.capabilities]
|
|
47
|
+
* @param {Array} [params.stages] - Loaded stages for checklist derivation
|
|
46
48
|
* @returns {JobDetailView|null}
|
|
47
49
|
*/
|
|
48
50
|
export function prepareJobDetail({
|
|
@@ -53,6 +55,7 @@ export function prepareJobDetail({
|
|
|
53
55
|
behaviours,
|
|
54
56
|
drivers,
|
|
55
57
|
capabilities,
|
|
58
|
+
stages,
|
|
56
59
|
}) {
|
|
57
60
|
// Track is optional (null = generalist)
|
|
58
61
|
if (!discipline || !grade) return null;
|
|
@@ -73,10 +76,10 @@ export function prepareJobDetail({
|
|
|
73
76
|
drivers,
|
|
74
77
|
});
|
|
75
78
|
|
|
76
|
-
// Derive checklists for each stage
|
|
79
|
+
// Derive checklists for each stage from loaded stage data
|
|
77
80
|
const checklists = {};
|
|
78
|
-
if (capabilities) {
|
|
79
|
-
const stageIds =
|
|
81
|
+
if (capabilities && stages) {
|
|
82
|
+
const stageIds = getStageOrder(stages);
|
|
80
83
|
for (const stageId of stageIds) {
|
|
81
84
|
checklists[stageId] = deriveChecklist({
|
|
82
85
|
stageId,
|
package/src/matching.js
CHANGED
|
@@ -25,6 +25,14 @@ import {
|
|
|
25
25
|
WEIGHT_DEV_TYPE_SECONDARY,
|
|
26
26
|
WEIGHT_DEV_TYPE_BROAD,
|
|
27
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,
|
|
28
36
|
} from "./policies/thresholds.js";
|
|
29
37
|
|
|
30
38
|
// ============================================================================
|
|
@@ -285,8 +293,12 @@ function calculateExpectationsScore(selfExpectations, jobExpectations) {
|
|
|
285
293
|
*/
|
|
286
294
|
export function calculateJobMatch(selfAssessment, job) {
|
|
287
295
|
// Get weights from track or use defaults (track may be null for trackless jobs)
|
|
288
|
-
const skillWeight =
|
|
289
|
-
|
|
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;
|
|
290
302
|
|
|
291
303
|
// Calculate skill score
|
|
292
304
|
const skillResult = calculateSkillScore(
|
|
@@ -312,7 +324,9 @@ export function calculateJobMatch(selfAssessment, job) {
|
|
|
312
324
|
job.expectations,
|
|
313
325
|
);
|
|
314
326
|
// Add up to 10% bonus for expectations match
|
|
315
|
-
overallScore =
|
|
327
|
+
overallScore =
|
|
328
|
+
overallScore * WEIGHT_SENIOR_BASE +
|
|
329
|
+
expectationsScore * WEIGHT_SENIOR_EXPECTATIONS;
|
|
316
330
|
}
|
|
317
331
|
|
|
318
332
|
// Combine all gaps
|
|
@@ -324,8 +338,8 @@ export function calculateJobMatch(selfAssessment, job) {
|
|
|
324
338
|
// Classify match into tier
|
|
325
339
|
const tier = classifyMatch(overallScore);
|
|
326
340
|
|
|
327
|
-
// Identify top priority gaps
|
|
328
|
-
const priorityGaps = allGaps.slice(0,
|
|
341
|
+
// Identify top priority gaps
|
|
342
|
+
const priorityGaps = allGaps.slice(0, LIMIT_PRIORITY_GAPS);
|
|
329
343
|
|
|
330
344
|
const result = {
|
|
331
345
|
overallScore,
|
|
@@ -544,11 +558,11 @@ export function findRealisticMatches({
|
|
|
544
558
|
skills,
|
|
545
559
|
});
|
|
546
560
|
|
|
547
|
-
// Determine grade range (±
|
|
561
|
+
// Determine grade range (±RANGE_GRADE_OFFSET levels)
|
|
548
562
|
const bestFitLevel = estimatedGrade.grade.ordinalRank;
|
|
549
563
|
const gradeRange = {
|
|
550
|
-
min: bestFitLevel -
|
|
551
|
-
max: bestFitLevel +
|
|
564
|
+
min: bestFitLevel - RANGE_GRADE_OFFSET,
|
|
565
|
+
max: bestFitLevel + RANGE_GRADE_OFFSET,
|
|
552
566
|
};
|
|
553
567
|
|
|
554
568
|
// Find all matches
|
|
@@ -608,11 +622,11 @@ export function findRealisticMatches({
|
|
|
608
622
|
}
|
|
609
623
|
|
|
610
624
|
// Filter each tier to only show grades within reasonable range of highest match
|
|
611
|
-
// For Strong/Good matches: show up to
|
|
625
|
+
// For Strong/Good matches: show up to RANGE_READY_GRADE_OFFSET levels below highest match
|
|
612
626
|
// For Stretch/Aspirational: show only at or above highest match (growth opportunities)
|
|
613
627
|
if (highestMatchedLevel > 0) {
|
|
614
|
-
const minLevelForReady = highestMatchedLevel -
|
|
615
|
-
const minLevelForStretch = highestMatchedLevel;
|
|
628
|
+
const minLevelForReady = highestMatchedLevel - RANGE_READY_GRADE_OFFSET;
|
|
629
|
+
const minLevelForStretch = highestMatchedLevel;
|
|
616
630
|
|
|
617
631
|
matchesByTier[1] = matchesByTier[1].filter(
|
|
618
632
|
(m) => m.job.grade.ordinalRank >= minLevelForReady,
|
|
@@ -799,8 +813,8 @@ export function findNextStepJob({
|
|
|
799
813
|
|
|
800
814
|
if (job) {
|
|
801
815
|
const analysis = calculateJobMatch(selfAssessment, job);
|
|
802
|
-
|
|
803
|
-
|
|
816
|
+
const trackBonus =
|
|
817
|
+
track.id === currentJob.track.id ? WEIGHT_SAME_TRACK_BONUS : 0;
|
|
804
818
|
candidates.push({
|
|
805
819
|
job,
|
|
806
820
|
analysis,
|
package/src/policies/composed.js
CHANGED
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
compareByLevelDesc,
|
|
14
14
|
compareByMaturityDesc,
|
|
15
15
|
compareByTypeAndName,
|
|
16
|
+
compareBySkillPriority,
|
|
16
17
|
} from "./orderings.js";
|
|
18
|
+
import { LIMIT_AGENT_PROFILE_SKILLS } from "./thresholds.js";
|
|
17
19
|
|
|
18
20
|
// =============================================================================
|
|
19
21
|
// Agent Skill Policies
|
|
@@ -78,6 +80,28 @@ export function sortJobSkills(skills) {
|
|
|
78
80
|
return [...skills].sort(compareByTypeAndName);
|
|
79
81
|
}
|
|
80
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
|
+
|
|
81
105
|
// =============================================================================
|
|
82
106
|
// Combined Filter + Sort Policies
|
|
83
107
|
// =============================================================================
|
package/src/policies/index.js
CHANGED
|
@@ -45,6 +45,35 @@ export {
|
|
|
45
45
|
WEIGHT_DEV_TYPE_SECONDARY,
|
|
46
46
|
WEIGHT_DEV_TYPE_BROAD,
|
|
47
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,
|
|
48
77
|
} from "./thresholds.js";
|
|
49
78
|
|
|
50
79
|
// Predicates
|
|
@@ -88,8 +117,9 @@ export {
|
|
|
88
117
|
export {
|
|
89
118
|
// Canonical orders
|
|
90
119
|
ORDER_SKILL_TYPE,
|
|
91
|
-
|
|
92
|
-
|
|
120
|
+
// Data-driven stage ordering
|
|
121
|
+
getStageOrder,
|
|
122
|
+
compareByStageOrder,
|
|
93
123
|
// Skill comparators
|
|
94
124
|
compareByLevelDesc,
|
|
95
125
|
compareByLevelAsc,
|
|
@@ -118,6 +148,8 @@ export {
|
|
|
118
148
|
// Agent skill filtering
|
|
119
149
|
filterAgentSkills,
|
|
120
150
|
filterToolkitSkills,
|
|
151
|
+
// Agent profile focus
|
|
152
|
+
focusAgentSkills,
|
|
121
153
|
// Sorting
|
|
122
154
|
sortAgentSkills,
|
|
123
155
|
sortAgentBehaviours,
|
|
@@ -12,8 +12,12 @@ import {
|
|
|
12
12
|
getSkillLevelIndex,
|
|
13
13
|
getBehaviourMaturityIndex,
|
|
14
14
|
getCapabilityOrder,
|
|
15
|
+
getStageOrder,
|
|
15
16
|
} from "@forwardimpact/schema/levels";
|
|
16
17
|
|
|
18
|
+
// Re-export getStageOrder for consumers
|
|
19
|
+
export { getStageOrder };
|
|
20
|
+
|
|
17
21
|
// =============================================================================
|
|
18
22
|
// Canonical Orderings
|
|
19
23
|
// =============================================================================
|
|
@@ -24,15 +28,27 @@ import {
|
|
|
24
28
|
*/
|
|
25
29
|
export const ORDER_SKILL_TYPE = ["primary", "secondary", "broad", "track"];
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
export const ORDER_STAGE = ["specify", "plan", "code", "review", "deploy"];
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Stage Comparators
|
|
33
|
+
// =============================================================================
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
*
|
|
36
|
+
* Create a comparator for sorting by stage lifecycle order
|
|
37
|
+
*
|
|
38
|
+
* The returned comparator uses the canonical order from loaded stage data,
|
|
39
|
+
* making the ordering data-driven rather than hardcoded.
|
|
40
|
+
*
|
|
41
|
+
* @param {Object[]} stages - Loaded stages array from stages.yaml
|
|
42
|
+
* @returns {(a: Object, b: Object) => number} Comparator function
|
|
34
43
|
*/
|
|
35
|
-
export
|
|
44
|
+
export function compareByStageOrder(stages) {
|
|
45
|
+
const order = getStageOrder(stages);
|
|
46
|
+
return (a, b) => {
|
|
47
|
+
const stageA = a.stageId || a.id || "";
|
|
48
|
+
const stageB = b.stageId || b.id || "";
|
|
49
|
+
return order.indexOf(stageA) - order.indexOf(stageB);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
36
52
|
|
|
37
53
|
// =============================================================================
|
|
38
54
|
// Skill Comparators
|
|
@@ -158,3 +158,160 @@ export const WEIGHT_DEV_TYPE_BROAD = 1;
|
|
|
158
158
|
* AI skills get extra emphasis in development planning.
|
|
159
159
|
*/
|
|
160
160
|
export const WEIGHT_DEV_AI_BOOST = 1.5;
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Agent Profile Limits
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Maximum number of skills shown in agent profile body
|
|
168
|
+
*
|
|
169
|
+
* Limits the skill index table and before-handoff checklist to keep
|
|
170
|
+
* agent context focused. All skills are still exported as SKILL.md files
|
|
171
|
+
* and listed via --skills.
|
|
172
|
+
*
|
|
173
|
+
* @see agent.js:buildStageProfileBodyData
|
|
174
|
+
*/
|
|
175
|
+
export const LIMIT_AGENT_PROFILE_SKILLS = 5;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Maximum number of working style entries from top behaviours
|
|
179
|
+
* in agent profiles.
|
|
180
|
+
*
|
|
181
|
+
* @see agent.js:buildWorkingStyleFromBehaviours
|
|
182
|
+
*/
|
|
183
|
+
export const LIMIT_AGENT_WORKING_STYLES = 3;
|
|
184
|
+
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// Interview Time Defaults
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Default expected duration for a standard interview question (minutes)
|
|
191
|
+
*/
|
|
192
|
+
export const DEFAULT_INTERVIEW_QUESTION_MINUTES = 5;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Default expected duration for a decomposition question (minutes)
|
|
196
|
+
*/
|
|
197
|
+
export const DEFAULT_DECOMPOSITION_QUESTION_MINUTES = 15;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Default expected duration for a stakeholder simulation question (minutes)
|
|
201
|
+
*/
|
|
202
|
+
export const DEFAULT_SIMULATION_QUESTION_MINUTES = 20;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Tolerance above target interview budget before stopping selection (minutes)
|
|
206
|
+
*
|
|
207
|
+
* Interview question selection allows exceeding the time budget by this amount
|
|
208
|
+
* to avoid under-filling interviews.
|
|
209
|
+
*/
|
|
210
|
+
export const TOLERANCE_INTERVIEW_BUDGET_MINUTES = 5;
|
|
211
|
+
|
|
212
|
+
// =============================================================================
|
|
213
|
+
// Decomposition Capability Weights
|
|
214
|
+
// =============================================================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Capability priority weights for decomposition interviews
|
|
218
|
+
*
|
|
219
|
+
* Delivery and scale capabilities are typically more important for
|
|
220
|
+
* system decomposition questions.
|
|
221
|
+
*
|
|
222
|
+
* @see interview.js:calculateCapabilityPriority
|
|
223
|
+
*/
|
|
224
|
+
export const WEIGHT_CAPABILITY_DECOMP_DELIVERY = 10;
|
|
225
|
+
export const WEIGHT_CAPABILITY_DECOMP_SCALE = 8;
|
|
226
|
+
export const WEIGHT_CAPABILITY_DECOMP_RELIABILITY = 6;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Priority boost applied to focus-area questions in focused interviews
|
|
230
|
+
*
|
|
231
|
+
* @see interview.js:deriveFocusedInterview
|
|
232
|
+
*/
|
|
233
|
+
export const WEIGHT_FOCUS_BOOST = 10;
|
|
234
|
+
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// Senior Grade Threshold
|
|
237
|
+
// =============================================================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Minimum ordinalRank for a grade to be considered "senior" (Staff+)
|
|
241
|
+
*
|
|
242
|
+
* Used to determine when additional expectations scoring applies
|
|
243
|
+
* in job matching.
|
|
244
|
+
*
|
|
245
|
+
* @see derivation.js:isSeniorGrade
|
|
246
|
+
*/
|
|
247
|
+
export const THRESHOLD_SENIOR_GRADE = 5;
|
|
248
|
+
|
|
249
|
+
// =============================================================================
|
|
250
|
+
// Assessment Weights
|
|
251
|
+
// =============================================================================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Default skill weight when track does not specify assessment weights
|
|
255
|
+
*/
|
|
256
|
+
export const WEIGHT_ASSESSMENT_SKILL_DEFAULT = 0.5;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Default behaviour weight when track does not specify assessment weights
|
|
260
|
+
*/
|
|
261
|
+
export const WEIGHT_ASSESSMENT_BEHAVIOUR_DEFAULT = 0.5;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Base weight for overall score in senior role matching (non-expectations portion)
|
|
265
|
+
*/
|
|
266
|
+
export const WEIGHT_SENIOR_BASE = 0.9;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Weight for expectations score bonus in senior role matching
|
|
270
|
+
*/
|
|
271
|
+
export const WEIGHT_SENIOR_EXPECTATIONS = 0.1;
|
|
272
|
+
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// Match Result Limits
|
|
275
|
+
// =============================================================================
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Number of top-priority gaps to surface in match analysis
|
|
279
|
+
*/
|
|
280
|
+
export const LIMIT_PRIORITY_GAPS = 3;
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Score bonus for same-track candidates in next-step job matching
|
|
284
|
+
*/
|
|
285
|
+
export const WEIGHT_SAME_TRACK_BONUS = 0.1;
|
|
286
|
+
|
|
287
|
+
// =============================================================================
|
|
288
|
+
// Realistic Match Filtering
|
|
289
|
+
// =============================================================================
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Grade offset (±) from best-fit grade for realistic match filtering
|
|
293
|
+
*/
|
|
294
|
+
export const RANGE_GRADE_OFFSET = 1;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Grade offset below highest strong/good match for ready-tier filtering
|
|
298
|
+
*
|
|
299
|
+
* Strong and Good matches are shown up to this many levels below the
|
|
300
|
+
* highest matched grade. Stretch and Aspirational matches are only shown
|
|
301
|
+
* at or above the highest matched grade.
|
|
302
|
+
*/
|
|
303
|
+
export const RANGE_READY_GRADE_OFFSET = 2;
|
|
304
|
+
|
|
305
|
+
// =============================================================================
|
|
306
|
+
// Driver Coverage Thresholds
|
|
307
|
+
// =============================================================================
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Minimum skill level for a skill to count as "covered" in driver analysis
|
|
311
|
+
*/
|
|
312
|
+
export const THRESHOLD_DRIVER_SKILL_LEVEL = "working";
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Minimum behaviour maturity for a behaviour to count as "covered" in driver analysis
|
|
316
|
+
*/
|
|
317
|
+
export const THRESHOLD_DRIVER_BEHAVIOUR_MATURITY = "practicing";
|
package/src/profile.js
CHANGED
|
@@ -4,13 +4,10 @@
|
|
|
4
4
|
* Shared functions for deriving skill and behaviour profiles for both
|
|
5
5
|
* human jobs and AI agents.
|
|
6
6
|
*
|
|
7
|
-
* - prepareBaseProfile() -
|
|
8
|
-
* - prepareAgentProfile() -
|
|
7
|
+
* - prepareBaseProfile() - core derivation (skills, behaviours, responsibilities)
|
|
8
|
+
* - prepareAgentProfile() - agent-specific derivation using composed policies
|
|
9
9
|
*
|
|
10
|
-
* @see policies/
|
|
11
|
-
* @see policies/filters.js - Matrix-level filter functions
|
|
12
|
-
* @see policies/orderings.js - Comparator functions
|
|
13
|
-
* @see policies/composed.js - Composed policies
|
|
10
|
+
* @see policies/composed.js - Agent filtering and sorting policies
|
|
14
11
|
*/
|
|
15
12
|
|
|
16
13
|
import {
|
|
@@ -20,11 +17,9 @@ import {
|
|
|
20
17
|
} from "./derivation.js";
|
|
21
18
|
|
|
22
19
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
compareByMaturityDesc,
|
|
27
|
-
} from "./policies/index.js";
|
|
20
|
+
prepareAgentSkillMatrix,
|
|
21
|
+
prepareAgentBehaviourProfile,
|
|
22
|
+
} from "./policies/composed.js";
|
|
28
23
|
|
|
29
24
|
// =============================================================================
|
|
30
25
|
// Utility Functions
|
|
@@ -47,14 +42,6 @@ export function getPositiveTrackCapabilities(track) {
|
|
|
47
42
|
// Profile Derivation
|
|
48
43
|
// =============================================================================
|
|
49
44
|
|
|
50
|
-
/**
|
|
51
|
-
* @typedef {Object} ProfileOptions
|
|
52
|
-
* @property {boolean} [excludeHumanOnly=false] - Filter out human-only skills
|
|
53
|
-
* @property {boolean} [keepHighestLevelOnly=false] - Keep only skills at the highest derived level
|
|
54
|
-
* @property {boolean} [sortByLevel=false] - Sort skills by level descending
|
|
55
|
-
* @property {boolean} [sortByMaturity=false] - Sort behaviours by maturity descending
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
45
|
/**
|
|
59
46
|
* @typedef {Object} BaseProfile
|
|
60
47
|
* @property {Array} skillMatrix - Derived skill matrix
|
|
@@ -66,13 +53,14 @@ export function getPositiveTrackCapabilities(track) {
|
|
|
66
53
|
*/
|
|
67
54
|
|
|
68
55
|
/**
|
|
69
|
-
* Prepare a base profile
|
|
56
|
+
* Prepare a base profile with raw derivation
|
|
70
57
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
58
|
+
* Core derivation entry point shared by jobs and agents. Produces the
|
|
59
|
+
* raw skill matrix, behaviour profile, and responsibilities without
|
|
60
|
+
* any filtering or sorting. Consumers apply policies as needed:
|
|
73
61
|
*
|
|
74
|
-
* - Human jobs:
|
|
75
|
-
* - AI agents:
|
|
62
|
+
* - Human jobs: use raw output directly (sorted by type in derivation)
|
|
63
|
+
* - AI agents: use prepareAgentProfile() which applies composed policies
|
|
76
64
|
*
|
|
77
65
|
* @param {Object} params
|
|
78
66
|
* @param {Object} params.discipline - The discipline
|
|
@@ -81,7 +69,6 @@ export function getPositiveTrackCapabilities(track) {
|
|
|
81
69
|
* @param {Array} params.skills - All available skills
|
|
82
70
|
* @param {Array} params.behaviours - All available behaviours
|
|
83
71
|
* @param {Array} [params.capabilities] - Optional capabilities for responsibility derivation
|
|
84
|
-
* @param {ProfileOptions} [params.options={}] - Filtering and sorting options
|
|
85
72
|
* @returns {BaseProfile} The prepared profile
|
|
86
73
|
*/
|
|
87
74
|
export function prepareBaseProfile({
|
|
@@ -91,40 +78,16 @@ export function prepareBaseProfile({
|
|
|
91
78
|
skills,
|
|
92
79
|
behaviours,
|
|
93
80
|
capabilities,
|
|
94
|
-
options = {},
|
|
95
81
|
}) {
|
|
96
|
-
const {
|
|
97
|
-
excludeHumanOnly = false,
|
|
98
|
-
keepHighestLevelOnly = false,
|
|
99
|
-
sortByLevel = false,
|
|
100
|
-
sortByMaturity = false,
|
|
101
|
-
} = options;
|
|
102
|
-
|
|
103
82
|
// Core derivation
|
|
104
|
-
|
|
105
|
-
|
|
83
|
+
const skillMatrix = deriveSkillMatrix({ discipline, grade, track, skills });
|
|
84
|
+
const behaviourProfile = deriveBehaviourProfile({
|
|
106
85
|
discipline,
|
|
107
86
|
grade,
|
|
108
87
|
track,
|
|
109
88
|
behaviours,
|
|
110
89
|
});
|
|
111
90
|
|
|
112
|
-
// Apply skill filters using policy functions
|
|
113
|
-
if (excludeHumanOnly) {
|
|
114
|
-
skillMatrix = skillMatrix.filter(isAgentEligible);
|
|
115
|
-
}
|
|
116
|
-
if (keepHighestLevelOnly) {
|
|
117
|
-
skillMatrix = filterHighestLevel(skillMatrix);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Apply sorting using policy comparators
|
|
121
|
-
if (sortByLevel) {
|
|
122
|
-
skillMatrix = [...skillMatrix].sort(compareByLevelDesc);
|
|
123
|
-
}
|
|
124
|
-
if (sortByMaturity) {
|
|
125
|
-
behaviourProfile = [...behaviourProfile].sort(compareByMaturityDesc);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
91
|
// Derive responsibilities if capabilities provided
|
|
129
92
|
let derivedResponsibilities = [];
|
|
130
93
|
if (capabilities && capabilities.length > 0) {
|
|
@@ -148,14 +111,14 @@ export function prepareBaseProfile({
|
|
|
148
111
|
/**
|
|
149
112
|
* Prepare a profile optimized for agent generation
|
|
150
113
|
*
|
|
151
|
-
* Applies agent-specific
|
|
114
|
+
* Applies agent-specific policies from composed.js:
|
|
152
115
|
* - Excludes human-only skills
|
|
153
116
|
* - Keeps only skills at the highest derived level
|
|
154
117
|
* - Sorts skills by level descending
|
|
155
118
|
* - Sorts behaviours by maturity descending
|
|
156
119
|
*
|
|
157
|
-
* @param {Object} params - Same as prepareBaseProfile
|
|
158
|
-
* @returns {BaseProfile} The prepared profile
|
|
120
|
+
* @param {Object} params - Same as prepareBaseProfile
|
|
121
|
+
* @returns {BaseProfile} The prepared profile with agent policies applied
|
|
159
122
|
*/
|
|
160
123
|
export function prepareAgentProfile({
|
|
161
124
|
discipline,
|
|
@@ -165,18 +128,18 @@ export function prepareAgentProfile({
|
|
|
165
128
|
behaviours,
|
|
166
129
|
capabilities,
|
|
167
130
|
}) {
|
|
168
|
-
|
|
131
|
+
const base = prepareBaseProfile({
|
|
169
132
|
discipline,
|
|
170
133
|
track,
|
|
171
134
|
grade,
|
|
172
135
|
skills,
|
|
173
136
|
behaviours,
|
|
174
137
|
capabilities,
|
|
175
|
-
options: {
|
|
176
|
-
excludeHumanOnly: true,
|
|
177
|
-
keepHighestLevelOnly: true,
|
|
178
|
-
sortByLevel: true,
|
|
179
|
-
sortByMaturity: true,
|
|
180
|
-
},
|
|
181
138
|
});
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
skillMatrix: prepareAgentSkillMatrix(base.skillMatrix),
|
|
143
|
+
behaviourProfile: prepareAgentBehaviourProfile(base.behaviourProfile),
|
|
144
|
+
};
|
|
182
145
|
}
|