Files
ankn a5f8fb49ab Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
2026-06-30 20:39:52 +02:00

150 lines
4.6 KiB
TypeScript

import { Score, Team, TeamResult, Criterion, CriterionStats } from '../shared/types.js';
/**
* Calculate the aggregated result for a single team.
*
* - total = sum of all score.value where score.teamId matches
* - jurorCount = count of distinct jurorIds that have submitted ALL criteria (criteriaCount scores)
* - average = total / jurorCount (or 0 if jurorCount is 0)
* - stddev = standard deviation of per-juror totals
* - complete = jurorCount equals total number of jurors who have at least one score for this team
* AND all have all criteria
*/
export function calculateTeamResult(
teamId: string,
teamName: string,
scores: Score[],
criteriaCount: number,
criteria?: Criterion[]
): TeamResult {
const teamScores = scores.filter((s) => s.teamId === teamId);
const total = teamScores.reduce((sum, s) => sum + s.value, 0);
// Group scores by jurorId
const scoresByJuror = new Map<string, Score[]>();
for (const score of teamScores) {
const existing = scoresByJuror.get(score.jurorId) || [];
existing.push(score);
scoresByJuror.set(score.jurorId, existing);
}
// Count jurors who have submitted ALL criteria
const jurorCount = Array.from(scoresByJuror.values()).filter(
(jurorScores) => jurorScores.length >= criteriaCount
).length;
const average = jurorCount > 0 ? total / jurorCount : 0;
// Calculate stddev of per-juror totals
const jurorTotals = Array.from(scoresByJuror.values())
.filter((jurorScores) => jurorScores.length >= criteriaCount)
.map((jurorScores) => jurorScores.reduce((sum, s) => sum + s.value, 0));
let stddev = 0;
if (jurorTotals.length > 1) {
const mean = jurorTotals.reduce((a, b) => a + b, 0) / jurorTotals.length;
const variance = jurorTotals.reduce((sum, v) => sum + (v - mean) ** 2, 0) / jurorTotals.length;
stddev = Math.sqrt(variance);
}
// Complete = all jurors who have at least one score have submitted all criteria
const totalJurorsWithScores = scoresByJuror.size;
const complete = totalJurorsWithScores > 0 && jurorCount === totalJurorsWithScores;
// Per-criterion stats
let criteriaStats: CriterionStats[] | undefined;
if (criteria) {
criteriaStats = criteria.map((criterion) => {
const criterionScores = teamScores
.filter((s) => s.criterionId === criterion.id)
.map((s) => s.value);
const cMean = criterionScores.length > 0
? criterionScores.reduce((a, b) => a + b, 0) / criterionScores.length
: 0;
let cStddev = 0;
if (criterionScores.length > 1) {
const cVariance = criterionScores.reduce((sum, v) => sum + (v - cMean) ** 2, 0) / criterionScores.length;
cStddev = Math.sqrt(cVariance);
}
return {
criterionId: criterion.id,
criterionName: criterion.nameDE,
mean: cMean,
stddev: cStddev,
scores: criterionScores,
};
});
}
return {
teamId,
teamName,
total,
average,
stddev,
rank: 0, // Will be assigned by calculateRankings
jurorCount,
complete,
criteriaStats,
};
}
/**
* Sort team results by total descending and assign dense ranking.
* Ties get the same rank.
* Example: totals [250, 240, 240, 230] → ranks [1, 2, 2, 4]
*/
export function calculateRankings(teamResults: TeamResult[]): TeamResult[] {
// Sort by total descending
const sorted = [...teamResults].sort((a, b) => b.total - a.total);
// Assign dense ranking with ties
let currentRank = 1;
for (let i = 0; i < sorted.length; i++) {
if (i > 0 && sorted[i].total < sorted[i - 1].total) {
currentRank = i + 1;
}
sorted[i] = { ...sorted[i], rank: currentRank };
}
return sorted;
}
/**
* Get the completion status for a specific juror-team pair.
*
* - 0 scores → 'pending'
* - 1 to criteriaCount-1 → 'partial'
* - criteriaCount → 'complete'
*/
export function getCompletionStatus(
jurorId: string,
teamId: string,
scores: Score[],
criteriaCount: number
): 'complete' | 'partial' | 'pending' {
const count = scores.filter(
(s) => s.jurorId === jurorId && s.teamId === teamId
).length;
if (count === 0) return 'pending';
if (count >= criteriaCount) return 'complete';
return 'partial';
}
/**
* Calculate results for all teams and apply rankings.
* Convenience function combining calculateTeamResult + calculateRankings.
*/
export function calculateAllResults(
scores: Score[],
teams: Team[],
criteriaCount: number,
criteria?: Criterion[]
): TeamResult[] {
const teamResults = teams.map((team) =>
calculateTeamResult(team.id, team.name, scores, criteriaCount, criteria)
);
return calculateRankings(teamResults);
}