@forwardimpact/model 0.7.0 → 0.7.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/model",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Derivation engine for roles, skills, and AI agent profiles",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
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
29
  import { ORDER_AGENT_STAGE } 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 = 3,
199
+ topN = LIMIT_AGENT_WORKING_STYLES,
198
200
  ) {
199
201
  const entries = [];
200
202
 
@@ -518,7 +520,7 @@ function buildStageProfileBodyData({
518
520
  ? substituteTemplateVars(rawPriority, humanDiscipline)
519
521
  : null;
520
522
 
521
- // Build skill index from derived skills with agent sections
523
+ // Build skill index from derived skills (already focused by deriveStageAgent)
522
524
  const skillIndex = derivedSkills
523
525
  .map((derived) => {
524
526
  const skill = skills.find((s) => s.id === derived.skillId);
@@ -538,7 +540,6 @@ function buildStageProfileBodyData({
538
540
  const workingStyles = buildWorkingStyleFromBehaviours(
539
541
  derivedBehaviours,
540
542
  agentBehaviours,
541
- 3,
542
543
  );
543
544
 
544
545
  // Constraints (stage + discipline + track)
@@ -602,13 +603,16 @@ export function deriveStageAgent({
602
603
  stages,
603
604
  }) {
604
605
  // Derive skills and behaviours
605
- const derivedSkills = deriveAgentSkills({
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 if applicable
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: derivedSkills,
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
@@ -7,15 +7,7 @@
7
7
  * Checklist = Stage × Skill Matrix × Skill Ready Criteria
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
- };
10
+ import { CHECKLIST_STAGE_MAP } from "./policies/orderings.js";
19
11
 
20
12
  /**
21
13
  * Derive checklist items for a specific stage
@@ -34,7 +26,7 @@ export function deriveChecklist({
34
26
  skills,
35
27
  capabilities,
36
28
  }) {
37
- const targetStage = STAGE_TO_HANDOFF[stageId];
29
+ const targetStage = CHECKLIST_STAGE_MAP[stageId];
38
30
  if (!targetStage) {
39
31
  return [];
40
32
  }
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 (level && skillLevelMeetsRequirement(level, SkillLevel.WORKING)) {
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
- BehaviourMaturity.PRACTICING,
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
- // Typically Staff+ is level 5 or higher
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,
@@ -152,6 +179,7 @@ export {
152
179
  ORDER_SKILL_TYPE,
153
180
  ORDER_STAGE,
154
181
  ORDER_AGENT_STAGE,
182
+ CHECKLIST_STAGE_MAP,
155
183
  compareByLevelDesc,
156
184
  compareByType,
157
185
  compareBySkillPriority,
@@ -160,6 +188,7 @@ export {
160
188
  // Composed policies
161
189
  filterAgentSkills,
162
190
  filterToolkitSkills,
191
+ focusAgentSkills,
163
192
  sortAgentSkills,
164
193
  sortAgentBehaviours,
165
194
  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 are core technical 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 += 10;
176
+ priority += WEIGHT_CAPABILITY_DECOMP_DELIVERY;
182
177
  } else if (capabilityId === Capability.SCALE) {
183
- priority += 8;
178
+ priority += WEIGHT_CAPABILITY_DECOMP_SCALE;
184
179
  } else if (capabilityId === Capability.RELIABILITY) {
185
- priority += 6;
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 || DEFAULT_QUESTION_MINUTES;
343
- if (skillMinutes + questionTime <= skillTimeBudget + 5) {
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 || DEFAULT_QUESTION_MINUTES;
356
- if (skillMinutes + questionTime <= skillTimeBudget + 5) {
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 || DEFAULT_QUESTION_MINUTES;
372
- if (behaviourMinutes + questionTime <= behaviourTimeBudget + 5) {
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 || DEFAULT_QUESTION_MINUTES;
385
- if (behaviourMinutes + questionTime <= behaviourTimeBudget + 5) {
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 + (q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES),
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 || DEFAULT_QUESTION_MINUTES;
481
+ nextQuestion.question.expectedDurationMinutes ||
482
+ DEFAULT_INTERVIEW_QUESTION_MINUTES;
473
483
 
474
484
  // Don't exceed budget by too much
475
- if (totalMinutes + questionTime > targetMinutes + 5) {
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 + (q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES),
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) + 10, // Boost for focus
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) + 10, // Boost for focus
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 + (q.question.expectedDurationMinutes || DEFAULT_QUESTION_MINUTES),
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 || DEFAULT_QUESTION_MINUTES;
738
- if (totalMinutes + questionTime <= targetMinutes + 5) {
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 || DEFAULT_QUESTION_MINUTES;
751
- if (totalMinutes + questionTime <= targetMinutes + 5) {
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 || DEFAULT_DECOMPOSITION_MINUTES;
858
- if (totalMinutes + questionTime <= targetMinutes + 5) {
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 || DEFAULT_DECOMPOSITION_MINUTES;
871
- if (totalMinutes + questionTime <= targetMinutes + 5) {
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
- (a, b) =>
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 || DEFAULT_SIMULATION_MINUTES;
944
- if (totalMinutes + questionTime > targetMinutes + 5) break;
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/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 = job.track?.assessmentWeights?.skillWeight ?? 0.5;
289
- const behaviourWeight = job.track?.assessmentWeights?.behaviourWeight ?? 0.5;
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 = overallScore * 0.9 + expectationsScore * 0.1;
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 (top 3 by gap size)
328
- const priorityGaps = allGaps.slice(0, 3);
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 (±1 level)
561
+ // Determine grade range (±RANGE_GRADE_OFFSET levels)
548
562
  const bestFitLevel = estimatedGrade.grade.ordinalRank;
549
563
  const gradeRange = {
550
- min: bestFitLevel - 1,
551
- max: bestFitLevel + 1,
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 2 levels below highest match
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 - 2; // Show some consolidation options
615
- const minLevelForStretch = highestMatchedLevel; // Stretch roles should be at or above current
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
- // Boost score for same track
803
- const trackBonus = track.id === currentJob.track.id ? 0.1 : 0;
816
+ const trackBonus =
817
+ track.id === currentJob.track.id ? WEIGHT_SAME_TRACK_BONUS : 0;
804
818
  candidates.push({
805
819
  job,
806
820
  analysis,
@@ -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
  // =============================================================================
@@ -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
@@ -90,6 +119,7 @@ export {
90
119
  ORDER_SKILL_TYPE,
91
120
  ORDER_STAGE,
92
121
  ORDER_AGENT_STAGE,
122
+ CHECKLIST_STAGE_MAP,
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,
@@ -34,6 +34,18 @@ export const ORDER_STAGE = ["specify", "plan", "code", "review", "deploy"];
34
34
  */
35
35
  export const ORDER_AGENT_STAGE = ["plan", "code", "review"];
36
36
 
37
+ /**
38
+ * Stage-to-handoff mapping for checklist derivation
39
+ *
40
+ * Maps stage IDs to the stage whose `.ready` criteria should be shown
41
+ * before leaving that stage.
42
+ */
43
+ export const CHECKLIST_STAGE_MAP = {
44
+ plan: "plan",
45
+ code: "code",
46
+ review: "review",
47
+ };
48
+
37
49
  // =============================================================================
38
50
  // Skill Comparators
39
51
  // =============================================================================
@@ -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() - full derivation with configurable options
8
- * - prepareAgentProfile() - convenience wrapper with agent-specific filtering
7
+ * - prepareBaseProfile() - core derivation (skills, behaviours, responsibilities)
8
+ * - prepareAgentProfile() - agent-specific derivation using composed policies
9
9
  *
10
- * @see policies/predicates.js - Entry-level predicate functions
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
- isAgentEligible,
24
- filterHighestLevel,
25
- compareByLevelDesc,
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 shared by jobs and agents
56
+ * Prepare a base profile with raw derivation
70
57
  *
71
- * This is the unified entry point for profile derivation. Both human jobs
72
- * and AI agents use this function, with different options:
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: No filtering, default sorting by type
75
- * - AI agents: Use prepareAgentProfile() for agent-specific filtering
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
- let skillMatrix = deriveSkillMatrix({ discipline, grade, track, skills });
105
- let behaviourProfile = deriveBehaviourProfile({
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 filtering and sorting:
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, without options
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
- return prepareBaseProfile({
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
  }