commit 74b6721fc35090cd8cf77d939d0dbc0ca91a16f4
parent 79a7fbf061112916743c2754164f5fae4ec89ea8
Author: triesap <tyson@radroots.org>
Date: Wed, 8 Apr 2026 19:56:15 +0000
capabilities: type semantic and explain input contracts
Diffstat:
4 files changed, 192 insertions(+), 10 deletions(-)
diff --git a/src/hyf_core/capabilities/explain_result.mojo b/src/hyf_core/capabilities/explain_result.mojo
@@ -3,15 +3,16 @@ from std.collections import List
from mojson import Value, loads
from hyf_core.capabilities.query_analysis import (
- analyze_query,
+ analyze_query_text,
build_deterministic_meta,
query_signal_tags,
serialize_extracted_filters,
string_array_value,
)
from hyf_core.capabilities.ranking_support import (
+ ExplainResultRequest,
evaluate_candidate,
- parse_single_candidate,
+ parse_explain_result_request,
)
from hyf_core.errors import (
CapabilityResult,
@@ -97,9 +98,11 @@ def execute_explain_result(
)
try:
- var analysis = analyze_query(input, context, "explain_result")
- var candidate = parse_single_candidate(input, "explain_result")
- var evaluation = evaluate_candidate(candidate, analysis, context)
+ var request: ExplainResultRequest = parse_explain_result_request(input)
+ var analysis = analyze_query_text(request.query_text, context)
+ var evaluation = evaluate_candidate(
+ request.candidate, analysis, context
+ )
var signal_tags = query_signal_tags(analysis)
for reason in evaluation.reasons:
diff --git a/src/hyf_core/capabilities/ranking_support.mojo b/src/hyf_core/capabilities/ranking_support.mojo
@@ -34,11 +34,36 @@ struct CandidateEvaluation(Copyable, Movable):
var scope_match: Bool
+@fieldwise_init
+struct SemanticRankRequest(Copyable, Movable):
+ var query_text: String
+ var candidates: List[SemanticCandidate]
+
+
+@fieldwise_init
+struct ExplainResultRequest(Copyable, Movable):
+ var query_text: String
+ var candidate: SemanticCandidate
+
+
def _require_object(value: Value, context: String) raises:
if not value.is_object():
raise Error(context + " must be a JSON object")
+def _require_allowed_keys(
+ value: Value, allowed_keys: List[String], context: String
+) raises:
+ for key in value.object_keys():
+ var allowed = False
+ for allowed_key in allowed_keys:
+ if key == allowed_key:
+ allowed = True
+ break
+ if not allowed:
+ raise Error(context + " contains unexpected field '" + key + "'")
+
+
def _copy_candidate(candidate: SemanticCandidate) -> SemanticCandidate:
return SemanticCandidate(
id=String(candidate.id),
@@ -73,6 +98,15 @@ def _copy_evaluation(evaluation: CandidateEvaluation) -> CandidateEvaluation:
def _parse_candidate(json: Value, context: String) raises -> SemanticCandidate:
_require_object(json, context)
+ var allowed_keys = List[String]()
+ allowed_keys.append("id")
+ allowed_keys.append("title")
+ allowed_keys.append("farm")
+ allowed_keys.append("delivery")
+ allowed_keys.append("distance_km")
+ allowed_keys.append("freshness_minutes")
+ _require_allowed_keys(json, allowed_keys, context)
+
var id = get_string(json, "id")
if collapse_whitespace(id) == "":
raise Error(context + " field 'id' must not be empty")
@@ -109,6 +143,67 @@ def _parse_candidate(json: Value, context: String) raises -> SemanticCandidate:
)
+def _parse_query_text(input: Value, capability_name: String) raises -> String:
+ var field_count = 0
+ if has_key(input, "text"):
+ field_count += 1
+ if has_key(input, "query"):
+ field_count += 1
+
+ if field_count == 0:
+ raise Error(
+ capability_name + " input requires exactly one of 'text' or 'query'"
+ )
+ if field_count > 1:
+ raise Error(
+ capability_name
+ + " input must provide exactly one of 'text' or 'query'"
+ )
+
+ var field_name = "text" if has_key(input, "text") else "query"
+ var text_value = input[field_name]
+ if not text_value.is_string():
+ raise Error(
+ capability_name + " input field '" + field_name + "' must be a string"
+ )
+
+ var collapsed = collapse_whitespace(text_value.string_value())
+ if collapsed == "":
+ raise Error(capability_name + " input text must not be empty")
+ return collapsed^
+
+
+def parse_semantic_rank_request(input: Value) raises -> SemanticRankRequest:
+ _require_object(input, "semantic_rank input")
+
+ var allowed_keys = List[String]()
+ allowed_keys.append("text")
+ allowed_keys.append("query")
+ allowed_keys.append("candidates")
+ _require_allowed_keys(input, allowed_keys, "semantic_rank input")
+
+ return SemanticRankRequest(
+ query_text=_parse_query_text(input, "semantic_rank"),
+ candidates=parse_candidate_array(input, "semantic_rank"),
+ )
+
+
+def parse_explain_result_request(input: Value) raises -> ExplainResultRequest:
+ _require_object(input, "explain_result input")
+
+ var allowed_keys = List[String]()
+ allowed_keys.append("text")
+ allowed_keys.append("query")
+ allowed_keys.append("candidate")
+ allowed_keys.append("result")
+ _require_allowed_keys(input, allowed_keys, "explain_result input")
+
+ return ExplainResultRequest(
+ query_text=_parse_query_text(input, "explain_result"),
+ candidate=parse_single_candidate(input, "explain_result"),
+ )
+
+
def parse_candidate_array(
input: Value, capability_name: String
) raises -> List[SemanticCandidate]:
diff --git a/src/hyf_core/capabilities/semantic_rank.mojo b/src/hyf_core/capabilities/semantic_rank.mojo
@@ -3,7 +3,7 @@ from std.collections import List
from mojson import Value, loads
from hyf_core.capabilities.query_analysis import (
- analyze_query,
+ analyze_query_text,
build_deterministic_meta,
query_signal_tags,
serialize_extracted_filters,
@@ -11,7 +11,8 @@ from hyf_core.capabilities.query_analysis import (
)
from hyf_core.capabilities.ranking_support import (
CandidateEvaluation,
- parse_candidate_array,
+ SemanticRankRequest,
+ parse_semantic_rank_request,
rank_candidates,
)
from hyf_core.errors import (
@@ -82,9 +83,9 @@ def execute_semantic_rank(
)
try:
- var analysis = analyze_query(input, context, "semantic_rank")
- var candidates = parse_candidate_array(input, "semantic_rank")
- var ranked = rank_candidates(candidates, analysis, context)
+ var request: SemanticRankRequest = parse_semantic_rank_request(input)
+ var analysis = analyze_query_text(request.query_text, context)
+ var ranked = rank_candidates(request.candidates, analysis, context)
var signal_tags = query_signal_tags(analysis)
signal_tags.append("candidate_set_evaluated")
diff --git a/tests/test_hyf.mojo b/tests/test_hyf.mojo
@@ -301,6 +301,38 @@ def test_semantic_rank_returns_ranked_ids_and_reasons() raises:
)
+def test_semantic_rank_rejects_unknown_top_level_field() raises:
+ var result = _dispatch(
+ '{"version":1,"request_id":"rank-bad-top-1","capability":"semantic_rank","input":{"query":"eggs near me","candidates":[{"id":"lst_7ak2","title":"Pasture eggs","farm":"La Huerta del Sur","delivery":"pickup","distance_km":3.2,"freshness_minutes":2}],"tone":"brief"}}'
+ )
+
+ assert_equal(Int(result["version"].int_value()), 1)
+ assert_equal(result["ok"].bool_value(), False)
+ assert_equal(result["request_id"].string_value(), "rank-bad-top-1")
+ assert_equal(result["error"]["code"].string_value(), "invalid_request")
+ assert_true(
+ result["error"]["message"].string_value().find("unexpected field")
+ >= 0
+ )
+
+
+def test_semantic_rank_rejects_unknown_candidate_field() raises:
+ var result = _dispatch(
+ '{"version":1,"request_id":"rank-bad-candidate-1","capability":"semantic_rank","input":{"query":"eggs near me","candidates":[{"id":"lst_7ak2","title":"Pasture eggs","farm":"La Huerta del Sur","delivery":"pickup","distance_km":3.2,"freshness_minutes":2,"rating":5}]}}'
+ )
+
+ assert_equal(Int(result["version"].int_value()), 1)
+ assert_equal(result["ok"].bool_value(), False)
+ assert_equal(
+ result["request_id"].string_value(), "rank-bad-candidate-1"
+ )
+ assert_equal(result["error"]["code"].string_value(), "invalid_request")
+ assert_true(
+ result["error"]["message"].string_value().find("unexpected field")
+ >= 0
+ )
+
+
def test_explain_result_returns_deterministic_summary_and_provenance() raises:
var result = _dispatch(
'{"version":1,"request_id":"explain-1","capability":"explain_result","context":{"consumer":"radroots-cli","return_provenance":true},"input":{"query":"eggs near me with weekend pickup","candidate":{"id":"lst_7ak2","title":"Pasture eggs","farm":"La Huerta del Sur","delivery":"pickup","distance_km":3.2,"freshness_minutes":2}}}'
@@ -325,6 +357,57 @@ def test_explain_result_returns_deterministic_summary_and_provenance() raises:
)
+def test_explain_result_accepts_result_alias() raises:
+ var result = _dispatch(
+ '{"version":1,"request_id":"explain-result-1","capability":"explain_result","input":{"query":"eggs near me with weekend pickup","result":{"id":"lst_7ak2","title":"Pasture eggs","farm":"La Huerta del Sur","delivery":"pickup","distance_km":3.2,"freshness_minutes":2}}}'
+ )
+
+ assert_equal(Int(result["version"].int_value()), 1)
+ assert_equal(result["ok"].bool_value(), True)
+ assert_equal(
+ result["output"]["result_id"].string_value(),
+ "lst_7ak2",
+ )
+ assert_equal(
+ result["output"]["explanation_kind"].string_value(),
+ "deterministic",
+ )
+
+
+def test_explain_result_rejects_unknown_top_level_field() raises:
+ var result = _dispatch(
+ '{"version":1,"request_id":"explain-bad-top-1","capability":"explain_result","input":{"query":"eggs near me","candidate":{"id":"lst_7ak2","title":"Pasture eggs","farm":"La Huerta del Sur","delivery":"pickup","distance_km":3.2,"freshness_minutes":2},"tone":"brief"}}'
+ )
+
+ assert_equal(Int(result["version"].int_value()), 1)
+ assert_equal(result["ok"].bool_value(), False)
+ assert_equal(
+ result["request_id"].string_value(), "explain-bad-top-1"
+ )
+ assert_equal(result["error"]["code"].string_value(), "invalid_request")
+ assert_true(
+ result["error"]["message"].string_value().find("unexpected field")
+ >= 0
+ )
+
+
+def test_explain_result_rejects_unknown_candidate_field() raises:
+ var result = _dispatch(
+ '{"version":1,"request_id":"explain-bad-candidate-1","capability":"explain_result","input":{"query":"eggs near me","candidate":{"id":"lst_7ak2","title":"Pasture eggs","farm":"La Huerta del Sur","delivery":"pickup","distance_km":3.2,"freshness_minutes":2,"rating":5}}}'
+ )
+
+ assert_equal(Int(result["version"].int_value()), 1)
+ assert_equal(result["ok"].bool_value(), False)
+ assert_equal(
+ result["request_id"].string_value(), "explain-bad-candidate-1"
+ )
+ assert_equal(result["error"]["code"].string_value(), "invalid_request")
+ assert_true(
+ result["error"]["message"].string_value().find("unexpected field")
+ >= 0
+ )
+
+
def test_semantic_rank_invalid_input_returns_invalid_request() raises:
var result = _dispatch(
'{"version":1,"request_id":"rank-bad-1","trace_id":"trace-rank-bad-1","capability":"semantic_rank","input":{"query":"eggs near me with weekend pickup","candidates":[]}}'