rating
This commit is contained in:
164
core/identity/domain/services/AdminTrustRatingCalculator.ts
Normal file
164
core/identity/domain/services/AdminTrustRatingCalculator.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user