Code Architecture

This document covers the TypeScript codebase structure, type system, calculator pattern, and module relationships.

Directory Structure

ts_algorithm/
├── run.ts                          # Simulation entry point
├── tsconfig.json                   # TypeScript config (path alias @/* -> src/*)
├── package.json                    # Dependencies: tsx, vitest, tsc-alias
│
├── src/                            # Core algorithm library
│   ├── types/
│   │   ├── enums.ts                # ScoreType, ScoreCategory, REVERSE_QUESTION_MAP, etc.
│   │   ├── models.ts               # Domain models: User, Question, QuestionGroup, UserAnswer
│   │   ├── workstyle.ts            # Calculator interfaces + UserAnswerMap type
│   │   ├── search.ts               # Search/match result types (future use)
│   │   └── index.ts                # Barrel re-export
│   │
│   ├── math/
│   │   └── statistics.ts           # mean, sampleStdDev, populationStdDev, variance
│   │
│   ├── workstyle/
│   │   ├── user-answer-utils.ts    # buildUserAnswerMap, getAnswersForUserAndGroup
│   │   ├── question-group-utils.ts # getUserQuestionGroupAverageAnswerScore (handles faking + reverse)
│   │   ├── survey-results.ts       # Survey text result loader (singleton cache)
│   │   ├── conflict-results.ts     # Conflict text result loader (singleton cache)
│   │   └── v1/
│   │       ├── dimension-stddev.ts # SUPPLEMENTARY, COMPLEMENTARY, MAX_MEAN, MIN_MEAN calculators
│   │       ├── dimension-values.ts # TOP_TEN (values overlap) calculator
│   │       └── dimension-conflict.ts # GROUP_AVERAGE (conflict v2) calculator
│   │
│   └── score-category.ts           # getScoreCategory: <33=LOW, 33-65=MEDIUM, >=66=HIGH
│
├── data/                           # Simulation dataset
│   ├── question-groups.ts          # 19 QuestionGroup definitions + weights
│   ├── questions.ts                # 158 Question definitions from production SQL
│   ├── personas.ts                 # 8 personas, 2 teams, 3 candidates
│   ├── answers.ts                  # 1,264 synthetic answers (compact grid format)
│   └── index.ts                    # Barrel re-export
│
├── tests/                          # Vitest test suite
│   └── ...
│
└── docs/                           # This documentation

Type System

Core Domain Models

The type system mirrors the original Java JPA entities.

User

interface User {
  id: number;
  firstName: string;
  lastName: string;
  username: string;
  emailAddress?: string;
  gender?: 'male' | 'female';
  isAdmin?: boolean;
  defaultOrganisationId?: number;
}

Question

interface Question {
  id: number;
  questionGroupId: number;
  orderIndex: number | null;
  type: QuestionType;           // 'LIKERT' | 'MULTIPLE_CHOICE'
  text: string;
  likertAnswerSet?: LikertAnswerSet; // 'important' | 'agree' | 'usually'
  metadata?: string;            // e.g., 'ConflictPS', 'ConflictYield'
  faking: boolean;              // true = excluded from scoring
  reverseScore: boolean;        // true = value is flipped (1<->5, 2<->4)
}

QuestionGroup

interface QuestionGroup {
  id: number;
  name: string;
  title: string;
  strapline?: string;
  scoreType: ScoreType;
  questionIds: number[];
}

UserAnswer

interface UserAnswer {
  id: number;
  userId: number;
  questionId: number;
  value: number;    // Raw answer value (1-5 for Likert)
}

The UserAnswerMap Type

The central data structure for scoring is a triple-nested Map:

type UserAnswerMap = Map<
  number,  // userId
  Map<
    number,  // questionGroupId
    Map<
      number,      // questionId
      UserAnswer   // the answer object
    >
  >
>;

This allows O(1) lookups by user, group, and question. Built by buildUserAnswerMap().

// Usage:
const answer = userAnswerMap.get(userId)?.get(questionGroupId)?.get(questionId);

The Calculator Pattern

DimensionMatchCalculator Interface

Every scoring dimension is handled by a calculator that implements this interface:

interface DimensionMatchCalculator {
  questionGroupId: number;
  questionGroupName: string;
  scoreType: ScoreType;
  weightPercentage: number;
  relativeWeightPercentage?: number;

  /** Baseline score for the team without any candidate */
  getOrigTeamScore(): number;

  /** Score when adding a specific candidate to the team */
  getCandidateTeamMatch(
    userId: number,
    candidateAnswers: UserAnswer[]
  ): WorkstyleDimensionMatchResult | null;
}

Factory Functions

Each calculator type has a factory function that:

  1. Takes the question group ID, name, weight, team member IDs, all questions, and the UserAnswerMap
  2. Pre-computes team statistics (means, stddev, baselines)
  3. Returns a DimensionMatchCalculator object
Factory ScoreType Source File
createSupplementaryCalculatorV1 SUPPLEMENTARY v1/dimension-stddev.ts
createComplementaryCalculatorV1 COMPLEMENTARY v1/dimension-stddev.ts
createMeanMaxCalculatorV1 MAX_MEAN v1/dimension-stddev.ts
createMeanMinCalculatorV1 MIN_MEAN v1/dimension-stddev.ts
createValuesCalculatorV1 TOP_TEN v1/dimension-values.ts
createConflictCalculatorV1 GROUP_AVERAGE v1/dimension-conflict.ts

Result Types

Calculators return one of two specialized result types:

// For SUPPLEMENTARY, COMPLEMENTARY, MAX_MEAN, MIN_MEAN, GROUP_AVERAGE
interface WorkstyleDimensionMatchGeneralResult {
  questionGroupId: number;
  questionGroupName: string;
  scoreType: ScoreType;
  scorePercentage: number;       // 0-100
  scoreCategory: ScoreCategory;  // 'low' | 'medium' | 'high'
  weightPercentage: number;
  candidateMean?: number;
  teamMean?: number;
  newTeamMean?: number;
  newTeamStdDev?: number;
}

// For TOP_TEN (values)
interface WorkstyleDimensionMatchValuesResult {
  questionGroupId: number;
  questionGroupName: string;
  scoreType: ScoreType;
  scorePercentage: number;
  scoreCategory: ScoreCategory;
  weightPercentage: number;
  topTeamQuestionIds: number[];
  topCandidateQuestionIds: number[];
  overlapQuestionIds: number[];
  overlapCount: number;
}

Calculator Implementations

dimension-stddev.ts – The Shared Foundation

This file contains the shared infrastructure for 4 calculator types. The key internal helper is:

computeTeamMeanStatistics

function computeTeamMeanStatistics(
  questionGroupId: number,
  teamUserIds: number[],
  questions: Question[],
  userAnswerMap: UserAnswerMap
): TeamMeanStatistics

For each team member:

  1. Retrieves their answers for the question group
  2. Calls getUserQuestionGroupAverageAnswerScore() which:
    • Skips faking questions (question.faking === true)
    • Applies reverse scoring (REVERSE_QUESTION_MAP[value]) where flagged
    • Returns the mean of remaining effective values
  3. Stores the per-user mean

Then computes aggregate team statistics:

  • origTeamMean: mean of all per-user means
  • origTeamStdDev: sample standard deviation (n-1) of per-user means
  • origTeamMeanPercent: (origTeamMean - 1) / 4 * 100

calculateTheoreticalMaxStdDev

Constructs the maximally-dispersed dataset for a given item count:

// For itemCount items on a 1-5 scale:
// floor(itemCount/2) values of 1, rest are 5
// Then compute sampleStdDev of this array

This gives the theoretical maximum standard deviation, used as the normalization ceiling.


dimension-values.ts – TOP_TEN Calculator

Handles the Values dimension (QG1). Key internal functions:

  • getTopAnswers(answers): Filter to value > 3, sort descending, take top 5
  • calcScore(userCount): 4 * (userCount - 1) – each additional user sharing a value adds 4 points
  • getMinScore(userCount, questionCount, topCount): Minimum possible score assuming maximum dispersion

The team’s “top values” are the 5 questions most frequently appearing in team members’ individual top-5 lists.


dimension-conflict.ts – GROUP_AVERAGE Calculator

Handles Conflict v2 (QG18). Key internal functions:

  • getDominantSubGroup(questions, answers, totalQuestionCount): Groups questions by metadata, computes each sub-group’s sum divided by the total question count (not per-sub-group count, matching Java), returns the one with the highest score
  • getConflictScore(dominantSubGroup): Maps to numeric score (PS=13, Comp=7, others=0)

The team baseline is the sum of all members’ conflict scores divided by (teamSize * 13).


Statistics Module

File: src/math/statistics.ts

Function Formula Notes
mean(values) sum / n Returns 0 for empty arrays
sampleVariance(values) sum((v - mean)^2) / (n - 1) Returns 0 if n <= 1
sampleStdDev(values) sqrt(sampleVariance) Used by all calculators
populationVariance(values) sum((v - mean)^2) / n Available but not used in V1
populationStdDev(values) sqrt(populationVariance) Available but not used in V1

:::warning
The algorithm uses sample standard deviation (n-1 denominator), not population (n). This matches the Apache Commons Math DescriptiveStatistics class from the original Java implementation. Using population stddev would produce different scores.
:::


Data Layer

Compact Answer Grid Format

The data/answers.ts file stores 1,264 answers in a compact grid format instead of individual objects:

const answerGrid: Record<number, Record<number, number[]>> = {
  // userId -> questionGroupId -> [raw values in questionIds order]
  1: {  // Alice
    1: [5, 5, 3, 1, 2, 5, 2, 3, 4, 2, 3, 3, 2, 3, 5, 5, 3, 4, 4, 3, 4],  // QG1 (21 questions)
    2: [4, 3, 4, 2, 3, 2, 5, 4, 4, 2],  // QG2 (10 questions)
    // ... etc
  },
  // ... other users
};

The expandAnswers() function converts this to a flat UserAnswer[] array at runtime by mapping each value to the corresponding question ID from questionGroup.questionIds.

Question Groups

The data/question-groups.ts file defines all 19 groups with their production scoreType and questionIds arrays. Weights are in a separate groupWeights map. Scoring group IDs (the 16 that are actually scored) are exported separately.


Module Dependency Map

run.ts
  ├── data/index.ts
  │     ├── data/question-groups.ts  →  src/types/enums (ScoreType)
  │     ├── data/questions.ts        →  src/types/enums + src/types/models
  │     ├── data/personas.ts         →  src/types/models (User)
  │     └── data/answers.ts          →  data/question-groups (questionIds)
  │
  ├── src/types/enums.ts             (ScoreType, ScoreCategory, REVERSE_QUESTION_MAP)
  ├── src/types/models.ts            (QuestionGroup, Question, UserAnswer)
  ├── src/types/workstyle.ts         (DimensionMatchCalculator, UserAnswerMap)
  │
  ├── src/workstyle/user-answer-utils.ts   →  types/workstyle, types/models
  │
  ├── src/workstyle/v1/dimension-stddev.ts
  │     ├── src/math/statistics.ts          (mean, sampleStdDev)
  │     ├── src/workstyle/question-group-utils.ts
  │     │     └── src/types/enums.ts        (REVERSE_QUESTION_MAP)
  │     └── src/score-category.ts
  │
  ├── src/workstyle/v1/dimension-values.ts
  │     ├── src/workstyle/question-group-utils.ts
  │     └── src/score-category.ts
  │
  └── src/workstyle/v1/dimension-conflict.ts
        └── src/score-category.ts

Path Aliases

The project uses the @/* path alias mapping to src/*:

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

So import { ScoreType } from '@/types/enums.js' resolves to src/types/enums.ts. The .js extension is required for ESM compatibility (resolved by tsx at runtime).