From 5d4b61ad3a25d35a6720ca7df50597991c8a6638 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:46:09 +0100 Subject: [PATCH] fix(core): compute the profile statistics Aggregate_Profile never assigned Aggregate_Profile computed means only: Std_Dev_ISA, Median_ISA, Category_Std_Devs, Category_Medians and Evaluated_At were returned uninitialized, and reports printed that undefined memory as real statistics. The E2E assertion Std_Dev_ISA >= 0.0 failed nondeterministically on stack garbage (first caught on PR #63, the first CI runs after the #66 gate repair; GNAT warned at vexometer-core.adb:100 all along). Std deviations use the two-pass form -- summed squared deviations cannot go negative, unlike E[x^2] - E[x]^2. Medians sort a copy. Numeric record components now default to zero so the N = 0 path returns defined values. Verified: 1282/1282 assertions pass, three consecutive runs. Co-Authored-By: Claude Fable 5 --- vexometer/src/vexometer-core.adb | 67 +++++++++++++++++++++++++++++++- vexometer/src/vexometer-core.ads | 20 +++++----- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/vexometer/src/vexometer-core.adb b/vexometer/src/vexometer-core.adb index 65babf0..2bf442f 100644 --- a/vexometer/src/vexometer-core.adb +++ b/vexometer/src/vexometer-core.adb @@ -10,6 +10,9 @@ pragma Ada_2022; +with Ada.Numerics.Elementary_Functions; +with Ada.Containers.Generic_Array_Sort; + package body Vexometer.Core is --------------------------------------------------------------------------- @@ -81,7 +84,8 @@ package body Vexometer.Core is -- Aggregate_Profile -- -- Combines multiple response analyses into a single model profile, - -- computing mean ISA and mean category scores across all analyses. + -- computing mean, population standard deviation, and median for the + -- ISA score and each category score across all analyses. -- The profile inherits model identity from the first analysis. --------------------------------------------------------------------------- @@ -91,6 +95,27 @@ package body Vexometer.Core is is pragma Unreferenced (Config); + package EF renames Ada.Numerics.Elementary_Functions; + + type Float_Array is array (Positive range <>) of Float; + + procedure Sort is new Ada.Containers.Generic_Array_Sort + (Index_Type => Positive, + Element_Type => Float, + Array_Type => Float_Array); + + function Median_Of (Values : Float_Array) return Float is + Sorted : Float_Array := Values; + Mid : constant Positive := Sorted'First + Sorted'Length / 2; + begin + Sort (Sorted); + if Sorted'Length mod 2 = 1 then + return Sorted (Mid); + else + return (Sorted (Mid - 1) + Sorted (Mid)) / 2.0; + end if; + end Median_Of; + Profile : Model_Profile; N : constant Natural := Natural (Analyses.Length); Sums : Category_Score_Array := Null_Category_Scores; @@ -114,10 +139,50 @@ package body Vexometer.Core is Profile.Category_Means (Cat) := Sums (Cat) / Float (N); end loop; + -- Standard deviations and medians. Two-pass form: sums of squared + -- deviations cannot go negative, unlike E[x^2] - E[x]^2. + declare + ISA_Vals : Float_Array (1 .. N); + ISA_SSD : Float := 0.0; + Cat_SSD : Category_Score_Array := Null_Category_Scores; + I : Positive := 1; + begin + for A of Analyses loop + ISA_Vals (I) := A.Overall_ISA; + I := I + 1; + ISA_SSD := ISA_SSD + (A.Overall_ISA - Profile.Mean_ISA) ** 2; + for Cat in Metric_Category loop + Cat_SSD (Cat) := Cat_SSD (Cat) + + (A.Category_Scores (Cat) - Profile.Category_Means (Cat)) ** 2; + end loop; + end loop; + + Profile.Std_Dev_ISA := EF.Sqrt (ISA_SSD / Float (N)); + Profile.Median_ISA := Median_Of (ISA_Vals); + + for Cat in Metric_Category loop + Profile.Category_Std_Devs (Cat) := EF.Sqrt (Cat_SSD (Cat) / Float (N)); + end loop; + + for Cat in Metric_Category loop + declare + Vals : Float_Array (1 .. N); + J : Positive := 1; + begin + for A of Analyses loop + Vals (J) := A.Category_Scores (Cat); + J := J + 1; + end loop; + Profile.Category_Medians (Cat) := Median_Of (Vals); + end; + end loop; + end; + -- Set identity fields from the first analysis element Profile.Analysis_Count := N; Profile.Model_ID := Analyses.First_Element.Model_ID; Profile.Model_Version := Analyses.First_Element.Model_Version; + Profile.Evaluated_At := Ada.Calendar.Clock; return Profile; end Aggregate_Profile; diff --git a/vexometer/src/vexometer-core.ads b/vexometer/src/vexometer-core.ads index ec86b4a..e37d551 100644 --- a/vexometer/src/vexometer-core.ads +++ b/vexometer/src/vexometer-core.ads @@ -235,17 +235,17 @@ package Vexometer.Core is Model_ID : Unbounded_String; Model_Version : Unbounded_String; Provider : Unbounded_String; -- e.g., "Anthropic", "OpenAI" - Analysis_Count : Natural; - Mean_ISA : Float; - Std_Dev_ISA : Float; - Median_ISA : Float; - Category_Means : Category_Score_Array; - Category_Std_Devs : Category_Score_Array; - Category_Medians : Category_Score_Array; + Analysis_Count : Natural := 0; + Mean_ISA : Float := 0.0; + Std_Dev_ISA : Float := 0.0; + Median_ISA : Float := 0.0; + Category_Means : Category_Score_Array := Null_Category_Scores; + Category_Std_Devs : Category_Score_Array := Null_Category_Scores; + Category_Medians : Category_Score_Array := Null_Category_Scores; Worst_Patterns : Finding_Vector; -- Most frequently triggered - Best_Categories : Metric_Category_Set; -- Below threshold - Worst_Categories : Metric_Category_Set; -- Above threshold - Comparison_Rank : Natural; -- Rank vs other models (1=best) + Best_Categories : Metric_Category_Set := [others => False]; -- Below threshold + Worst_Categories : Metric_Category_Set := [others => False]; -- Above threshold + Comparison_Rank : Natural := 0; -- Rank vs other models (1=best) Evaluated_At : Ada.Calendar.Time; end record;