164 lines
4.9 KiB
TypeScript
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);
|
|
}
|
|
} |