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:
- Takes the question group ID, name, weight, team member IDs, all questions, and the UserAnswerMap
- Pre-computes team statistics (means, stddev, baselines)
- Returns a
DimensionMatchCalculatorobject
| 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:
- Retrieves their answers for the question group
- 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
- Skips faking questions (
- Stores the per-user mean
Then computes aggregate team statistics:
origTeamMean: mean of all per-user meansorigTeamStdDev: sample standard deviation (n-1) of per-user meansorigTeamMeanPercent:(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 5calcScore(userCount):4 * (userCount - 1)– each additional user sharing a value adds 4 pointsgetMinScore(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 scoregetConflictScore(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).