Integration Guide
This guide covers how to run the simulation and how to use the matching algorithm in your own application.
Prerequisites
- Node.js >= 18.x
- npm (comes with Node.js)
Install dependencies:
cd ts_algorithm
npm install
The project uses tsx to run TypeScript directly without a build step.
Running the Simulation
cd ts_algorithm
npx tsx run.ts
This runs the full matching algorithm with:
- 19 question groups (16 scored)
- 158 questions
- 8 personas (5 team members across 2 teams, 3 candidates)
- 1,264 synthetic survey answers
The output includes:
- Personas – descriptions of all team members and candidates
- Individual Answers – raw answers with faking/reverse flags per person
- Team Baselines – baseline scores for each team before adding any candidate
- Candidate Scores – per-dimension scores for each candidate against each team
- Comparison Tables – side-by-side candidate comparison per team
- Grand Summary – overall best matches across all teams
Using the Library in Your Application
The src/ directory is a standalone library you can import into your own project. Here is the step-by-step process.
Step 1: Build a UserAnswerMap
The UserAnswerMap is the central data structure. It indexes all survey answers by user, question group, and question for O(1) lookups.
import { buildUserAnswerMap } from '@/workstyle/user-answer-utils.js';
import type { UserAnswer, Question } from '@/types/models.js';
// Your raw data
const userAnswers: UserAnswer[] = [
{ id: 1, userId: 101, questionId: 22, value: 4 },
{ id: 2, userId: 101, questionId: 23, value: 3 },
// ... all answers for all users
];
const questions: Question[] = [
// Your question definitions (need id, questionGroupId, faking, reverseScore at minimum)
];
const userIds: number[] = [101, 102, 103, 201]; // all user IDs to index
const questionGroupIds: number[] = [1, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18];
const answerMap = buildUserAnswerMap(userAnswers, questions, userIds, questionGroupIds);
Step 2: Create Calculators for Each Dimension
Each question group (dimension) needs its own calculator based on the group’s scoreType:
import { ScoreType } from '@/types/enums.js';
import type { QuestionGroup } from '@/types/models.js';
import type { DimensionMatchCalculator, UserAnswerMap } from '@/types/workstyle.js';
import {
createSupplementaryCalculatorV1,
createComplementaryCalculatorV1,
createMeanMaxCalculatorV1,
createMeanMinCalculatorV1,
} from '@/workstyle/v1/dimension-stddev.js';
import { createValuesCalculatorV1 } from '@/workstyle/v1/dimension-values.js';
import { createConflictCalculatorV1 } from '@/workstyle/v1/dimension-conflict.js';
function createCalculator(
qg: QuestionGroup,
weight: number,
teamMemberIds: number[],
questions: Question[],
answerMap: UserAnswerMap
): DimensionMatchCalculator {
switch (qg.scoreType) {
case ScoreType.SUPPLEMENTARY:
return createSupplementaryCalculatorV1(qg.id, qg.name, weight, teamMemberIds, questions, answerMap);
case ScoreType.COMPLEMENTARY:
return createComplementaryCalculatorV1(qg.id, qg.name, weight, teamMemberIds, questions, answerMap);
case ScoreType.MAX_MEAN:
return createMeanMaxCalculatorV1(qg.id, qg.name, weight, teamMemberIds, questions, answerMap);
case ScoreType.MIN_MEAN:
return createMeanMinCalculatorV1(qg.id, qg.name, weight, teamMemberIds, questions, answerMap);
case ScoreType.TOP_TEN:
return createValuesCalculatorV1(qg.id, qg.name, weight, teamMemberIds, questions, answerMap);
case ScoreType.GROUP_AVERAGE:
return createConflictCalculatorV1(qg.id, qg.name, weight, teamMemberIds, questions, answerMap);
default:
throw new Error(`Unsupported scoreType: ${qg.scoreType}`);
}
}
Create one calculator per scored dimension:
const teamMemberIds = [101, 102, 103]; // the existing team
const calculators: DimensionMatchCalculator[] = scoringGroups.map(qg =>
createCalculator(qg, groupWeights[qg.id], teamMemberIds, questions, answerMap)
);
Step 3: Score Each Candidate
For each candidate, get their answers for each dimension and compute the match:
import { getAnswersForUserAndGroup } from '@/workstyle/user-answer-utils.js';
import type { WorkstyleDimensionMatchResult } from '@/types/workstyle.js';
const candidateId = 201;
const dimensionResults: WorkstyleDimensionMatchResult[] = [];
for (const calc of calculators) {
const candidateAnswers = getAnswersForUserAndGroup(answerMap, candidateId, calc.questionGroupId);
const result = calc.getCandidateTeamMatch(candidateId, candidateAnswers);
if (result) {
dimensionResults.push(result);
}
}
Each result contains:
scorePercentage– 0 to 100scoreCategory–'low','medium', or'high'weightPercentage– the dimension’s weight- Additional fields depending on calculator type (team mean, stddev, overlap counts, etc.)
Step 4: Compute the Weighted Overall Score
import { getScoreCategory } from '@/score-category.js';
let weightedSum = 0;
let totalWeight = 0;
for (const result of dimensionResults) {
weightedSum += result.scorePercentage * result.weightPercentage;
totalWeight += result.weightPercentage;
}
const overallScore = totalWeight === 0 ? 0 : weightedSum / totalWeight;
const overallCategory = getScoreCategory(Math.round(overallScore));
console.log(`Overall: ${overallScore.toFixed(1)}% ${overallCategory}`);
Complete Minimal Example
Here is a self-contained example that scores one candidate against one team:
import { buildUserAnswerMap, getAnswersForUserAndGroup } from '@/workstyle/user-answer-utils.js';
import { createMeanMaxCalculatorV1 } from '@/workstyle/v1/dimension-stddev.js';
import { getScoreCategory } from '@/score-category.js';
import { ScoreType, QuestionType } from '@/types/enums.js';
import type { Question, UserAnswer } from '@/types/models.js';
// Define a simple question group: 3 questions about teamwork
const questions: Question[] = [
{ id: 1, questionGroupId: 2, orderIndex: 1, type: QuestionType.LIKERT,
text: 'I prefer to work as part of a team', faking: false, reverseScore: false },
{ id: 2, questionGroupId: 2, orderIndex: 2, type: QuestionType.LIKERT,
text: 'Working in teams brings out the best', faking: false, reverseScore: false },
{ id: 3, questionGroupId: 2, orderIndex: 3, type: QuestionType.LIKERT,
text: 'I enjoy social interaction of teams', faking: false, reverseScore: false },
];
// Team of 2 people + 1 candidate
const answers: UserAnswer[] = [
// Team member 1 (userId: 1) -- high teamwork
{ id: 1, userId: 1, questionId: 1, value: 5 },
{ id: 2, userId: 1, questionId: 2, value: 4 },
{ id: 3, userId: 1, questionId: 3, value: 5 },
// Team member 2 (userId: 2) -- moderate teamwork
{ id: 4, userId: 2, questionId: 1, value: 3 },
{ id: 5, userId: 2, questionId: 2, value: 4 },
{ id: 6, userId: 2, questionId: 3, value: 3 },
// Candidate (userId: 3) -- high teamwork
{ id: 7, userId: 3, questionId: 1, value: 5 },
{ id: 8, userId: 3, questionId: 2, value: 5 },
{ id: 9, userId: 3, questionId: 3, value: 4 },
];
const teamIds = [1, 2];
const candidateId = 3;
const allUserIds = [1, 2, 3];
const groupIds = [2]; // teamwork group
// Build the answer map
const answerMap = buildUserAnswerMap(answers, questions, allUserIds, groupIds);
// Create a MAX_MEAN calculator for teamwork (higher = better)
const calc = createMeanMaxCalculatorV1(
2, // questionGroupId
'teamwork', // name
100, // weight
teamIds, // team member IDs
questions, // all questions
answerMap // the answer map
);
// Get team baseline
console.log(`Team baseline: ${calc.getOrigTeamScore().toFixed(1)}%`);
// Score the candidate
const candidateAnswers = getAnswersForUserAndGroup(answerMap, candidateId, 2);
const result = calc.getCandidateTeamMatch(candidateId, candidateAnswers);
if (result) {
console.log(`Candidate score: ${result.scorePercentage.toFixed(1)}% ${result.scoreCategory}`);
}
Customizing Weights
Dimension weights control how much each dimension contributes to the overall score. In the production dataset, weights are either 100 (full) or 50 (half).
To customize:
// Define your own weights
const customWeights: Record<number, number> = {
1: 100, // values
2: 100, // teamwork
4: 50, // work expectation (reduced)
5: 100, // reliance
// ... etc
};
// Use when creating calculators
const calc = createSupplementaryCalculatorV1(
qg.id,
qg.name,
customWeights[qg.id], // your custom weight
teamMemberIds,
questions,
answerMap
);
Adding a New Score Type
To add a new calculator type:
- Create a new file in
src/workstyle/v1/(e.g.,dimension-custom.ts) - Export a factory function that returns a
DimensionMatchCalculator - Add the new
ScoreTypeenum value insrc/types/enums.ts - Add the case to your
createCalculatorswitch
The factory function signature should follow this pattern:
export function createCustomCalculatorV1(
questionGroupId: number,
questionGroupName: string,
weightPercentage: number,
teamUserIds: number[],
questions: Question[],
userAnswerMap: UserAnswerMap
): DimensionMatchCalculator {
// Pre-compute team statistics here...
return {
questionGroupId,
questionGroupName,
scoreType: ScoreType.YOUR_TYPE,
weightPercentage,
getOrigTeamScore: () => { /* baseline calculation */ },
getCandidateTeamMatch(userId, candidateAnswers) {
// Candidate scoring logic
return {
questionGroupId,
questionGroupName,
scoreType: ScoreType.YOUR_TYPE,
scorePercentage,
scoreCategory: getScoreCategory(Math.round(scorePercentage)),
weightPercentage,
// ... additional fields
};
},
};
}
Adding New Question Groups
To add a new scoring dimension:
- Add the
QuestionGroupdefinition indata/question-groups.ts:
{
id: 20,
name: 'my-dimension',
title: 'My Custom Dimension',
scoreType: ScoreType.MAX_MEAN,
questionIds: [159, 160, 161, 162],
}
- Add the questions in
data/questions.ts - Set the weight in
groupWeights - Add the group ID to
scoringGroupIds - Add answers for all users in
data/answers.ts
Running Tests
npx vitest run
Or in watch mode:
npx vitest
Building for Production
npx tsc
Output goes to dist/. Path aliases are resolved by tsc-alias (configured in package.json build script).