@forwardimpact/model 0.5.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/README.md CHANGED
@@ -29,8 +29,8 @@ import {
29
29
  deriveSkillMatrix,
30
30
  deriveBehaviourProfile,
31
31
  } from "@forwardimpact/model/derivation";
32
- import { prepareAgentProfile } from "@forwardimpact/model/agent";
33
- import { selectInterviewQuestions } from "@forwardimpact/model/interview";
32
+ import { prepareAgentProfile } from "@forwardimpact/model/profile";
33
+ import { deriveInterviewQuestions } from "@forwardimpact/model/interview";
34
34
  import { analyzeProgression } from "@forwardimpact/model/progression";
35
35
  ```
36
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/model",
3
- "version": "0.5.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": {
@@ -10,25 +10,27 @@
10
10
  },
11
11
  "homepage": "https://www.forwardimpact.team/model",
12
12
  "type": "module",
13
- "main": "lib/index.js",
13
+ "main": "src/index.js",
14
14
  "files": [
15
- "lib/"
15
+ "src/"
16
16
  ],
17
17
  "exports": {
18
- ".": "./lib/index.js",
19
- "./derivation": "./lib/derivation.js",
20
- "./modifiers": "./lib/modifiers.js",
21
- "./agent": "./lib/agent.js",
22
- "./interview": "./lib/interview.js",
23
- "./job": "./lib/job.js",
24
- "./job-cache": "./lib/job-cache.js",
25
- "./checklist": "./lib/checklist.js",
26
- "./matching": "./lib/matching.js",
27
- "./profile": "./lib/profile.js",
28
- "./progression": "./lib/progression.js"
18
+ ".": "./src/index.js",
19
+ "./derivation": "./src/derivation.js",
20
+ "./modifiers": "./src/modifiers.js",
21
+ "./agent": "./src/agent.js",
22
+ "./interview": "./src/interview.js",
23
+ "./job": "./src/job.js",
24
+ "./job-cache": "./src/job-cache.js",
25
+ "./checklist": "./src/checklist.js",
26
+ "./matching": "./src/matching.js",
27
+ "./profile": "./src/profile.js",
28
+ "./progression": "./src/progression.js",
29
+ "./policies": "./src/policies/index.js",
30
+ "./toolkit": "./src/toolkit.js"
29
31
  },
30
32
  "dependencies": {
31
- "@forwardimpact/schema": "^0.4.0"
33
+ "@forwardimpact/schema": "^0.6.0"
32
34
  },
33
35
  "engines": {
34
36
  "node": ">=18.0.0"
@@ -21,10 +21,13 @@
21
21
  import { deriveSkillMatrix, deriveBehaviourProfile } from "./derivation.js";
22
22
  import { deriveChecklist } from "./checklist.js";
23
23
  import {
24
- filterSkillsForAgent,
25
- sortByLevelDescending,
26
- sortByMaturityDescending,
27
- } from "./profile.js";
24
+ filterAgentSkills,
25
+ sortAgentSkills,
26
+ sortAgentBehaviours,
27
+ focusAgentSkills,
28
+ } from "./policies/composed.js";
29
+ import { ORDER_AGENT_STAGE } from "./policies/orderings.js";
30
+ import { LIMIT_AGENT_WORKING_STYLES } from "./policies/thresholds.js";
28
31
  import { SkillLevel } from "@forwardimpact/schema/levels";
29
32
 
30
33
  /**
@@ -123,9 +126,9 @@ export function deriveAgentSkills({ discipline, track, grade, skills }) {
123
126
  skills,
124
127
  });
125
128
 
126
- // Apply agent-specific filtering and sorting
127
- const filtered = filterSkillsForAgent(skillMatrix);
128
- return sortByLevelDescending(filtered);
129
+ // Apply agent-specific filtering and sorting using policies
130
+ const filtered = filterAgentSkills(skillMatrix);
131
+ return sortAgentSkills(filtered);
129
132
  }
130
133
 
131
134
  /**
@@ -151,7 +154,7 @@ export function deriveAgentBehaviours({
151
154
  behaviours,
152
155
  });
153
156
 
154
- return sortByMaturityDescending(profile);
157
+ return sortAgentBehaviours(profile);
155
158
  }
156
159
 
157
160
  /**
@@ -193,7 +196,7 @@ function findAgentBehaviour(agentBehaviours, id) {
193
196
  function buildWorkingStyleFromBehaviours(
194
197
  derivedBehaviours,
195
198
  agentBehaviours,
196
- topN = 3,
199
+ topN = LIMIT_AGENT_WORKING_STYLES,
197
200
  ) {
198
201
  const entries = [];
199
202
 
@@ -229,7 +232,7 @@ function buildWorkingStyleFromBehaviours(
229
232
  * @param {Array} stages - All stage entities
230
233
  * @returns {Object} Skill with frontmatter, title, stages array, reference, dirname
231
234
  */
232
- export function generateSkillMd(skillData, stages) {
235
+ export function generateSkillMarkdown(skillData, stages) {
233
236
  const { agent, name } = skillData;
234
237
 
235
238
  if (!agent) {
@@ -272,10 +275,11 @@ export function generateSkillMd(skillData, stages) {
272
275
  },
273
276
  );
274
277
 
275
- // Sort stages in order: plan, code, review
276
- const stageOrder = ["plan", "code", "review"];
278
+ // Sort stages using canonical ordering from policies
277
279
  stagesArray.sort(
278
- (a, b) => stageOrder.indexOf(a.stageId) - stageOrder.indexOf(b.stageId),
280
+ (a, b) =>
281
+ ORDER_AGENT_STAGE.indexOf(a.stageId) -
282
+ ORDER_AGENT_STAGE.indexOf(b.stageId),
279
283
  );
280
284
 
281
285
  return {
@@ -516,7 +520,7 @@ function buildStageProfileBodyData({
516
520
  ? substituteTemplateVars(rawPriority, humanDiscipline)
517
521
  : null;
518
522
 
519
- // Build skill index from derived skills with agent sections
523
+ // Build skill index from derived skills (already focused by deriveStageAgent)
520
524
  const skillIndex = derivedSkills
521
525
  .map((derived) => {
522
526
  const skill = skills.find((s) => s.id === derived.skillId);
@@ -536,7 +540,6 @@ function buildStageProfileBodyData({
536
540
  const workingStyles = buildWorkingStyleFromBehaviours(
537
541
  derivedBehaviours,
538
542
  agentBehaviours,
539
- 3,
540
543
  );
541
544
 
542
545
  // Constraints (stage + discipline + track)
@@ -600,13 +603,16 @@ export function deriveStageAgent({
600
603
  stages,
601
604
  }) {
602
605
  // Derive skills and behaviours
603
- const derivedSkills = deriveAgentSkills({
606
+ const allSkills = deriveAgentSkills({
604
607
  discipline,
605
608
  track,
606
609
  grade,
607
610
  skills,
608
611
  });
609
612
 
613
+ // Focus skills for profile body (limited set to reduce context bloat)
614
+ const focusedSkills = focusAgentSkills(allSkills);
615
+
610
616
  const derivedBehaviours = deriveAgentBehaviours({
611
617
  discipline,
612
618
  track,
@@ -622,13 +628,13 @@ export function deriveStageAgent({
622
628
  stages,
623
629
  });
624
630
 
625
- // Derive checklist if applicable
631
+ // Derive checklist from focused skills only
626
632
  const checklistStage = getChecklistStage(stage.id);
627
633
  let checklist = [];
628
634
  if (checklistStage && capabilities) {
629
635
  checklist = deriveChecklist({
630
636
  stageId: checklistStage,
631
- skillMatrix: derivedSkills,
637
+ skillMatrix: focusedSkills,
632
638
  skills,
633
639
  capabilities,
634
640
  });
@@ -638,7 +644,7 @@ export function deriveStageAgent({
638
644
  stage,
639
645
  discipline,
640
646
  track,
641
- derivedSkills,
647
+ derivedSkills: focusedSkills,
642
648
  derivedBehaviours,
643
649
  handoffs,
644
650
  constraints: [
@@ -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
  }
@@ -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,
@@ -18,6 +16,12 @@ import {
18
16
  } from "@forwardimpact/schema/levels";
19
17
 
20
18
  import { resolveSkillModifier } from "./modifiers.js";
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";
21
25
 
22
26
  /**
23
27
  * Build a Map of skillId → skillType for a discipline
@@ -226,14 +230,10 @@ export function deriveSkillMatrix({ discipline, grade, track = null, skills }) {
226
230
  }
227
231
 
228
232
  // Sort by type (primary first, then secondary, then broad, then track) and then by name
229
- const typeOrder = {
230
- [SkillType.PRIMARY]: 0,
231
- [SkillType.SECONDARY]: 1,
232
- [SkillType.BROAD]: 2,
233
- [SkillType.TRACK]: 3,
234
- };
233
+ // Use ORDER_SKILL_TYPE from policies for canonical ordering
235
234
  matrix.sort((a, b) => {
236
- const typeCompare = typeOrder[a.type] - typeOrder[b.type];
235
+ const typeCompare =
236
+ ORDER_SKILL_TYPE.indexOf(a.type) - ORDER_SKILL_TYPE.indexOf(b.type);
237
237
  if (typeCompare !== 0) return typeCompare;
238
238
  return a.skillName.localeCompare(b.skillName);
239
239
  });
@@ -485,7 +485,7 @@ export function deriveResponsibilities({
485
485
  capability: capabilityId,
486
486
  capabilityName: capability.name,
487
487
  emojiIcon: capability.emojiIcon || "💡",
488
- displayOrder: capability.displayOrder ?? 999,
488
+ ordinalRank: capability.ordinalRank ?? 999,
489
489
  responsibility: responsibilityText,
490
490
  level,
491
491
  levelIndex: SKILL_LEVEL_ORDER.indexOf(level),
@@ -498,7 +498,7 @@ export function deriveResponsibilities({
498
498
  if (b.levelIndex !== a.levelIndex) {
499
499
  return b.levelIndex - a.levelIndex;
500
500
  }
501
- return a.displayOrder - b.displayOrder;
501
+ return a.ordinalRank - b.ordinalRank;
502
502
  });
503
503
 
504
504
  // Remove levelIndex from output (internal use only)
@@ -598,7 +598,10 @@ export function calculateDriverCoverage({ job, drivers }) {
598
598
 
599
599
  for (const skillId of contributingSkills) {
600
600
  const level = jobSkillLevels.get(skillId);
601
- if (level && skillLevelMeetsRequirement(level, SkillLevel.WORKING)) {
601
+ if (
602
+ level &&
603
+ skillLevelMeetsRequirement(level, THRESHOLD_DRIVER_SKILL_LEVEL)
604
+ ) {
602
605
  coveredSkills.push(skillId);
603
606
  } else {
604
607
  missingSkills.push(skillId);
@@ -614,7 +617,7 @@ export function calculateDriverCoverage({ job, drivers }) {
614
617
  const coveredBehaviours = [];
615
618
  const missingBehaviours = [];
616
619
  const practicingIndex = getBehaviourMaturityIndex(
617
- BehaviourMaturity.PRACTICING,
620
+ THRESHOLD_DRIVER_BEHAVIOUR_MATURITY,
618
621
  );
619
622
 
620
623
  for (const behaviourId of contributingBehaviours) {
@@ -681,8 +684,7 @@ export function getGradeLevel(grade) {
681
684
  * @returns {boolean} True if the grade is senior level
682
685
  */
683
686
  export function isSeniorGrade(grade) {
684
- // Typically Staff+ is level 5 or higher
685
- return grade.ordinalRank >= 5;
687
+ return grade.ordinalRank >= THRESHOLD_SENIOR_GRADE;
686
688
  }
687
689
 
688
690
  /**
@@ -33,11 +33,11 @@ export {
33
33
 
34
34
  // Job caching
35
35
  export {
36
- makeJobKey,
36
+ buildJobKey,
37
37
  getOrCreateJob,
38
- clearJobCache,
39
- invalidateJob,
40
- getCacheSize,
38
+ clearCache,
39
+ invalidateCachedJob,
40
+ getCachedJobCount,
41
41
  } from "./job-cache.js";
42
42
 
43
43
  // Modifiers
@@ -45,17 +45,17 @@ export {
45
45
  isCapability,
46
46
  getSkillsByCapability,
47
47
  buildCapabilityToSkillsMap,
48
- expandSkillModifiers,
48
+ expandModifiersToSkills,
49
49
  extractCapabilityModifiers,
50
- extractIndividualModifiers,
50
+ extractSkillModifiers,
51
51
  resolveSkillModifier,
52
52
  } from "./modifiers.js";
53
53
 
54
54
  // Matching
55
55
  export {
56
56
  MatchTier,
57
- MATCH_TIER_CONFIG,
58
- classifyMatchTier,
57
+ CONFIG_MATCH_TIER,
58
+ classifyMatch,
59
59
  GAP_SCORES,
60
60
  calculateGapScore,
61
61
  calculateJobMatch,
@@ -87,16 +87,18 @@ export {
87
87
  deriveShortInterview,
88
88
  deriveBehaviourQuestions,
89
89
  deriveFocusedInterview,
90
+ deriveMissionFitInterview,
91
+ deriveDecompositionInterview,
92
+ deriveStakeholderInterview,
90
93
  } from "./interview.js";
91
94
 
92
95
  // Agent generation
93
96
  export {
94
97
  deriveReferenceGrade,
95
98
  getDisciplineAbbreviation,
96
- toKebabCase,
97
99
  deriveAgentSkills,
98
100
  deriveAgentBehaviours,
99
- generateSkillMd,
101
+ generateSkillMarkdown,
100
102
  validateAgentProfile,
101
103
  validateAgentSkill,
102
104
  deriveHandoffs,
@@ -111,15 +113,84 @@ export { deriveChecklist, formatChecklistMarkdown } from "./checklist.js";
111
113
  // Toolkit
112
114
  export { deriveToolkit } from "./toolkit.js";
113
115
 
114
- // Profile filtering (for agents)
116
+ // Profile derivation
115
117
  export {
116
118
  getPositiveTrackCapabilities,
117
- filterHumanOnlySkills,
118
- filterByHighestLevel,
119
- filterSkillsForAgent,
120
- sortByLevelDescending,
121
- sortByMaturityDescending,
122
119
  prepareBaseProfile,
123
- AGENT_PROFILE_OPTIONS,
124
120
  prepareAgentProfile,
125
121
  } from "./profile.js";
122
+
123
+ // Policies - re-export key items for convenience
124
+ export {
125
+ // Thresholds
126
+ THRESHOLD_MATCH_STRONG,
127
+ THRESHOLD_MATCH_GOOD,
128
+ THRESHOLD_MATCH_STRETCH,
129
+ THRESHOLD_MATCH_ASPIRATIONAL,
130
+ SCORE_GAP,
131
+ WEIGHT_SKILL_TYPE,
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,
160
+ // Predicates
161
+ isHumanOnly,
162
+ isAgentEligible,
163
+ isPrimary,
164
+ isSecondary,
165
+ isBroad,
166
+ isTrack,
167
+ isCore,
168
+ isSupporting,
169
+ hasMinLevel,
170
+ allOf,
171
+ anyOf,
172
+ not,
173
+ // Filters
174
+ filterHighestLevel,
175
+ filterAboveAwareness,
176
+ applyFilters,
177
+ composeFilters,
178
+ // Orderings
179
+ ORDER_SKILL_TYPE,
180
+ ORDER_STAGE,
181
+ ORDER_AGENT_STAGE,
182
+ CHECKLIST_STAGE_MAP,
183
+ compareByLevelDesc,
184
+ compareByType,
185
+ compareBySkillPriority,
186
+ compareByMaturityDesc,
187
+ compareByBehaviourPriority,
188
+ // Composed policies
189
+ filterAgentSkills,
190
+ filterToolkitSkills,
191
+ focusAgentSkills,
192
+ sortAgentSkills,
193
+ sortAgentBehaviours,
194
+ prepareAgentSkillMatrix,
195
+ prepareAgentBehaviourProfile,
196
+ } from "./policies/index.js";