Files
gridpilot.gg/core/identity/domain/services/AdminTrustRatingCalculator.ts
2025-12-29 22:27:33 +01:00

164 lines
4.9 KiB
TypeScript

import { RatingEvent } from '../entities/RatingEvent';
import { AdminVoteOutcome } from '../entities/AdminVoteSession';
import { RatingDelta } from '../value-objects/RatingDelta';
/**
* Input for vote outcome calculation
*/
export interface VoteOutcomeInput {
outcome: AdminVoteOutcome;
eligibleVoterCount: number;
voteCount: number;
percentPositive: number;
}
/**
* Input for system signal calculation
*/
export interface SystemSignalInput {
actionType: 'sla_response' | 'reversal' | 'rule_clarity' | 'abuse_report';
details: Record<string, unknown>;
severity?: 'minor' | 'major';
}
/**
* Domain Service: AdminTrustRatingCalculator
*
* Pure, stateless calculator for admin trust rating.
* Implements full logic per ratings-architecture-concept.md sections 5.2 and 7.1.1
*/
export class AdminTrustRatingCalculator {
/**
* Calculate admin trust rating delta from events
*
* Logic:
* - Vote outcomes: weighted by participation and percentage
* - System signals: fixed deltas based on action type
* - All events are summed with their weights
*/
static calculate(events: RatingEvent[]): number {
return events.reduce((sum, event) => {
// Apply weight if present, otherwise use delta directly
const weightedDelta = event.weight ? event.delta.value * event.weight : event.delta.value;
return sum + weightedDelta;
}, 0);
}
/**
* Calculate delta from vote outcome
*
* Based on section 5.2.1:
* - Votes produce events with reference to voteSessionId
* - Delta is weighted by eligible voter count and participation
* - Range: -20 to +20 based on percentage
*
* @param input - Vote outcome data
* @returns Rating delta
*/
static calculateFromVote(input: VoteOutcomeInput): RatingDelta {
const { outcome, eligibleVoterCount, voteCount, percentPositive } = input;
// If no votes, no change
if (voteCount === 0) {
return RatingDelta.create(0);
}
// Calculate base delta from percentage
// Positive outcome: +1 to +20
// Negative outcome: -1 to -20
// Tie: 0
let baseDelta: number;
if (outcome.outcome === 'positive') {
baseDelta = (percentPositive / 100) * 20; // 0 to +20
} else if (outcome.outcome === 'negative') {
baseDelta = -((100 - percentPositive) / 100) * 20; // -20 to 0
} else {
baseDelta = 0; // Tie
}
// Weight by participation rate (higher participation = more trust in result)
// Minimum 50% participation for full weight
const participationRate = voteCount / eligibleVoterCount;
const participationMultiplier = Math.max(0.5, Math.min(1, participationRate));
const weightedDelta = baseDelta * participationMultiplier;
// Round to 2 decimal places
const roundedDelta = Math.round(weightedDelta * 100) / 100;
return RatingDelta.create(roundedDelta);
}
/**
* Calculate delta from system signal
*
* Based on section 5.2.2:
* - ADMIN_ACTION_SLA_BONUS: +5
* - ADMIN_ACTION_REVERSAL_PENALTY: -10 (minor) or -20 (major)
* - ADMIN_ACTION_RULE_CLARITY_BONUS: +3
* - ADMIN_ACTION_ABUSE_REPORT_PENALTY: -15 (minor) or -30 (major)
*
* @param input - System signal data
* @returns Rating delta
*/
static calculateFromSystemSignal(input: SystemSignalInput): RatingDelta {
const { actionType, severity } = input;
switch (actionType) {
case 'sla_response':
return RatingDelta.create(5);
case 'reversal':
return RatingDelta.create(severity === 'major' ? -20 : -10);
case 'rule_clarity':
return RatingDelta.create(3);
case 'abuse_report':
return RatingDelta.create(severity === 'major' ? -30 : -15);
default:
return RatingDelta.create(0);
}
}
/**
* Calculate combined delta from multiple vote outcomes
* Useful for batch processing
*/
static calculateFromMultipleVotes(inputs: VoteOutcomeInput[]): RatingDelta {
const totalDelta = inputs.reduce((sum, input) => {
const delta = this.calculateFromVote(input);
return sum + delta.value;
}, 0);
return RatingDelta.create(totalDelta);
}
/**
* Calculate combined delta from multiple system signals
*/
static calculateFromMultipleSystemSignals(inputs: SystemSignalInput[]): RatingDelta {
const totalDelta = inputs.reduce((sum, input) => {
const delta = this.calculateFromSystemSignal(input);
return sum + delta.value;
}, 0);
return RatingDelta.create(totalDelta);
}
/**
* Calculate total delta from mixed sources
* Combines votes and system signals
*/
static calculateTotalDelta(
voteInputs: VoteOutcomeInput[],
systemInputs: SystemSignalInput[]
): RatingDelta {
const voteDelta = this.calculateFromMultipleVotes(voteInputs);
const systemDelta = this.calculateFromMultipleSystemSignals(systemInputs);
return RatingDelta.create(voteDelta.value + systemDelta.value);
}
}