Current section

Files

Jump to
erlang_adk src evaluation adk_eval_dev_view.erl
Raw

src/evaluation/adk_eval_dev_view.erl

%% @doc Pure, bounded evaluation-report boundary for developer tooling.
%%
%% Inputs are already-decoded JSON maps. This module never loads files,
%% resolves modules, starts agents, or converts input strings to atoms. It
%% classifies checked persisted results and canonical baseline comparisons,
%% then delegates rendering and comparison to the evaluation APIs.
-module(adk_eval_dev_view).
-compile({no_auto_import, [error/1]}).
-export([classify/1, render/3, compare/3]).
-define(DEFAULT_MAX_OUTPUT_BYTES, 1048576).
-define(MAX_OUTPUT_BYTES, 16777216).
-define(MAX_TOLERANCES, 1000).
-type kind() :: eval_result | baseline_comparison.
-type tagged_error() :: {error, {eval_dev_view, atom()}}.
-export_type([kind/0, tagged_error/0]).
-spec classify(map()) -> {ok, kind(), map()} | tagged_error().
classify(Value) when is_map(Value) ->
case bounded_json(Value) of
ok -> classify_json(Value);
{error, limit} -> error(input_limit_exceeded);
{error, shape} -> error(invalid_json_map)
end;
classify(_) ->
error(invalid_json_map).
-spec render(map(), binary(), map()) -> {ok, binary()} | tagged_error().
render(Value, FormatInput, Options) ->
case classify(Value) of
{error, _} = Error -> Error;
{ok, _Kind, Canonical} ->
case {render_format(FormatInput), render_options(Options)} of
{{ok, Format}, {ok, MaxBytes}} ->
case adk_eval_set:report(Canonical, Format) of
{ok, Output} when byte_size(Output) =< MaxBytes ->
{ok, Output};
{ok, _Output} -> error(output_limit_exceeded);
{error, _} -> error(render_failed)
end;
{{error, _} = Error, _} -> Error;
{_, {error, _} = Error} -> Error
end
end.
-spec compare(map(), map(), map()) -> {ok, map()} | tagged_error().
compare(Baseline0, Current0, Options) ->
case {classify(Baseline0), classify(Current0),
comparison_options(Options)} of
{{ok, eval_result, Baseline}, {ok, eval_result, Current},
{ok, CompareOptions, MaxBytes}} ->
compare_checked(Baseline, Current, CompareOptions, MaxBytes);
{{ok, baseline_comparison, _}, _, _} ->
error(baseline_must_be_eval_result);
{_, {ok, baseline_comparison, _}, _} ->
error(current_must_be_eval_result);
{{error, _} = Error, _, _} -> Error;
{_, {error, _} = Error, _} -> Error;
{_, _, {error, _} = Error} -> Error
end.
compare_checked(Baseline, Current, Options, MaxBytes) ->
case adk_eval_set:compare(Baseline, Current, Options) of
{ok, Comparison} ->
case strict_comparison(Comparison) of
ok ->
case adk_eval_set:report(Comparison, json) of
{ok, Encoded} when byte_size(Encoded) =< MaxBytes ->
{ok, Comparison};
{ok, _Encoded} -> error(output_limit_exceeded);
{error, _} -> error(comparison_failed)
end;
error -> error(comparison_failed)
end;
{error, eval_set_mismatch} -> error(eval_set_mismatch);
{error, _} -> error(comparison_failed)
end.
classify_json(Value) ->
HasResult = maps:is_key(<<"result_schema_version">>, Value),
HasReport = maps:is_key(<<"report_schema_version">>, Value)
orelse maps:is_key(<<"report_type">>, Value),
case {HasResult, HasReport} of
{true, true} -> error(ambiguous_report_kind);
{true, false} -> classify_result(Value);
{false, true} -> classify_comparison(Value);
{false, false} -> error(unknown_report_kind)
end.
classify_result(Value) ->
case adk_eval_set:decode_result(Value) of
{ok, Canonical} -> {ok, eval_result, Canonical};
{error, _} -> error(invalid_eval_result)
end.
classify_comparison(#{<<"report_type">> := <<"baseline_comparison">>}
= Value) ->
case strict_comparison(Value) of
ok -> {ok, baseline_comparison, Value};
error -> error(invalid_baseline_comparison)
end;
classify_comparison(#{<<"report_type">> := _}) ->
error(unsupported_report_type);
classify_comparison(_) ->
error(invalid_baseline_comparison).
render_format(<<"json">>) -> {ok, json};
render_format(<<"markdown">>) -> {ok, markdown};
render_format(_) -> error(invalid_format).
render_options(Options) ->
case bounded_json(Options) of
ok when is_map(Options) ->
case only_allowed_keys(Options, [<<"max_output_bytes">>]) of
true -> max_output_bytes(Options);
false -> error(invalid_options)
end;
_ -> error(invalid_options)
end.
comparison_options(Options) ->
case bounded_json(Options) of
ok when is_map(Options) ->
Allowed = [<<"max_pass_rate_drop">>,
<<"metric_tolerances">>,
<<"max_output_bytes">>],
case only_allowed_keys(Options, Allowed) of
false -> error(invalid_comparison_options);
true -> normalize_comparison_options(Options)
end;
_ -> error(invalid_comparison_options)
end.
normalize_comparison_options(Options) ->
MaxDrop = maps:get(<<"max_pass_rate_drop">>, Options, 0.0),
Tolerances = maps:get(<<"metric_tolerances">>, Options, #{}),
case {valid_fraction(MaxDrop), valid_tolerances(Tolerances),
max_output_bytes(Options)} of
{true, true, {ok, MaxBytes}} ->
{ok, #{max_pass_rate_drop => MaxDrop,
metric_tolerances => Tolerances}, MaxBytes};
_ -> error(invalid_comparison_options)
end.
max_output_bytes(Options) ->
Value = maps:get(<<"max_output_bytes">>, Options,
?DEFAULT_MAX_OUTPUT_BYTES),
case is_integer(Value) andalso Value > 0
andalso Value =< ?MAX_OUTPUT_BYTES of
true -> {ok, Value};
false -> error(invalid_output_limit)
end.
valid_tolerances(Value) when is_map(Value),
map_size(Value) =< ?MAX_TOLERANCES ->
lists:all(
fun({Id, Tolerance}) ->
is_binary(Id) andalso byte_size(Id) > 0
andalso valid_utf8(Id)
andalso valid_fraction(Tolerance)
end, maps:to_list(Value));
valid_tolerances(_) -> false.
strict_comparison(Value) when is_map(Value) ->
case adk_eval_report:validate_comparison(Value) of
ok -> ok;
{error, _} -> error
end.
only_allowed_keys(Map, Allowed) ->
lists:all(fun(Key) -> lists:member(Key, Allowed) end,
maps:keys(Map)).
bounded_json(Value) ->
case adk_eval_limits:check(Value) of
ok ->
case json_value(Value) of
true -> ok;
false -> {error, shape}
end;
{error, _} -> {error, limit}
end.
json_value(Value) when is_binary(Value) -> valid_utf8(Value);
json_value(Value) when is_integer(Value) -> true;
json_value(Value) when is_float(Value) -> Value =:= Value;
json_value(true) -> true;
json_value(false) -> true;
json_value(null) -> true;
json_value(Value) when is_list(Value) ->
lists:all(fun json_value/1, Value);
json_value(Value) when is_map(Value) ->
lists:all(
fun({Key, Nested}) ->
is_binary(Key) andalso valid_utf8(Key) andalso json_value(Nested)
end, maps:to_list(Value));
json_value(_) -> false.
valid_utf8(Binary) ->
try unicode:characters_to_binary(Binary) of
Binary -> true;
_ -> false
catch
_:_ -> false
end.
valid_fraction(Value) when is_integer(Value) ->
Value >= 0 andalso Value =< 1;
valid_fraction(Value) when is_float(Value) ->
Value =:= Value andalso Value >= 0.0 andalso Value =< 1.0;
valid_fraction(_) -> false.
error(Code) -> {error, {eval_dev_view, Code}}.