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.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user