Model overview
How raw evidence becomes a six-pillar GM grade.
A reference for model behavior, inputs, and limitations. Scores are evaluations under a mandate—not win probabilities or guarantees of transaction acceptance.
- 01 · EvidenceRoster, rotation, contracts, picks
- 02 · MetricsRaw values → normalized scores
- 03 · CompositeSix pillars × mandate weights
- 04 · Final gradePlausibility penalty and cap
The canonical score
Each pillar combines metric results. The selected mandate supplies the six pillar weights. The canonical composite evaluates the final scenario; starting scores and deltas are explanatory fields, not a replacement for final-state grading.
base = Σ(pillar_score × mandate_weight)
final = clamp(min(base − plausibility_penalty, score_cap or 100), 0, 100)
Two aggregation boundaries
Metric weights define a pillar. Mandate weights define the composite. Changing a mandate does not simply add an improvement bonus. Timeline Alignment is an explicit exception to mandate-independent pillar evaluation because its targets depend on the team's strategic job.
Precision and validation
The grader retains full precision through aggregation. A display value is not the stored calculation. Missing or invalid rotation evidence can produce provisional pillar placeholders; those must be interpreted with validation status, not as ordinary low scores.
Reading this reference
Weights and version identifiers below come from the running application. Implementation notes and downloadable source are bundled from the same checkout when this documentation is compiled. These pages describe this deployment, not every historical grade or a promised future model.
Source coverage and licensing
This edition publishes a partial, inspectable source bundle—not a standalone algorithm release. It contains 55 aggregation and reference modules, but not the orchestration, shared valuation dependencies, frozen benchmark tables, model artifacts, runtime stores, or upstream datasets needed to reproduce a grade independently.
The manifest names the omitted dependency groups and reports current artifact versions and calibration flags. No project license terms for reuse of this bundled code are currently stated. Visibility permits inspection; it does not by itself make the code open source or grant rights to upstream basketball data.
Implementation
These notes and source files are captured from this deployment's code. Expand a module for its algorithm and branches, or download the Elixir file for local inspection.
grader.exElixir
lib/trade_machine/gm_grade_v2/grader.ex
Pure end-to-end AVC GM grader.
Pillars and plausibility retain full precision. Display rounding occurs only
when
TradeMachine.GMGradeV2.Result.GradeResult.finalize/1
is called.
SHA-256 8ed93893b8dec3125ee587d15ebf1a061e2157566f823243ad7fb9986218bd5f
defmodule TradeMachine.GMGradeV2.Grader do
@moduledoc """
Pure end-to-end AVC GM grader.
Pillars and plausibility retain full precision. Display rounding occurs only
when `TradeMachine.GMGradeV2.Result.GradeResult.finalize/1` is called.
"""
alias TradeMachine.GMGradeV2.CapState
alias TradeMachine.GMGradeV2.ChallengeSnapshot
alias TradeMachine.GMGradeV2.ComparableState
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.MoveSummary
alias TradeMachine.GMGradeV2.Narrative
alias TradeMachine.GMGradeV2.Plausibility
alias TradeMachine.GMGradeV2.Progression
alias TradeMachine.GMGradeV2.Pillars.AssetManagement
alias TradeMachine.GMGradeV2.Pillars.CapFlexibility
alias TradeMachine.GMGradeV2.Pillars.CompetitiveStrength
alias TradeMachine.GMGradeV2.Pillars.ContractHealth
alias TradeMachine.GMGradeV2.Pillars.RosterConstruction
alias TradeMachine.GMGradeV2.Pillars.TimelineAlignment
alias TradeMachine.GMGradeV2.Result.GradeResult
alias TradeMachine.GMGradeV2.Result.PillarResult
@weight_tolerance 1.0e-9
@pillar_order [
:competitive_strength,
:roster_construction,
:contract_health,
:cap_flexibility,
:asset_management,
:timeline_alignment
]
@doc """
Runs all six pillar aggregators and returns an unrounded grade.
The assembled state must include `:asset_metric_results`; callers that begin
with a persisted scenario should use `ScenarioAssembly.grade/2`.
"""
@spec grade(map(), ChallengeSnapshot.t(), struct()) :: GradeResult.t()
def grade(scenario_state, %ChallengeSnapshot{} = snapshot, mandate_profile) do
grade(scenario_state, snapshot, mandate_profile, [])
end
@doc "Grades with optional lab-only `:constants_override` values."
@spec grade(map(), ChallengeSnapshot.t(), struct(), keyword()) :: GradeResult.t()
def grade(scenario_state, %ChallengeSnapshot{} = snapshot, mandate_profile, opts)
when is_list(opts) do
scenario_state =
Map.put(
scenario_state,
:constants_override,
Keyword.get(opts, :constants_override, %{})
)
pillar_results = score_pillars(scenario_state, snapshot, mandate_profile)
aggregate(pillar_results, scenario_state, snapshot, mandate_profile)
end
@doc """
Aggregates already-computed pillar results.
This is the narrow unit-test seam for the final arithmetic; production uses
`grade/3`, which always computes the six pillars.
"""
@spec aggregate([PillarResult.t()], map(), ChallengeSnapshot.t(), struct()) :: GradeResult.t()
def aggregate(pillar_results, scenario_state, snapshot, mandate_profile)
when is_list(pillar_results) do
weights = mandate_profile.pillar_weights
base_score = composite_score(pillar_results, weights)
moves = build_moves([scenario_state])
plausibility =
scenario_state
|> Plausibility.assess(snapshot, pillar_results)
|> Plausibility.integrate_moves(moves)
final_score = apply_plausibility(base_score, plausibility)
result = %GradeResult{
model_version: ModelVersion.model_version(),
mandate_key: mandate_profile.key,
mandate_profile_version: mandate_profile.version,
pillar_results: pillar_results,
pillar_weights: weights,
base_score: base_score,
plausibility: plausibility,
final_score: final_score,
validation_status: validation_status(pillar_results, scenario_state),
snapshot_refs: snapshot_refs(snapshot, mandate_profile, scenario_state)
}
%{result | narrative: Narrative.build(result, mandate_profile, moves)}
end
@doc "Applies penalty, then the minimum cap, and clamps once at full precision."
@spec apply_plausibility(number(), %{penalty: number(), cap: number() | nil}) :: float()
def apply_plausibility(base_score, plausibility) when is_number(base_score) do
base_score
|> Kernel.-(Map.fetch!(plausibility, :penalty))
|> min(Map.get(plausibility, :cap) || 100.0)
|> max(0.0)
|> min(100.0)
|> Kernel.*(1.0)
end
@doc """
Computes the canonical mandate-weighted composite for six pillar results.
This is also the reusable arithmetic boundary for projections that retain
AVC's pillar weights while deliberately omitting the plausibility layer.
"""
@spec composite_score([PillarResult.t()], %{atom() => number()}) :: float()
def composite_score(pillar_results, weights)
when is_list(pillar_results) and is_map(weights) do
assert_weights!(weights)
assert_pillars!(pillar_results, weights)
pillar_results
|> Enum.sort_by(&pillar_order!/1)
|> Enum.reduce(0.0, fn pillar, total ->
total + pillar.score * Map.fetch!(weights, pillar.key)
end)
|> Kernel.*(1.0)
end
@doc """
Grades starting and final states against identical frozen evidence.
The returned result is the final grade. Starting scores and pillar/metric
deltas are explanatory fields and never enter the composite.
"""
@spec grade_pair(map(), map(), ChallengeSnapshot.t(), struct()) :: GradeResult.t()
def grade_pair(starting_state, final_state, snapshot, mandate_profile) do
grade_pair(starting_state, final_state, snapshot, mandate_profile, [])
end
@doc """
Grades a state pair with optional lab-only overrides.
The separately versioned Progression result is opt-in shadow data. Pass
`progression?: true` from an explicitly authorized orchestration boundary;
the canonical grade is identical either way.
"""
@spec grade_pair(map(), map(), ChallengeSnapshot.t(), struct(), keyword()) :: GradeResult.t()
def grade_pair(starting_state, final_state, snapshot, mandate_profile, opts)
when is_list(opts) do
starting = grade(starting_state, snapshot, mandate_profile, opts)
final = grade(final_state, snapshot, mandate_profile, opts)
starting_by_key = Map.new(starting.pillar_results, &{&1.key, &1})
pillar_results =
Enum.map(final.pillar_results, fn result ->
starting_result = Map.fetch!(starting_by_key, result.key)
%{
result
| starting_score: starting_result.score,
delta: result.score - starting_result.score
}
end)
starting_scores = Map.new(starting.pillar_results, &{&1.key, &1.score})
deltas =
Map.new(pillar_results, fn result ->
starting_result = Map.fetch!(starting_by_key, result.key)
{result.key,
%{
score: result.delta,
metrics: metric_deltas(starting_result, result)
}}
end)
progression =
if Keyword.get(opts, :progression?, false) do
progression(starting, final, mandate_profile, opts)
end
result = %{
final
| pillar_results: pillar_results,
starting: starting_scores,
deltas: deltas,
progression: progression
}
%{
result
| narrative: build_narrative(result, mandate_profile, [starting_state, final_state])
}
end
defp build_narrative(result, mandate_profile, states) do
Narrative.build(result, mandate_profile, build_moves(states))
end
defp build_moves(states) do
final_state = List.last(states) || %{}
final_state
|> Map.get(:modifications, %{})
|> MoveSummary.build(players_by_id(states))
end
defp players_by_id(states) do
states
|> Enum.flat_map(&Map.get(&1, :roster, []))
|> Enum.reduce(%{}, fn player, players ->
case Map.get(player, :id) || Map.get(player, "id") do
nil -> players
id -> Map.put(players, id, player)
end
end)
end
defp progression(starting, final, mandate_profile, opts) do
progression_opts = [model_override: Keyword.get(opts, :progression_override, %{})]
progression_opts =
case Keyword.get(opts, :progression_policy) do
nil -> progression_opts
policy -> Keyword.put(progression_opts, :comparison_policy, policy)
end
case ComparableState.project_pair(starting, final) do
{:ok, comparable} ->
Progression.evaluate(
mandate_profile.key,
comparable.baseline.score,
comparable.final.score,
final.final_score,
progression_opts ++
[
pillar_deltas: comparable.pillar_deltas,
metric_deltas: comparable.metric_deltas
]
)
{:error, reason} ->
Progression.unavailable(
mandate_profile.key,
final.final_score,
reason,
progression_opts
)
end
end
defp score_pillars(state, snapshot, _mandate_profile) do
unless Map.has_key?(state, :asset_metric_results) do
raise ArgumentError,
"Grader.grade/3 requires an assembled state with :asset_metric_results; " <>
"use ScenarioAssembly.grade/2 for persisted scenarios"
end
cap_state =
Map.get_lazy(state, :cap_state, fn -> CapState.build(state, season_start!(snapshot)) end)
asset_results =
Map.fetch!(state, :asset_metric_results)
[
CompetitiveStrength.score(state, snapshot),
RosterConstruction.score(state, snapshot),
ContractHealth.score(state, snapshot),
CapFlexibility.score(cap_state),
AssetManagement.score(asset_results),
TimelineAlignment.score(state, snapshot)
]
end
defp season_start!(snapshot) do
snapshot
|> ChallengeSnapshot.section(:season)
|> Map.fetch!("start_date")
|> Date.from_iso8601!()
end
defp assert_weights!(weights) do
sum = weights |> Map.values() |> Enum.sum()
if abs(sum - 1.0) > @weight_tolerance do
raise ArgumentError, "mandate pillar weights sum to #{sum}, not 1.0"
end
end
defp assert_pillars!(results, weights) do
result_keys = results |> Enum.map(& &1.key) |> MapSet.new()
weight_keys = weights |> Map.keys() |> MapSet.new()
if length(results) != map_size(weights) or result_keys != weight_keys do
raise ArgumentError,
"pillar results must contain exactly #{inspect(Map.keys(weights))}; got " <>
inspect(Enum.map(results, & &1.key))
end
end
defp validation_status(results, state) do
if Enum.any?(results, & &1.provisional?) or
Map.get(state, :provisional_player_ids, []) != [] do
:provisional
else
:ok
end
end
# `evidence_source` is `nil` for a hand-assembled state, which is the honest
# answer: nothing accounted for where its evidence came from. Only
# `ScenarioAssembly` can say, and it always does.
defp snapshot_refs(snapshot, profile, state) do
benchmark = snapshot.benchmark
%{
benchmark_id: benchmark.id,
benchmark_version: benchmark.version,
snapshot_id: snapshot.id,
model_version: ModelVersion.model_version(),
model_versions: ChallengeSnapshot.section(snapshot, :model_versions),
mandate_key: profile.key,
mandate_profile_version: profile.version,
provisional_player_ids: Map.get(state, :provisional_player_ids, []),
starting_rotation_fallback: Map.get(state, :starting_rotation_fallback),
evidence_source: Map.get(state, :evidence_source),
live_evidence: Map.get(state, :live_evidence, [])
}
end
defp metric_deltas(starting, final) do
starting_metrics = Map.new(starting.metric_results, &{&1.key, &1.score})
Map.new(final.metric_results, fn metric ->
delta =
case Map.fetch(starting_metrics, metric.key) do
{:ok, starting_score} -> metric.score - starting_score
:error -> nil
end
{metric.key, delta}
end)
end
defp pillar_order!(%PillarResult{key: key}) do
case Enum.find_index(@pillar_order, &(&1 == key)) do
nil -> raise ArgumentError, "unknown pillar key #{inspect(key)}"
index -> index
end
end
end
grade_result.exElixir
lib/trade_machine/gm_grade_v2/result/grade_result.ex
The final GM Grade result for a scenario.
Holds the six
PillarResult
s and their mandate weights, the full-precision
mandate-weighted
base_score
, the deterministic plausibility outcome
(findings + penalty + optional cap), the full-precision
final_score
, and
the deterministic plain-English
narrative
derived from those same frozen
inputs.
**Rounding happens in exactly one place:
finalize/1
.** It derives the
integer
display_score
and its
letter
via
TradeMachine.Grading.Scale
,
the shared rounding boundary for both GM Grade and Trade Grade. Every
intermediate value stays full-precision so an independently reproduced grade
agrees to the last decimal.
snapshot_refs
names every frozen input version (benchmark, challenge
snapshot, model + sub-model versions, mandate profile) so the grade can be
reproduced after real-world data changes. Its
evidence_source
says whether
that reproduction is actually possible:
:frozen
means the snapshot answered
every question the grade asked, while
:live_fallback
means at least one
input was read from the database at grade time —
live_evidence
names which,
and a grade carrying it is only reproducible while that data holds still.
SHA-256 aaa6d02e2d36dc6d830b5f985196979bc8997ca598c19fd4a886597a59402ae0
defmodule TradeMachine.GMGradeV2.Result.GradeResult do
@moduledoc """
The final GM Grade result for a scenario.
Holds the six `PillarResult`s and their mandate weights, the full-precision
mandate-weighted `base_score`, the deterministic plausibility outcome
(findings + penalty + optional cap), the full-precision `final_score`, and
the deterministic plain-English `narrative` derived from those same frozen
inputs.
**Rounding happens in exactly one place: `finalize/1`.** It derives the
integer `display_score` and its `letter` via `TradeMachine.Grading.Scale`,
the shared rounding boundary for both GM Grade and Trade Grade. Every
intermediate value stays full-precision so an independently reproduced grade
agrees to the last decimal.
`snapshot_refs` names every frozen input version (benchmark, challenge
snapshot, model + sub-model versions, mandate profile) so the grade can be
reproduced after real-world data changes. Its `evidence_source` says whether
that reproduction is actually possible: `:frozen` means the snapshot answered
every question the grade asked, while `:live_fallback` means at least one
input was read from the database at grade time — `live_evidence` names which,
and a grade carrying it is only reproducible while that data holds still.
"""
alias TradeMachine.Grading.Scale
alias TradeMachine.GMGradeV2.Result.PillarResult
alias TradeMachine.GMGradeV2.Result.ProgressionResult
@enforce_keys [:model_version, :mandate_key, :mandate_profile_version]
defstruct model_version: nil,
mandate_key: nil,
mandate_profile_version: nil,
pillar_results: [],
pillar_weights: %{},
base_score: 0.0,
plausibility: %{
findings: [],
material_findings: [],
material_concern_count: 0,
penalty: 0.0,
cap: nil
},
final_score: 0.0,
display_score: nil,
letter: nil,
validation_status: :ok,
snapshot_refs: %{},
starting: %{},
deltas: %{},
progression: nil,
narrative: nil
@type plausibility :: %{
findings: [map()],
material_findings: [map()],
material_concern_count: non_neg_integer(),
penalty: float(),
cap: float() | nil
}
@type t :: %__MODULE__{
model_version: String.t(),
mandate_key: atom(),
mandate_profile_version: String.t(),
pillar_results: [PillarResult.t()],
pillar_weights: %{atom() => float()},
base_score: float(),
plausibility: plausibility(),
final_score: float(),
display_score: non_neg_integer() | nil,
letter: String.t() | nil,
validation_status: :ok | :provisional | {:invalid, [term()]},
snapshot_refs: map(),
starting: map(),
deltas: map(),
progression: ProgressionResult.t() | nil,
narrative: TradeMachine.GMGradeV2.Narrative.t() | nil
}
@doc """
The single rounding boundary: derives `display_score` (integer) and `letter`
from the full-precision `final_score`. Call this once, at the presentation edge.
## Examples
iex> alias TradeMachine.GMGradeV2.Result.GradeResult
iex> r = %GradeResult{model_version: "avc-1.0.0", mandate_key: :ascending,
...> mandate_profile_version: "1.0.0", final_score: 76.4}
iex> r = GradeResult.finalize(r)
iex> {r.display_score, r.letter}
{76, "C"}
"""
@spec finalize(t()) :: t()
def finalize(%__MODULE__{final_score: final} = result) do
display = final |> round() |> Scale.clamp(0, 100)
%{result | display_score: display, letter: Scale.letter(display)}
end
end