defmodule TradeMachine.GMGradeV2.Editions do @moduledoc "Reviewed public projections of frozen lab outputs. Never exposes internal diagnostics." import Ecto.Query alias TradeMachine.Accounts.Policy alias TradeMachine.GMGradeV2.{Edition, Lab, MandateProfile} alias TradeMachine.Repo @pillar_keys ~w(competitive_strength roster_construction contract_health cap_flexibility asset_management timeline_alignment) def list do Repo.all( from e in Edition, order_by: [desc: e.inserted_at, desc: e.id], select: struct(e, [:id, :title, :inserted_at]), limit: 100 ) end def get(nil), do: Repo.one(from e in Edition, order_by: [desc: e.inserted_at, desc: e.id], limit: 1) def get(id) do case Ecto.UUID.cast(id) do {:ok, id} -> Repo.get(Edition, id) _ -> nil end end def preview(run_id, actor) do with true <- Policy.can?(actor, :run_grading_lab), run when not is_nil(run) <- Lab.get_run(run_id) do project(run) else _ -> {:error, "This preview is available to grading editors only."} end end def publish(run_id, attrs, actor) do with {:ok, document} <- preview(run_id, actor) do %Edition{lab_run_id: run_id, published_by_id: actor.id, document: document} |> Edition.insert_changeset(attrs) |> Repo.insert() end end def project(run) do mandates = Enum.map(MandateProfile.keys(), &Atom.to_string/1) teams = Enum.group_by(Map.values(run.fixtures), &get_in(&1, ["metadata", "team_id"])) issues = publication_issues(run) if issues == [] do run = Repo.preload(run, :benchmark) rows = Enum.map(teams, fn {id, fixtures} -> meta = hd(fixtures)["metadata"] %{ "id" => id, "name" => meta["team_name"], "abbreviation" => meta["team_abbreviation"], "evidence" => public_evidence(meta["public_evidence"]), "grades" => Map.new(fixtures, fn fixture -> {fixture["mandate_key"], public_grade(run.results[fixture["id"]])} end) } end) {:ok, %{ "schema_version" => 1, "model_version" => run.model_version, "stats_season" => run.benchmark.season, "benchmark_version" => run.benchmark.version, "benchmark_id" => run.benchmark_id, "built_at" => DateTime.to_iso8601(run.inserted_at), "mandates" => mandates, "teams" => Enum.sort_by(rows, & &1["name"]) }} else {:error, publication_error(run, issues)} end end @doc "Exact publication blockers, grouped by team and mandate in the editor report." def publication_issues(run) do mandates = Enum.map(MandateProfile.keys(), &Atom.to_string/1) teams = Enum.group_by(Map.values(run.fixtures), &get_in(&1, ["metadata", "team_id"])) population = failed_checks([ {map_size(run.fixtures) == 180, "Expected 180 fixtures; found #{map_size(run.fixtures)}."}, {map_size(run.results) == 180, "Expected 180 grade results; found #{map_size(run.results)}."}, {map_size(teams) == 30, "Expected 30 teams; found #{map_size(teams)}."}, {not Map.has_key?(teams, nil), "Some fixtures have no team identity."}, {Enum.all?(teams, fn {_id, rows} -> Enum.sort(Enum.map(rows, & &1["mandate_key"])) == Enum.sort(mandates) end), "Every team must have exactly one result for each of the six mandates. Use a complete team backtest, not progression scenarios."} ]) fixture_issues = Enum.flat_map(run.fixtures, fn {id, fixture} -> meta = fixture["metadata"] || %{} team = meta["team_name"] || meta["team_id"] || id Enum.map(fixture_reasons(fixture, run.results[id]), fn reason -> %{team: team, mandate: fixture["mandate_key"], reason: reason} end) end) Enum.map(population, &%{team: "League", mandate: nil, reason: &1}) ++ fixture_issues end defp fixture_reasons(fixture, result) do meta = fixture["metadata"] || %{} evidence = meta["public_evidence"] || %{} failed_checks([ {meta["archetype"] == "team_state" and is_nil(meta["fixture_kind"]), "This is not an unmodified real-team fixture. Generate a team backtest."}, {is_binary(meta["team_name"]), "Team name is missing."}, {is_binary(evidence["captured_at"]), "Roster capture date is missing. This run must be rebuilt with edition evidence enabled."}, {is_list(evidence["roster"]) and evidence["roster"] != [], "Captured payroll roster is missing or empty. Check the team's active contracts before rebuilding."} ]) ++ result_reasons(result, fixture["mandate_key"]) end defp result_reasons(result, mandate) when is_map(result) do pillars = result["pillar_results"] || [] source = get_in(result, ["snapshot_refs", "evidence_source"]) live = get_in(result, ["snapshot_refs", "live_evidence"]) failed_checks([ {result["mandate_key"] == mandate, "Result mandate does not match its fixture."}, {valid_final_score?(result["final_score"]), "Final score is missing or outside 0–100."}, {is_number(result["base_score"]), "Weighted base score is missing."}, {source == "frozen", "Evidence is not fully frozen (source: #{inspect(source)}; live inputs: #{inspect(live)}). Inspect those snapshot inputs before rebuilding."}, {Enum.sort(Enum.map(pillars, & &1["key"])) == Enum.sort(@pillar_keys), "The result does not contain exactly the six expected pillars."} ]) ++ Enum.flat_map(pillars, &pillar_reasons(&1, result["pillar_weights"] || %{})) end defp result_reasons(_, _), do: ["Grade result is missing."] defp pillar_reasons(pillar, weights) do label = String.replace(pillar["key"] || "Unknown pillar", "_", " ") failed_checks([ {is_number(pillar["score"]), "#{label}: numeric score is missing."}, {pillar["provisional?"] == false, "#{label}: provisional or missing final status. #{pillar["explanation"] || "Inspect this pillar's inputs in the scorecard."}"}, {is_number(weights[pillar["key"]]), "#{label}: mandate weight is missing."} ]) end defp valid_final_score?(score), do: is_number(score) and score >= 0 and score <= 100 defp failed_checks(checks), do: for({false, reason} <- checks, do: reason) defp publication_error(run, issues) do groups = issues |> Enum.group_by(&{&1.team, &1.reason}) |> Enum.sort_by(&elem(&1, 0)) lines = Enum.map(groups, fn {{team, reason}, rows} -> mandates = rows |> Enum.map(& &1.mandate) |> Enum.reject(&is_nil/1) |> Enum.uniq() |> Enum.sort() scope = case length(mandates) do 0 -> "" 6 -> " (all six mandates)" _ -> " (#{Enum.join(mandates, ", ")})" end "• #{team}#{scope}: #{reason}" end) "Publication blocked for run #{run.id} (#{run.model_version}).\n" <> Enum.join(lines, "\n") end def rows(document, mandate, sort \\ "total") do rows = Enum.map(document["teams"], fn team -> Map.put(team, "grade", team["grades"][mandate]) end) ranks = Map.new(rows, fn team -> rank = 1 + Enum.count(rows, &(&1["grade"]["final_score"] > team["grade"]["final_score"])) {team["id"], rank} end) rows |> Enum.map(&Map.put(&1, "rank", ranks[&1["id"]])) |> Enum.sort_by(fn row -> {-sort_score(row["grade"], sort), row["name"]} end) end def pillar(grade, key), do: Enum.find(grade["pillars"], &(&1["key"] == key)) defp sort_score(grade, "total"), do: grade["final_score"] defp sort_score(grade, key), do: (pillar(grade, key) || %{})["score"] || grade["final_score"] defp public_evidence(evidence) do evidence |> Map.take(~w(captured_at roster_season roster)) |> Map.update!("roster", fn roster -> Enum.map(roster, &Map.take(&1, ~w(id name salary position contract_type))) end) end defp public_grade(result) do %{ "final_score" => result["final_score"], "display_score" => result["display_score"], "base_score" => result["base_score"], "weights" => result["pillar_weights"], "evidence_source" => get_in(result, ["snapshot_refs", "evidence_source"]), "pillars" => Enum.map(result["pillar_results"], fn p -> %{ "key" => p["key"], "score" => p["score"], "explanation" => p["explanation"], "metrics" => Enum.map(p["metric_results"], fn m -> %{ "key" => m["key"], "score" => m["score"], "explanation" => m["explanation"], "weight" => p["metric_weights"][m["key"]], "facts" => scalar_facts(m["inputs"] || %{}), "fallback_count" => length(m["fallbacks"] || []), "assets" => public_assets(m) } end) } end) } end defp scalar_facts(inputs) do inputs |> Enum.filter(fn {_k, v} -> is_number(v) or is_binary(v) end) |> Map.new() end defp public_assets(%{"key" => "draft_capital_value"} = m) do Enum.map(get_in(m, ["supporting", "assets"]) || [], fn asset -> Map.take( asset, ~w(year round kind original_team_id inventory_dav dav baseline_dav provisional?) ) end) end defp public_assets(_), do: [] end