hyf

Context-aware query service for Radroots
git clone https://radroots.dev/git/hyf.git
Log | Files | Refs | README | LICENSE

commit d03bebf7ce20a4390d84c47819b202633b9ab799
parent 275827002624bdb1a269bb95c0d9c0431f135198
Author: triesap <tyson@radroots.org>
Date:   Mon, 21 Sep 2026 13:05:19 +0000

provider: parse ordered score answers

Diffstat:
Msrc/hyf_provider/jev_answers.mojo | 46++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_jev.mojo | 28++++++++++++++++++++++++++++
2 files changed, 74 insertions(+), 0 deletions(-)

diff --git a/src/hyf_provider/jev_answers.mojo b/src/hyf_provider/jev_answers.mojo @@ -58,3 +58,49 @@ def parse_choice_answer( if total < 0.999 or total > 1.001: raise Error("provider_answer_bad_distribution_sum") return typed_choice(question_id, selected, confidence) + + +def parse_score_answer( + question_id: String, value: Value, rubric: List[String] +) raises -> TypedAnswer: + if not value.is_object() or not _has_key(value, "type"): + raise Error("provider_answer_invalid") + if value["type"].string_value() != "score": + raise Error("provider_answer_wrong_type") + if not _has_key(value, "score"): + raise Error("provider_answer_invalid") + var score_value = value["score"] + var raw_score = 0.0 + if score_value.is_int(): + raw_score = Float64(score_value.int_value()) + elif score_value.is_float(): + raw_score = score_value.float_value() + else: + raise Error("provider_answer_invalid") + var score = Int(raw_score) + if Float64(score) != raw_score: + raise Error("provider_answer_invalid") + if score < 0 or score >= len(rubric): + raise Error("provider_answer_score_out_of_range") + if not _has_key(value, "confidence") or not value["confidence"].is_float(): + raise Error("provider_answer_invalid") + var confidence = value["confidence"].float_value() + if confidence < 0.0 or confidence > 1.0: + raise Error("provider_answer_confidence_out_of_range") + if _has_key(value, "legend"): + var legend = value["legend"] + if not legend.is_object() or len(legend.object_keys()) != len(rubric): + raise Error("provider_answer_wrong_legend") + if _has_key(value, "probabilities"): + var probabilities = value["probabilities"] + if not probabilities.is_object() or len(probabilities.object_keys()) != len(rubric): + raise Error("provider_answer_missing_level") + var total = 0.0 + for level in range(len(rubric)): + var key = String(level) + if not _has_key(probabilities, key): + raise Error("provider_answer_missing_level") + total += probabilities[key].float_value() + if total < 0.999 or total > 1.001: + raise Error("provider_answer_bad_distribution_sum") + return typed_score(question_id, score, len(rubric), confidence) diff --git a/tests/test_jev.mojo b/tests/test_jev.mojo @@ -80,3 +80,31 @@ def test_parse_choice_and_noul_answers() raises: _ = parse_choice_answer( "q", loads('{"type":"noul","noul":0.5}'), choices ) + + +from hyf_provider.jev_answers import parse_score_answer + + +def test_parse_score_answer_validates_rubric_and_levels() raises: + var rubric = List[String]() + rubric.append("unsuitable") + rubric.append("limited") + rubric.append("suitable") + var answer = parse_score_answer( + "culinary_fit", + loads('{"type":"score","score":2,"legend":{"0":"unsuitable","1":"limited","2":"suitable"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"confidence":1.0}'), + rubric, + ) + assert_equal(answer.score, 2) + with assert_raises(): + _ = parse_score_answer( + "q", + loads('{"type":"score","score":5,"legend":{"0":"a","1":"b","2":"c"},"confidence":1.0}'), + rubric, + ) + with assert_raises(): + _ = parse_score_answer( + "q", + loads('{"type":"score","score":1,"legend":{"0":"a"},"confidence":1.0}'), + rubric, + )