hyf

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

commit 00e388437301229880c97bca03540c32ba732191
parent 8d0e7300bb65431cbe9306a07d1853f17317822a
Author: triesap <tyson@radroots.org>
Date:   Thu, 24 Sep 2026 15:58:32 +0000

C004: parse corrected operation context and guard pre-activation dispatch

Diffstat:
Asrc/hyf_core/operation_context.mojo | 318+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/hyf_stdio/envelope.mojo | 28++++++++++++++++++++++++++--
Msrc/hyf_stdio/server.mojo | 7+++++++
Mtests/test_hyf.mojo | 390+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 741 insertions(+), 2 deletions(-)

diff --git a/src/hyf_core/operation_context.mojo b/src/hyf_core/operation_context.mojo @@ -0,0 +1,318 @@ +# ADR-0025 D45 CB01-CB05 — corrected hyf_ops_v2 capability context. +# +# This module owns only the strict, capability-aware operation context for the +# three corrected operations selected by ``context.versions.schema == +# "hyf_ops_v2"``. It does not implement the operation pipelines (C008/C009), +# typed semantic consistency (C023) or activation (C042-C046). A recognized v2 +# request is parsed here and then refused by the pre-activation guard in +# hyf_stdio.server; it must never reach the legacy shortcut handlers. + +from std.collections import List, Optional + +from json import Value +from json.deserialize import get_bool, get_int, get_string + + +comptime OPERATION_CONTRACT_SCHEMA_V2: String = "hyf_ops_v2" +comptime OPERATION_CONTEXT_DEFAULT_DEADLINE_MS: Int = 2500 + + +def corrected_operation_selector() -> String: + return OPERATION_CONTRACT_SCHEMA_V2 + + +def is_corrected_operation(capability: String) -> Bool: + return ( + capability == "farm_update.interpret" + or capability == "buyer_request.interpret" + or capability == "buyer_request.match" + ) + + +def corrected_operation_requires_farm_id(capability: String) -> Bool: + return capability == "farm_update.interpret" + + +def corrected_operation_activation_enabled() -> Bool: + """Activation boundary owned by C042-C046; C004 binds but never activates. + """ + return False + + +def _has_key(value: Value, key: String) -> Bool: + for candidate in value.object_keys(): + if candidate == key: + return True + return False + + +def _require_object(value: Value, context: String) raises: + if not value.is_object(): + raise Error(context + " must be a JSON object") + + +def _require_no_duplicate_keys(value: Value, context: String) raises: + var keys = value.object_keys() + for left in range(len(keys)): + for right in range(left + 1, len(keys)): + if keys[left] == keys[right]: + raise Error( + context + " contains duplicate field '" + keys[left] + "'" + ) + + +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 _require_non_empty(value: String, context: String) raises: + if value == "": + raise Error(context + " must not be empty") + + +def _has_nonblank(value: String) -> Bool: + return String(value).strip().byte_length() > 0 + + +def _require_nonblank(value: String, context: String) raises: + if not _has_nonblank(value): + raise Error(context + " must not be blank") + + +def _optional_string( + value: Value, key: String, context: String +) raises -> Optional[String]: + if not _has_key(value, key): + return None + var raw = get_string(value, key) + _require_nonblank(raw, context) + return String(raw) + + +def _required_string( + value: Value, key: String, context: String +) raises -> String: + if not _has_key(value, key): + raise Error(context + " is required") + var raw = get_string(value, key) + _require_nonblank(raw, context) + return String(raw) + + +def _object_or_empty(value: Value, key: String) -> Value: + if _has_key(value, key): + return value[key].clone() + return Value(None) + + +@fieldwise_init +struct OperationVersions(Copyable, Movable): + var schema: String + var taxonomy: String + var normalization: String + var review_policy: String + var ranking_policy: String + var question_bundle: String + var model: String + + +def _parse_operation_versions( + json: Value, context: String +) raises -> OperationVersions: + _require_object(json, context) + _require_no_duplicate_keys(json, context) + + var allowed_keys = List[String]() + for key in [ + "schema", + "taxonomy", + "normalization", + "review_policy", + "ranking_policy", + "question_bundle", + "model", + ]: + allowed_keys.append(key) + _require_allowed_keys(json, allowed_keys, context) + + var schema = _required_string(json, "schema", context + " schema") + if schema != OPERATION_CONTRACT_SCHEMA_V2: + raise Error( + context + + " selects unsupported operation contract schema '" + + schema + + "'" + ) + + return OperationVersions( + schema=schema, + taxonomy=_required_string(json, "taxonomy", context + " taxonomy"), + normalization=_required_string( + json, "normalization", context + " normalization" + ), + review_policy=_required_string( + json, "review_policy", context + " review_policy" + ), + ranking_policy=_required_string( + json, "ranking_policy", context + " ranking_policy" + ), + question_bundle=_required_string( + json, "question_bundle", context + " question_bundle" + ), + model=_required_string(json, "model", context + " model"), + ) + + +@fieldwise_init +struct OperationContext(Copyable, Movable): + var consumer: String + var execution_mode_preference: String + var deadline_ms: Int + var evaluation_time: String + var timezone: Optional[String] + var locale: Optional[String] + var versions: OperationVersions + var return_provenance: Bool + var actor_id: String + var farm_id: Optional[String] + + +def default_operation_context() -> OperationContext: + return OperationContext( + consumer="unknown", + execution_mode_preference="deterministic", + deadline_ms=OPERATION_CONTEXT_DEFAULT_DEADLINE_MS, + evaluation_time="", + timezone=None, + locale=None, + versions=OperationVersions( + schema="", + taxonomy="", + normalization="", + review_policy="", + ranking_policy="", + question_bundle="", + model="", + ), + return_provenance=False, + actor_id="", + farm_id=None, + ) + + +def operation_context_selects_v2(context_json: Value) raises -> Bool: + """True only for a well-typed ``context.versions.schema == hyf_ops_v2``. + + Missing, null, wrong-type and unknown selectors are not recognized here; + they fall through to the unchanged legacy parser, which rejects + ``versions`` for unrelated capabilities and returns invalid_request. + """ + if not context_json.is_object(): + return False + if not _has_key(context_json, "versions"): + return False + var versions = context_json["versions"] + if not versions.is_object(): + return False + if not _has_key(versions, "schema"): + return False + var schema = versions["schema"] + if not schema.is_string(): + return False + return String(schema.string_value()) == OPERATION_CONTRACT_SCHEMA_V2 + + +def parse_operation_context( + json: Value, capability: String +) raises -> OperationContext: + if not is_corrected_operation(capability): + raise Error( + "operation context is only defined for corrected operations, not '" + + capability + + "'" + ) + + _require_object(json, "operation context") + _require_no_duplicate_keys(json, "operation context") + + var requires_farm = corrected_operation_requires_farm_id(capability) + var allowed_keys = List[String]() + for key in [ + "consumer", + "execution_mode_preference", + "deadline_ms", + "evaluation_time", + "timezone", + "locale", + "versions", + "return_provenance", + "actor_id", + ]: + allowed_keys.append(key) + if requires_farm: + allowed_keys.append("farm_id") + _require_allowed_keys(json, allowed_keys, "operation context") + + var context = default_operation_context() + + var consumer = _optional_string( + json, "consumer", "operation context consumer" + ) + if consumer: + context.consumer = consumer.value() + + if _has_key(json, "execution_mode_preference"): + var preference = get_string(json, "execution_mode_preference") + if preference != "deterministic" and preference != "assisted": + raise Error( + "operation context execution_mode_preference must be" + " 'deterministic' or 'assisted'" + ) + context.execution_mode_preference = String(preference) + + if _has_key(json, "deadline_ms"): + context.deadline_ms = get_int(json, "deadline_ms") + if context.deadline_ms <= 0: + raise Error( + "operation context deadline_ms must be greater than zero" + ) + + context.evaluation_time = _required_string( + json, "evaluation_time", "operation context evaluation_time" + ) + + context.timezone = _optional_string( + json, "timezone", "operation context timezone" + ) + context.locale = _optional_string( + json, "locale", "operation context locale" + ) + + if not _has_key(json, "versions"): + raise Error("operation context versions is required") + context.versions = _parse_operation_versions( + json["versions"].clone(), "operation context versions" + ) + + if _has_key(json, "return_provenance"): + context.return_provenance = get_bool(json, "return_provenance") + + context.actor_id = _required_string( + json, "actor_id", "operation context actor_id" + ) + + if requires_farm: + context.farm_id = _required_string( + json, "farm_id", "operation context farm_id" + ) + + return context^ diff --git a/src/hyf_stdio/envelope.mojo b/src/hyf_stdio/envelope.mojo @@ -4,7 +4,17 @@ from json import Value, loads from json.deserialize import Deserializable, get_string from hyf_core.metadata import hyf_protocol_version -from hyf_core.request_context import RequestContext, parse_request_context +from hyf_core.operation_context import ( + OperationContext, + is_corrected_operation, + operation_context_selects_v2, + parse_operation_context, +) +from hyf_core.request_context import ( + RequestContext, + default_request_context, + parse_request_context, +) from hyf_stdio.errors import WireError @@ -88,6 +98,10 @@ struct WireRequest(Copyable, Deserializable, Movable): var capability: String var context: RequestContext var input: Value + # ADR-0025 D45 CB01: present only for a recognized hyf_ops_v2 request to one + # of the three corrected operations. Legacy requests leave this None and + # keep the unchanged legacy context. + var operation_context: Optional[OperationContext] @staticmethod def from_json(json: Value) raises -> Self: @@ -107,7 +121,16 @@ struct WireRequest(Copyable, Deserializable, Movable): if _has_key(json, "context"): context_json = json["context"].clone() - var context = parse_request_context(context_json) + var context = default_request_context() + var operation_context: Optional[OperationContext] = None + if is_corrected_operation(capability) and operation_context_selects_v2( + context_json + ): + operation_context = parse_operation_context( + context_json.clone(), capability + ) + else: + context = parse_request_context(context_json) var input = _require_input_value(json) return Self( @@ -117,6 +140,7 @@ struct WireRequest(Copyable, Deserializable, Movable): capability=capability, context=context^, input=input^, + operation_context=operation_context^, ) diff --git a/src/hyf_stdio/server.mojo b/src/hyf_stdio/server.mojo @@ -196,6 +196,13 @@ def _route_business_capability( request_id: String, runtime_context: RuntimeStartupContext, ) raises -> String: + # ADR-0025 D45 CB01 pre-activation guard: a recognized hyf_ops_v2 request has + # already passed the strict capability-context parse, but activation belongs + # to C042-C046, so it must not reach the legacy shortcut handlers even when + # their legacy enable flag is set. C008/C009 retain this guard. + if request.operation_context: + return encode_error(_unavailable_response(request)) + if is_gated_operation(request.capability): if not operation_enabled(runtime_context.config, request.capability): return encode_error(_disabled_response(request)) diff --git a/tests/test_hyf.mojo b/tests/test_hyf.mojo @@ -1498,3 +1498,393 @@ def test_stdio_envelope_boundary_rejects_null_and_numeric_fields() raises: assert_true(messages[2].find("'version' is required") >= 0) assert_true(messages[3].find("not a string") >= 0) assert_true(messages[4].find("not a string") >= 0) + + +# ADR-0025 D45 C004: strict hyf_ops_v2 capability context and pre-activation guard. +from hyf_core.operation_context import ( + corrected_operation_activation_enabled, + is_corrected_operation, + operation_context_selects_v2, +) +from hyf_runtime.config import operation_enabled as _operation_enabled + + +def _v2_versions_json() -> String: + return ( + '"versions":{"schema":"hyf_ops_v2",' + '"taxonomy":"hyf_ops_v2.taxonomy.v1",' + '"normalization":"hyf_ops_v2.normalization.v1",' + '"review_policy":"hyf_ops_v2.review_policy.v1",' + '"ranking_policy":"hyf_ops_v2.ranking_policy.v1",' + '"question_bundle":"hyf_ops_v2.question_bundle.v1",' + '"model":"jev-1.13.0"}' + ) + + +def _v2_farm_references_json() -> String: + return ( + '"references":{"taxonomy":{"version":"hyf_ops_v2.taxonomy.v1",' + '"provenance":"host_supplied","products":[' + '{"catalogue_id":"tomato.roma","terms":["Roma tomatoes"]}]},' + '"normalization":{"version":"hyf_ops_v2.normalization.v1",' + '"provenance":"host_supplied","units":[{"unit":"lb","dimension":"mass"}],' + '"conversions":[],"packs":[]}}' + ) + + +def _v2_farm_source_json() -> String: + return ( + '"source":{"source_id":"s1","revision":"r1",' + '"text":"80 lb of Roma tomatoes",' + '"source_time":"2026-09-24T08:30:00-07:00",' + '"timezone":"America/Vancouver","actor_id":"farm-1","farm_id":"farm-1"}' + ) + + +def _v2_farm_request() -> String: + return ( + '{"version":1,"request_id":"v2-farm-1",' + '"capability":"farm_update.interpret",' + '"context":{"consumer":"radroots-cli",' + '"execution_mode_preference":"deterministic","deadline_ms":2500,' + '"evaluation_time":"2026-09-24T09:00:00-07:00",' + '"timezone":"America/Vancouver","locale":"en-CA",' + + _v2_versions_json() + + ',"return_provenance":true,"actor_id":"farm-1","farm_id":"farm-1"},' + '"input":{' + + _v2_farm_source_json() + + "," + + _v2_farm_references_json() + + "}}" + ) + + +def _v2_farm_request_with_context(context_body: String) -> String: + return ( + '{"version":1,"request_id":"v2-farm-variant",' + '"capability":"farm_update.interpret","context":{' + + context_body + + '},"input":{' + + _v2_farm_source_json() + + "," + + _v2_farm_references_json() + + "}}" + ) + + +def _decode_error_message(line: String) -> String: + try: + _ = decode_request(line) + except e: + return String(e) + return "" + + +def test_c004_corrected_operation_registry_and_activation_boundary() raises: + assert_true(is_corrected_operation("farm_update.interpret")) + assert_true(is_corrected_operation("buyer_request.interpret")) + assert_true(is_corrected_operation("buyer_request.match")) + assert_true(not is_corrected_operation("query_rewrite")) + # C042-C046 own activation; C004 binds the guard but never activates. + assert_true(not corrected_operation_activation_enabled()) + + +def test_c004_decode_request_parses_operation_v2_context() raises: + var request = decode_request(_v2_farm_request()) + assert_equal(request.capability, "farm_update.interpret") + assert_true(request.operation_context) + var context = request.operation_context.value().copy() + assert_equal(context.consumer, "radroots-cli") + assert_equal(context.execution_mode_preference, "deterministic") + assert_equal(context.deadline_ms, 2500) + assert_equal(context.evaluation_time, "2026-09-24T09:00:00-07:00") + assert_equal(context.timezone.value(), "America/Vancouver") + assert_equal(context.locale.value(), "en-CA") + assert_equal(context.return_provenance, True) + assert_equal(context.actor_id, "farm-1") + assert_equal(context.farm_id.value(), "farm-1") + assert_equal(context.versions.schema, "hyf_ops_v2") + assert_equal(context.versions.taxonomy, "hyf_ops_v2.taxonomy.v1") + assert_equal(context.versions.normalization, "hyf_ops_v2.normalization.v1") + assert_equal(context.versions.review_policy, "hyf_ops_v2.review_policy.v1") + assert_equal( + context.versions.ranking_policy, "hyf_ops_v2.ranking_policy.v1" + ) + assert_equal( + context.versions.question_bundle, "hyf_ops_v2.question_bundle.v1" + ) + assert_equal(context.versions.model, "jev-1.13.0") + + +def test_c004_operation_v2_context_defaults_are_not_host_guesses() raises: + var request = decode_request( + _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"' + ) + ) + assert_true(request.operation_context) + var context = request.operation_context.value().copy() + assert_equal(context.consumer, "unknown") + assert_equal(context.execution_mode_preference, "deterministic") + assert_equal(context.deadline_ms, 2500) + assert_equal(context.return_provenance, False) + # Absent timezone/locale stay unknown; they are not filled from the host clock. + assert_true(not context.timezone) + assert_true(not context.locale) + + +def test_c004_operation_v2_context_is_strict() raises: + var missing_actor = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"farm_id":"farm-1"' + ) + assert_true(_decode_error_message(missing_actor).find("actor_id") >= 0) + + var duplicate_actor = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","actor_id":"farm-2","farm_id":"farm-1"' + ) + assert_true(_decode_error_message(duplicate_actor).find("duplicate") >= 0) + + var missing_farm = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1"' + ) + assert_true(_decode_error_message(missing_farm).find("farm_id") >= 0) + + var null_timezone = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00","timezone":null,' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"' + ) + assert_true(_decode_error_message(null_timezone).find("string") >= 0) + + var unknown_field = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00","planner":"strict",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"' + ) + assert_true(_decode_error_message(unknown_field).find("unexpected") >= 0) + + var empty_actor = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"","farm_id":"farm-1"' + ) + assert_true(_decode_error_message(empty_actor).find("actor_id") >= 0) + + +def test_c004_operation_v2_versions_are_closed_and_complete() raises: + var incomplete = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00","versions":{' + '"schema":"hyf_ops_v2","taxonomy":"t","normalization":"n",' + '"review_policy":"r","ranking_policy":"k","model":"jev-1.13.0"},' + '"actor_id":"farm-1","farm_id":"farm-1"' + ) + assert_true(_decode_error_message(incomplete).find("question_bundle") >= 0) + + var unknown_axis = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00","versions":{' + '"schema":"hyf_ops_v2","taxonomy":"t","normalization":"n",' + '"review_policy":"r","ranking_policy":"k","question_bundle":"q",' + '"model":"jev-1.13.0","extra":"x"},' + '"actor_id":"farm-1","farm_id":"farm-1"' + ) + assert_true(_decode_error_message(unknown_axis).find("unexpected") >= 0) + + +def test_c004_operation_v2_buyer_forbids_farm_identity() raises: + var buyer_with_farm = ( + '{"version":1,"request_id":"v2-buyer-farm",' + '"capability":"buyer_request.interpret",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"buyer-7","farm_id":"farm-1"},' + '"input":{"source":{"source_id":"s1","revision":"r1","text":"25 lb",' + '"source_time":"2026-09-24T08:45:00-07:00","actor_id":"buyer-7"},' + + _v2_farm_references_json() + + "}}" + ) + assert_true(_decode_error_message(buyer_with_farm).find("unexpected") >= 0) + + var buyer_ok = ( + '{"version":1,"request_id":"v2-buyer-ok",' + '"capability":"buyer_request.interpret",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"buyer-7"},' + '"input":{"source":{"source_id":"s1","revision":"r1","text":"25 lb",' + '"source_time":"2026-09-24T08:45:00-07:00","actor_id":"buyer-7"},' + + _v2_farm_references_json() + + "}}" + ) + var request = decode_request(buyer_ok) + assert_true(request.operation_context) + assert_true(not request.operation_context.value().copy().farm_id) + assert_equal(request.operation_context.value().copy().actor_id, "buyer-7") + + +def test_c004_selector_detection_does_not_broaden_legacy_context() raises: + var context = loads("{}") + context.set("versions", loads('{"schema":"hyf_ops_v2"}')) + assert_true(operation_context_selects_v2(context)) + assert_true(not operation_context_selects_v2(loads('{"consumer":"cli"}'))) + assert_true( + not operation_context_selects_v2( + loads('{"versions":{"schema":"hyf_ops_v3"}}') + ) + ) + assert_true(not operation_context_selects_v2(loads('{"versions":1}'))) + + # An unrelated capability keeps the unchanged legacy admission and rejects + # the advertised-but-unsupported `versions` field. + var legacy_with_versions = ( + '{"version":1,"request_id":"legacy-versions",' + '"capability":"query_rewrite","context":{' + + _v2_versions_json() + + '},"input":{"query":"eggs"}}' + ) + assert_true( + _decode_error_message(legacy_with_versions).find("unexpected") >= 0 + ) + + # The unchanged legacy envelope still parses its original context. + var legacy_request = decode_request( + '{"version":1,"request_id":"legacy-ok","capability":"query_rewrite",' + '"context":{"consumer":"radroots-cli","deadline_ms":2500},' + '"input":{"query":"eggs"}}' + ) + assert_true(not legacy_request.operation_context) + assert_equal(legacy_request.context.consumer, "radroots-cli") + + +def test_c004_operation_v2_guard_blocks_shortcut_even_when_enabled() raises: + with SafeTempDir() as temp_dir: + var runtime_context = resolve_startup_context( + RuntimeStartupInput( + env_paths_profile="repo_local", + env_repo_local_base_root=temp_dir, + user_home="/home/unused", + argv=List[String](), + ) + ) + runtime_context.config.effective.runtime.enable_farm_update_interpret = ( + True + ) + assert_true( + _operation_enabled(runtime_context.config, "farm_update.interpret") + ) + var response = loads( + handle_request_line_with_runtime_context( + _v2_farm_request(), runtime_context + ) + ) + assert_equal(response["ok"].bool_value(), False) + assert_equal( + response["error"]["code"].string_value(), "capability_unavailable" + ) + # Never the placeholder shortcut output, and zero provider calls. + assert_true(not _has_key(response, "output")) + + +def test_c004_legacy_gated_operation_still_executes_when_enabled() raises: + with SafeTempDir() as temp_dir: + var runtime_context = resolve_startup_context( + RuntimeStartupInput( + env_paths_profile="repo_local", + env_repo_local_base_root=temp_dir, + user_home="/home/unused", + argv=List[String](), + ) + ) + runtime_context.config.effective.runtime.enable_farm_update_interpret = ( + True + ) + var legacy = ( + '{"version":1,"request_id":"legacy-farm-1",' + '"capability":"farm_update.interpret","input":{' + + _v2_farm_source_json() + + "}}" + ) + var response = loads( + handle_request_line_with_runtime_context(legacy, runtime_context) + ) + assert_equal(response["ok"].bool_value(), True) + assert_true(_has_key(response["output"], "claims")) + + +def test_c004_v2_schema_assets_and_manifest_integrity() raises: + var schema_dir = _dir_of_current_file() / ".." / "schemas" / "hyf_ops_v2" + var manifest = loads((schema_dir / "manifest.json").read_text()) + assert_equal(manifest["selector"]["value"].string_value(), "hyf_ops_v2") + assert_equal(Int(manifest["envelope_version"].int_value()), 1) + assert_equal( + manifest["activation"]["guard_error_code"].string_value(), + "capability_unavailable", + ) + assert_equal( + manifest["activation"]["state"].string_value(), "pre_activation" + ) + + var versions = loads((schema_dir / "version_manifest.json").read_text())[ + "versions" + ] + var axes = List[String]() + axes.append("schema") + axes.append("taxonomy") + axes.append("normalization") + axes.append("review_policy") + axes.append("ranking_policy") + axes.append("question_bundle") + axes.append("model") + for axis in axes: + assert_true(_has_key(versions, axis)) + assert_true(versions[axis].string_value() != "") + assert_equal(versions["schema"].string_value(), "hyf_ops_v2") + + var operations = manifest["operations"] + for operation in operations.object_keys(): + var entry = operations[operation] + var request_schema = loads( + (schema_dir / entry["request_schema"].string_value()).read_text() + ) + var response_schema = loads( + (schema_dir / entry["response_schema"].string_value()).read_text() + ) + assert_equal( + request_schema["properties"]["capability"]["const"].string_value(), + operation, + ) + var context_def = request_schema["$defs"][ + "farm_context" if operation + == "farm_update.interpret" else "buyer_context" + ] + var context_required = List[String]() + for value in context_def["required"].array_items(): + context_required.append(value.string_value()) + assert_true("actor_id" in context_required) + assert_true("versions" in context_required) + assert_true("evaluation_time" in context_required) + var context_properties = context_def["properties"] + if operation == "farm_update.interpret": + assert_true(_has_key(context_properties, "farm_id")) + else: + assert_true(not _has_key(context_properties, "farm_id")) + var response_required = List[String]() + for required in response_schema["required"].array_items(): + response_required.append(required.string_value()) + assert_equal(len(response_required), 3) + assert_true("version" in response_required) + assert_true("request_id" in response_required) + assert_true("ok" in response_required) + + var corpus = loads((schema_dir / "examples" / "corpus.json").read_text()) + var entries = corpus["entries"].array_items() + assert_true(len(entries) >= 30) + for entry in entries: + assert_true(exists(schema_dir / entry["file"].string_value()))