defmodule TradeMachine.GMGradeV2.Pillars.RosterConstruction do @moduledoc """ Aggregates AVC's four Roster Construction metrics over the final roster. Roster Completeness, Position Coverage, Skill Coverage, and Functional Depth retain full precision through the fixed 15/25/35/25 weighted sum. """ alias TradeMachine.GMGradeV2.Benchmark alias TradeMachine.GMGradeV2.ChallengeSnapshot alias TradeMachine.GMGradeV2.Pillars.RosterConstruction.FunctionalDepth alias TradeMachine.GMGradeV2.Pillars.RosterConstruction.Inputs alias TradeMachine.GMGradeV2.Pillars.RosterConstruction.PositionCoverage alias TradeMachine.GMGradeV2.Pillars.RosterConstruction.RosterCompleteness alias TradeMachine.GMGradeV2.Pillars.RosterConstruction.SkillCoverage alias TradeMachine.GMGradeV2.Result.MetricResult alias TradeMachine.GMGradeV2.Result.PillarResult @metric_weights %{ roster_completeness: 0.15, position_coverage: 0.25, skill_coverage: 0.35, functional_depth: 0.25 } @metric_modules [ RosterCompleteness, PositionCoverage, SkillCoverage, FunctionalDepth ] if abs(Enum.sum(Map.values(@metric_weights)) - 1.0) > 1.0e-9 do raise "Roster Construction metric weights must sum to 1.0" end @doc "Returns the full-precision Roster Construction pillar result." @spec score(map(), ChallengeSnapshot.t()) :: PillarResult.t() def score( %{roster: roster} = scenario_state, %ChallengeSnapshot{benchmark: %Benchmark{} = benchmark} = snapshot ) when is_list(roster) do scoring_state = scenario_state |> Map.put(:roster, Inputs.hydrate_roster(roster, snapshot)) |> Map.put( :model_override, Map.merge( Inputs.model_override(snapshot), Map.get(scenario_state, :constants_override, %{}) ) ) metric_results = Enum.map(@metric_modules, & &1.score(scoring_state, snapshot)) {strongest, weakest} = extrema(metric_results) %PillarResult{ key: :roster_construction, version: benchmark.version, score: weighted_score(metric_results), metric_results: metric_results, metric_weights: @metric_weights, explanation: explanation(strongest, weakest), strongest: strongest.key, weakest: weakest.key } end defp weighted_score(metric_results) do Enum.reduce(metric_results, 0.0, fn result, total -> total + result.score * Map.fetch!(@metric_weights, result.key) end) end # Enum.max_by/min_by retain the first item on a tie, so published metric order # provides a stable tie-break without a second priority table. defp extrema(metric_results) do { Enum.max_by(metric_results, & &1.score), Enum.min_by(metric_results, & &1.score) } end defp explanation( %MetricResult{key: strongest_key, score: strongest_score}, %MetricResult{key: weakest_key, score: weakest_score} ) do "Roster Construction combines four full-roster metrics; " <> "#{label(strongest_key)} is strongest at #{strongest_score}, while " <> "#{label(weakest_key)} is weakest at #{weakest_score}." end defp label(:roster_completeness), do: "Roster Completeness" defp label(:position_coverage), do: "Position Coverage" defp label(:skill_coverage), do: "Skill Coverage" defp label(:functional_depth), do: "Functional Depth" end