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:

  1. Personas – descriptions of all team members and candidates
  2. Individual Answers – raw answers with faking/reverse flags per person
  3. Team Baselines – baseline scores for each team before adding any candidate
  4. Candidate Scores – per-dimension scores for each candidate against each team
  5. Comparison Tables – side-by-side candidate comparison per team
  6. 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 100
  • scoreCategory'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:

  1. Create a new file in src/workstyle/v1/ (e.g., dimension-custom.ts)
  2. Export a factory function that returns a DimensionMatchCalculator
  3. Add the new ScoreType enum value in src/types/enums.ts
  4. Add the case to your createCalculator switch

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:

  1. Add the QuestionGroup definition in data/question-groups.ts:
{
  id: 20,
  name: 'my-dimension',
  title: 'My Custom Dimension',
  scoreType: ScoreType.MAX_MEAN,
  questionIds: [159, 160, 161, 162],
}
  1. Add the questions in data/questions.ts
  2. Set the weight in groupWeights
  3. Add the group ID to scoringGroupIds
  4. 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).