commit 16765a8ae4cc227e1dc138e844741e6ac180cc8a parent 170d40c5063c5db119e6a071c7243378677eccc9 Author: triesap <tyson@radroots.org> Date: Thu, 24 Sep 2026 17:44:00 +0000 C004: requalify hyf_ops_v2 contracts under ADR-0026 D46 (CR01-CR04) - Enforce exact response wire algebra, five-family error codes and conditional request_id correlation in all three response schemas - Bind closed span|record_field evidence, clarification expected_revision, farm-owned prior records, output versions and execution degradation - Replace quadratic context duplicate scan with a linear seen-key gate and reject ambiguous duplicate root keys before v2 selector dispatch - Add safe duplicate-correlation handling, the bounded dispatch sentinel and CR04 controls (77 unit tests) Diffstat:
24 files changed, 1032 insertions(+), 166 deletions(-)
diff --git a/schemas/hyf_ops_v2/buyer_request_interpret.request.schema.json b/schemas/hyf_ops_v2/buyer_request_interpret.request.schema.json @@ -233,9 +233,10 @@ "source_id": {"type": "string", "minLength": 1}, "revision": {"type": "string", "minLength": 1}, "target_field": {"type": "string", "minLength": 1}, - "text": {"type": "string", "minLength": 1} + "text": {"type": "string", "minLength": 1}, + "expected_revision": {"type": "string", "minLength": 1} }, - "required": ["source_id", "revision", "target_field", "text"], + "required": ["source_id", "revision", "target_field", "text", "expected_revision"], "additionalProperties": false }, "buyer_interpret_input": { diff --git a/schemas/hyf_ops_v2/buyer_request_interpret.response.schema.json b/schemas/hyf_ops_v2/buyer_request_interpret.response.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "hyf_ops_v2/buyer_request_interpret.response.schema.json", - "title": "hyf_ops_v2 buyer_request.interpret strict response (ADR-0025 D45 CB02-CB04)", - "$comment": "Typed demand lines with the D23 reported decimal-string projection; inference is never treated as confirmation.", + "title": "hyf_ops_v2 buyer_request.interpret strict response (ADR-0025 D45 CB01-CB04, ADR-0026 D46 CR01-CR02)", + "$comment": "Exact pre-activation wire algebra: ok:true requires output and forbids error; ok:false requires error and forbids output and meta. Typed demand lines with the D23 reported decimal-string projection; inference is never treated as confirmation. Envelope version stays 1. output.versions echoes the requested seven axes; execution.model is the actual model use.", "type": "object", "properties": { "version": { @@ -10,11 +10,11 @@ "const": 1 }, "request_id": { - "type": "string", - "minLength": 1 + "type": "string" }, "trace_id": { - "type": "string" + "type": "string", + "minLength": 1 }, "ok": { "type": "boolean" @@ -32,28 +32,109 @@ }, "required": ["version", "request_id", "ok"], "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": {"ok": {"const": true}}, + "required": ["ok"] + }, + "then": { + "required": ["output"], + "properties": {"request_id": {"type": "string", "minLength": 1}}, + "not": {"required": ["error"]} + } + }, + { + "if": { + "properties": {"ok": {"const": false}}, + "required": ["ok"] + }, + "then": { + "required": ["error"], + "not": {"anyOf": [{"required": ["output"]}, {"required": ["meta"]}]} + } + } + ], "$defs": { "wire_error": { "type": "object", "properties": { - "code": {"type": "string", "minLength": 1}, + "code": { + "type": "string", + "enum": [ + "invalid_request", + "unsupported_capability", + "capability_disabled", + "capability_unavailable", + "internal_error" + ] + }, "message": {"type": "string"} }, "required": ["code", "message"], "additionalProperties": false }, + "output_versions": { + "type": "object", + "properties": { + "schema": {"type": "string", "const": "hyf_ops_v2"}, + "taxonomy": {"type": "string", "minLength": 1}, + "normalization": {"type": "string", "minLength": 1}, + "review_policy": {"type": "string", "minLength": 1}, + "ranking_policy": {"type": "string", "minLength": 1}, + "question_bundle": {"type": "string", "minLength": 1}, + "model": {"type": "string", "minLength": 1} + }, + "required": [ + "schema", + "taxonomy", + "normalization", + "review_policy", + "ranking_policy", + "question_bundle", + "model" + ], + "additionalProperties": false + }, "evidence_span": { "type": "object", "properties": { + "kind": {"type": "string", "const": "span"}, "source_id": {"type": "string", "minLength": 1}, "revision": {"type": "string", "minLength": 1}, "start": {"type": "integer", "minimum": 0}, "end": {"type": "integer", "minimum": 0}, "method": {"type": "string", "minLength": 1} }, - "required": ["source_id", "revision", "start", "end", "method"], + "required": ["kind", "source_id", "revision", "start", "end", "method"], "additionalProperties": false }, + "record_field_evidence": { + "type": "object", + "properties": { + "kind": {"type": "string", "const": "record_field"}, + "source_id": {"type": "string", "minLength": 1}, + "revision": {"type": "string", "minLength": 1}, + "record_id": {"type": "string", "minLength": 1}, + "field": {"type": "string", "minLength": 1}, + "method": {"type": "string", "const": "trusted_record"} + }, + "required": [ + "kind", + "source_id", + "revision", + "record_id", + "field", + "method" + ], + "additionalProperties": false + }, + "evidence_ref": { + "oneOf": [ + {"$ref": "#/$defs/evidence_span"}, + {"$ref": "#/$defs/record_field_evidence"} + ] + }, "reported_quantity": { "type": "object", "properties": { @@ -134,7 +215,7 @@ "value": {"type": ["string", "null"]}, "evidence": { "type": "array", - "items": {"$ref": "#/$defs/evidence_span"} + "items": {"$ref": "#/$defs/evidence_ref"} } }, "required": ["kind", "strength", "evidence"], @@ -143,7 +224,7 @@ }, "evidence": { "type": "array", - "items": {"$ref": "#/$defs/evidence_span"} + "items": {"$ref": "#/$defs/evidence_ref"} }, "review_required": {"type": "boolean"} }, @@ -183,14 +264,48 @@ "status": {"type": "string", "enum": ["complete", "degraded", "failed"]}, "provider_calls": {"type": "integer", "minimum": 0}, "model": {"type": ["string", "null"]}, - "question_bundle": {"type": ["string", "null"]} + "question_bundle": {"type": ["string", "null"]}, + "degraded_reason": {"type": ["string", "null"], "minLength": 1} }, - "required": ["status", "provider_calls", "model", "question_bundle"], - "additionalProperties": false + "required": [ + "status", + "provider_calls", + "model", + "question_bundle", + "degraded_reason" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": {"status": {"const": "degraded"}}, + "required": ["status"] + }, + "then": { + "required": ["degraded_reason"], + "properties": {"degraded_reason": {"type": "string", "minLength": 1}} + } + }, + { + "if": { + "properties": {"status": {"enum": ["complete", "failed"]}}, + "required": ["status"] + }, + "then": {"properties": {"degraded_reason": {"type": "null"}}} + }, + { + "if": { + "properties": {"provider_calls": {"const": 0}}, + "required": ["provider_calls"] + }, + "then": {"properties": {"model": {"type": "null"}}} + } + ] }, "interpret_output": { "type": "object", "properties": { + "versions": {"$ref": "#/$defs/output_versions"}, "demand_lines": { "type": "array", "items": {"$ref": "#/$defs/demand_line"} @@ -198,7 +313,7 @@ "review": {"$ref": "#/$defs/review"}, "execution": {"$ref": "#/$defs/execution"} }, - "required": ["demand_lines", "review", "execution"], + "required": ["versions", "demand_lines", "review", "execution"], "additionalProperties": false } } diff --git a/schemas/hyf_ops_v2/buyer_request_match.request.schema.json b/schemas/hyf_ops_v2/buyer_request_match.request.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "hyf_ops_v2/buyer_request_match.request.schema.json", "title": "hyf_ops_v2 buyer_request.match strict request (ADR-0025 D45 CB01-CB04)", - "$comment": "Corrected versioned operation contract. Consumes the typed reviewed need, authorized snapshots and coverage; no flattened product_phrase/unreserved_value/scale shortcut.", + "$comment": "Corrected versioned operation contract (ADR-0026 D46 CR02). Consumes the typed reviewed need, authorized snapshots and coverage; no flattened product_phrase/unreserved_value/scale shortcut. Evidence is the closed span|record_field union.", "type": "object", "properties": { "version": { @@ -75,15 +75,35 @@ "evidence_span": { "type": "object", "properties": { + "kind": {"type": "string", "const": "span"}, "source_id": {"type": "string", "minLength": 1}, "revision": {"type": "string", "minLength": 1}, "start": {"type": "integer", "minimum": 0}, "end": {"type": "integer", "minimum": 0}, "method": {"type": "string", "minLength": 1} }, - "required": ["source_id", "revision", "start", "end", "method"], + "required": ["kind", "source_id", "revision", "start", "end", "method"], "additionalProperties": false }, + "record_field_evidence": { + "type": "object", + "properties": { + "kind": {"type": "string", "const": "record_field"}, + "source_id": {"type": "string", "minLength": 1}, + "revision": {"type": "string", "minLength": 1}, + "record_id": {"type": "string", "minLength": 1}, + "field": {"type": "string", "minLength": 1}, + "method": {"type": "string", "const": "trusted_record"} + }, + "required": ["kind", "source_id", "revision", "record_id", "field", "method"], + "additionalProperties": false + }, + "evidence_ref": { + "oneOf": [ + {"$ref": "#/$defs/evidence_span"}, + {"$ref": "#/$defs/record_field_evidence"} + ] + }, "reported_quantity": { "type": "object", "properties": { @@ -257,7 +277,7 @@ "value": {"type": ["string", "null"]}, "evidence": { "type": "array", - "items": {"$ref": "#/$defs/evidence_span"} + "items": {"$ref": "#/$defs/evidence_ref"} } }, "required": ["kind", "strength", "evidence"], @@ -266,7 +286,7 @@ }, "evidence": { "type": "array", - "items": {"$ref": "#/$defs/evidence_span"} + "items": {"$ref": "#/$defs/evidence_ref"} }, "review_required": {"type": "boolean"} }, diff --git a/schemas/hyf_ops_v2/buyer_request_match.response.schema.json b/schemas/hyf_ops_v2/buyer_request_match.response.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "hyf_ops_v2/buyer_request_match.response.schema.json", - "title": "hyf_ops_v2 buyer_request.match strict response (ADR-0025 D45 CB02-CB04)", - "$comment": "Assessments and bounded single-supplier plans are suggestions, never reservations. Allocated quantities use the D23 reported projection and carry the lot revision.", + "title": "hyf_ops_v2 buyer_request.match strict response (ADR-0025 D45 CB01-CB04, ADR-0026 D46 CR01-CR02)", + "$comment": "Exact pre-activation wire algebra: ok:true requires output and forbids error; ok:false requires error and forbids output and meta. Assessments and bounded single-supplier plans are suggestions, never reservations. Allocated quantities use the D23 reported projection and carry the lot revision. Envelope version stays 1. output.versions echoes the requested seven axes; execution.model is the actual model use.", "type": "object", "properties": { "version": { @@ -10,11 +10,11 @@ "const": 1 }, "request_id": { - "type": "string", - "minLength": 1 + "type": "string" }, "trace_id": { - "type": "string" + "type": "string", + "minLength": 1 }, "ok": { "type": "boolean" @@ -32,28 +32,109 @@ }, "required": ["version", "request_id", "ok"], "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": {"ok": {"const": true}}, + "required": ["ok"] + }, + "then": { + "required": ["output"], + "properties": {"request_id": {"type": "string", "minLength": 1}}, + "not": {"required": ["error"]} + } + }, + { + "if": { + "properties": {"ok": {"const": false}}, + "required": ["ok"] + }, + "then": { + "required": ["error"], + "not": {"anyOf": [{"required": ["output"]}, {"required": ["meta"]}]} + } + } + ], "$defs": { "wire_error": { "type": "object", "properties": { - "code": {"type": "string", "minLength": 1}, + "code": { + "type": "string", + "enum": [ + "invalid_request", + "unsupported_capability", + "capability_disabled", + "capability_unavailable", + "internal_error" + ] + }, "message": {"type": "string"} }, "required": ["code", "message"], "additionalProperties": false }, + "output_versions": { + "type": "object", + "properties": { + "schema": {"type": "string", "const": "hyf_ops_v2"}, + "taxonomy": {"type": "string", "minLength": 1}, + "normalization": {"type": "string", "minLength": 1}, + "review_policy": {"type": "string", "minLength": 1}, + "ranking_policy": {"type": "string", "minLength": 1}, + "question_bundle": {"type": "string", "minLength": 1}, + "model": {"type": "string", "minLength": 1} + }, + "required": [ + "schema", + "taxonomy", + "normalization", + "review_policy", + "ranking_policy", + "question_bundle", + "model" + ], + "additionalProperties": false + }, "evidence_span": { "type": "object", "properties": { + "kind": {"type": "string", "const": "span"}, "source_id": {"type": "string", "minLength": 1}, "revision": {"type": "string", "minLength": 1}, "start": {"type": "integer", "minimum": 0}, "end": {"type": "integer", "minimum": 0}, "method": {"type": "string", "minLength": 1} }, - "required": ["source_id", "revision", "start", "end", "method"], + "required": ["kind", "source_id", "revision", "start", "end", "method"], + "additionalProperties": false + }, + "record_field_evidence": { + "type": "object", + "properties": { + "kind": {"type": "string", "const": "record_field"}, + "source_id": {"type": "string", "minLength": 1}, + "revision": {"type": "string", "minLength": 1}, + "record_id": {"type": "string", "minLength": 1}, + "field": {"type": "string", "minLength": 1}, + "method": {"type": "string", "const": "trusted_record"} + }, + "required": [ + "kind", + "source_id", + "revision", + "record_id", + "field", + "method" + ], "additionalProperties": false }, + "evidence_ref": { + "oneOf": [ + {"$ref": "#/$defs/evidence_span"}, + {"$ref": "#/$defs/record_field_evidence"} + ] + }, "reported_quantity": { "type": "object", "properties": { @@ -103,7 +184,7 @@ "reason": {"type": "string", "minLength": 1}, "evidence": { "type": "array", - "items": {"$ref": "#/$defs/evidence_span"} + "items": {"$ref": "#/$defs/evidence_ref"} } }, "required": ["kind", "result", "reason"], @@ -206,14 +287,48 @@ "status": {"type": "string", "enum": ["complete", "degraded", "failed"]}, "provider_calls": {"type": "integer", "minimum": 0}, "model": {"type": ["string", "null"]}, - "question_bundle": {"type": ["string", "null"]} + "question_bundle": {"type": ["string", "null"]}, + "degraded_reason": {"type": ["string", "null"], "minLength": 1} }, - "required": ["status", "provider_calls", "model", "question_bundle"], - "additionalProperties": false + "required": [ + "status", + "provider_calls", + "model", + "question_bundle", + "degraded_reason" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": {"status": {"const": "degraded"}}, + "required": ["status"] + }, + "then": { + "required": ["degraded_reason"], + "properties": {"degraded_reason": {"type": "string", "minLength": 1}} + } + }, + { + "if": { + "properties": {"status": {"enum": ["complete", "failed"]}}, + "required": ["status"] + }, + "then": {"properties": {"degraded_reason": {"type": "null"}}} + }, + { + "if": { + "properties": {"provider_calls": {"const": 0}}, + "required": ["provider_calls"] + }, + "then": {"properties": {"model": {"type": "null"}}} + } + ] }, "match_output": { "type": "object", "properties": { + "versions": {"$ref": "#/$defs/output_versions"}, "assessments": { "type": "array", "items": {"$ref": "#/$defs/assessment"} @@ -225,7 +340,13 @@ "limitations": {"$ref": "#/$defs/limitations"}, "execution": {"$ref": "#/$defs/execution"} }, - "required": ["assessments", "plans", "limitations", "execution"], + "required": [ + "versions", + "assessments", + "plans", + "limitations", + "execution" + ], "additionalProperties": false } } diff --git a/schemas/hyf_ops_v2/domain_manifest.json b/schemas/hyf_ops_v2/domain_manifest.json @@ -11,19 +11,24 @@ {"schema_path": "input.references.normalization.units", "domain": "hyf_core.domain.units.unit_dimension", "owner": "C008/C009/C023", "status": "schema_bound"}, {"schema_path": "input.references.normalization.conversions", "domain": "hyf_core.domain.units.ConversionRule / conversion_rule", "owner": "C008/C009/C023", "status": "schema_bound"}, {"schema_path": "input.references.normalization.packs", "domain": "hyf_core.domain.pack.PackRule / pack_rule", "owner": "C008/C009/C023", "status": "schema_bound"}, - {"schema_path": "input.prior_records", "domain": "hyf_core.domain.source.Revision (immutable id/revision)", "owner": "C008/C009", "status": "schema_bound"}, - {"schema_path": "input.clarification", "domain": "hyf_core.domain.clarification.ClarificationEvidence", "owner": "C008/C009/C023", "status": "schema_bound"}, + {"schema_path": "input.prior_records", "domain": "hyf_core.domain.source.Revision (immutable record_id/revision/role) + per-record farm_id ownership", "owner": "C008/C009/C023", "status": "schema_bound; C023 compares each prior_records[].farm_id with context.farm_id"}, + {"schema_path": "input.clarification.source_id", "domain": "hyf_core.domain.clarification.ClarificationEvidence.source_id (clarification-source identity)", "owner": "C008/C009/C023", "status": "schema_bound"}, + {"schema_path": "input.clarification.revision", "domain": "hyf_core.domain.clarification.ClarificationEvidence.revision (clarification-source revision, NOT the target revision)", "owner": "C008/C009/C023", "status": "schema_bound; C009/C023 migration: clarification_is_stale currently compares this clarification-source revision to the current revision and must be migrated to compare expected_revision"}, + {"schema_path": "input.clarification.expected_revision", "domain": "primary input.source.revision linkage (ADR-0026 CR02)", "owner": "C008/C009/C023", "status": "schema_bound; C023 validates the linkage before application; C009 maps it. No runtime constructor change in C004."}, + {"schema_path": "input.clarification.target_field", "domain": "hyf_core.domain.clarification.ClarificationEvidence.target_field (approved field, not arbitrary mutation)", "owner": "C008/C009/C023", "status": "schema_bound"}, + {"schema_path": "input.clarification.text", "domain": "hyf_core.domain.clarification.ClarificationEvidence.text", "owner": "C008/C009", "status": "schema_bound"}, {"schema_path": "input.need", "domain": "hyf_core.domain.demand.DemandLine", "owner": "C008/C009/C023", "status": "schema_bound"}, {"schema_path": "input.need.actor_id", "domain": "hyf_core.domain.source.ActorId", "owner": "C023", "status": "schema_bound"}, {"schema_path": "input.snapshots", "domain": "hyf_core.domain.snapshot.SupplySnapshot", "owner": "C008/C009/C023", "status": "schema_bound"}, {"schema_path": "input.coverage", "domain": "hyf_core.domain.coverage.Coverage", "owner": "C008/C009/C023", "status": "schema_bound"}, {"schema_path": "*.quantity.value", "domain": "hyf_core.domain.quantity.Quantity value/scale (D23 decimal-string grammar)", "owner": "C004/C009", "status": "schema_bound"}, - {"schema_path": "*.evidence", "domain": "hyf_core.domain.evidence.EvidenceRef (UTF-8 byte span, source_id + revision)", "owner": "C008/C009/C023", "status": "schema_bound"}, + {"schema_path": "*.evidence", "domain": "hyf_core.domain.evidence.EvidenceRef closed span|record_field union (UTF-8 byte span; trusted_record provenance)", "owner": "C008/C009/C023", "status": "schema_bound"}, {"schema_path": "output.claims/proposed_changes", "domain": "hyf_application.farm_review_output", "owner": "C008/C009/C030", "status": "schema_bound"}, {"schema_path": "output.demand_lines", "domain": "hyf_application.buyer_needs", "owner": "C008/C009/C030", "status": "schema_bound"}, {"schema_path": "output.assessments/plans/limitations", "domain": "hyf_application.match_checks / match_plan / match_index", "owner": "C008/C009/C038", "status": "schema_bound"}, {"schema_path": "output.execution", "domain": "hyf_core.domain.execution.execution_state", "owner": "C008/C009", "status": "schema_bound"} ], "authority_integration": "docs/spec/hyf_v1_jev/specs/AUTHORITY_INTEGRATION.md: HYF is read-only; supplied records are host-authorized preconditions, HYF validates consistency but performs no writes and claims no authentication.", + "clarification_reconciliation": "ADR-0026 D46 CR02 separates the clarification source identity/revision (input.clarification.revision) from the primary target revision (input.clarification.expected_revision, which equals input.source.revision). The legacy ClarificationEvidence constructor is preserved unchanged until its planned C009/C023 migration; the schema binding and this mapping are the C004 obligation.", "unavailability": "C004 parses the corrected context and then refuses dispatch with capability_unavailable until C042-C046 activate the operations." } diff --git a/schemas/hyf_ops_v2/examples/corpus.json b/schemas/hyf_ops_v2/examples/corpus.json @@ -2,7 +2,7 @@ "schema_version": 1, "program": "hyf_http_v1", "revision": "hyf_ops_v2", - "note": "Reference-validation corpus for C004. schema_valid records the JSON Schema outcome; semantic names a labelled obligation that JSON Schema cannot express and that is exercised separately (semantic-cases.json). Targets: '<operation>.<request|response>' uses that operation schema; 'def:<name>' validates against that $defs entry of the farm request schema.", + "note": "Reference-validation corpus for C004 (ADR-0026 D46 CR01-CR03 successor). schema_valid records the JSON Schema outcome; semantic names a labelled obligation that JSON Schema cannot express and that is exercised separately (semantic-cases.json). Targets: '<operation>.<request|response>' uses that operation schema; 'def:<name>' validates against that named $defs entry in any hyf_ops_v2 schema (resolved across all schemas, first match). The CR01 envelope algebra and CR02 linkage/degradation controls are executed programmatically by the successor reference diagnostic (docs/execution/evidence/hyf_http_v1/C004-requalification/reference-schema-validate.v2.py.txt).", "entries": [ {"file": "examples/valid/farm_update_interpret.request.json", "target": "farm_update.interpret.request", "schema_valid": true}, {"file": "examples/valid/farm_update_interpret.response.json", "target": "farm_update.interpret.response", "schema_valid": true}, @@ -18,13 +18,15 @@ {"file": "examples/valid/quantity.unknown.json", "target": "def:reported_quantity", "schema_valid": true}, {"file": "examples/valid/taxonomy.unicode.json", "target": "def:taxonomy_bundle", "schema_valid": true}, {"file": "examples/valid/normalization.full.json", "target": "def:normalization_bundle", "schema_valid": true}, - {"file": "examples/invalid/quantity.ten_fractional_digits.json", "target": "def:reported_quantity", "schema_valid": false}, - {"file": "examples/invalid/quantity.leading_zero.json", "target": "def:reported_quantity", "schema_valid": false}, - {"file": "examples/invalid/quantity.exponent.json", "target": "def:reported_quantity", "schema_valid": false}, - {"file": "examples/invalid/quantity.leading_plus.json", "target": "def:reported_quantity", "schema_valid": false}, - {"file": "examples/invalid/quantity.unknown_with_value.json", "target": "def:reported_quantity", "schema_valid": false}, - {"file": "examples/invalid/quantity.known_without_value.json", "target": "def:reported_quantity", "schema_valid": false}, - {"file": "examples/invalid/quantity.number_not_string.json", "target": "def:reported_quantity", "schema_valid": false}, + {"file": "examples/valid/evidence.record_field.json", "target": "def:evidence_ref", "schema_valid": true}, + {"file": "examples/invalid/evidence.span_with_record_fields.json", "target": "def:evidence_ref", "schema_valid": false}, + {"file": "examples/invalid/quantity.ten_fractional_digits.json", "target": "def:reported_quantity", "schema_valid": false, "semantic": "fractional_width_10"}, + {"file": "examples/invalid/quantity.leading_zero.json", "target": "def:reported_quantity", "schema_valid": false, "semantic": "ambiguous_leading_zero"}, + {"file": "examples/invalid/quantity.exponent.json", "target": "def:reported_quantity", "schema_valid": false, "semantic": "exponent_spelling"}, + {"file": "examples/invalid/quantity.leading_plus.json", "target": "def:reported_quantity", "schema_valid": false, "semantic": "leading_plus"}, + {"file": "examples/invalid/quantity.unknown_with_value.json", "target": "def:reported_quantity", "schema_valid": false, "semantic": "unknown_quantity_with_value"}, + {"file": "examples/invalid/quantity.known_without_value.json", "target": "def:reported_quantity", "schema_valid": false, "semantic": "known_quantity_without_value"}, + {"file": "examples/invalid/quantity.number_not_string.json", "target": "def:reported_quantity", "schema_valid": false, "semantic": "type_coercion_number_for_string"}, {"file": "examples/invalid/quantity.bad_qualifier.json", "target": "def:reported_quantity", "schema_valid": false}, {"file": "examples/invalid/quantity.unknown_field.json", "target": "def:reported_quantity", "schema_valid": false}, {"file": "examples/invalid/quantity.negative_zero.json", "target": "def:reported_quantity", "schema_valid": true, "semantic": "negative_zero"}, @@ -35,12 +37,12 @@ {"file": "examples/invalid/normalization.pack_zero.json", "target": "def:normalization_bundle", "schema_valid": false}, {"file": "examples/invalid/farm_update_interpret.request.unknown_field.json", "target": "farm_update.interpret.request", "schema_valid": false}, {"file": "examples/invalid/farm_update_interpret.request.missing_actor.json", "target": "farm_update.interpret.request", "schema_valid": false}, - {"file": "examples/invalid/farm_update_interpret.request.present_null_timezone.json", "target": "farm_update.interpret.request", "schema_valid": false}, + {"file": "examples/invalid/farm_update_interpret.request.present_null_timezone.json", "target": "farm_update.interpret.request", "schema_valid": false, "semantic": "present_null_optional_scalar"}, {"file": "examples/invalid/farm_update_interpret.response.unknown_field.json", "target": "farm_update.interpret.response", "schema_valid": false}, {"file": "examples/invalid/farm_update_interpret.response.negative_zero_quantity.json", "target": "farm_update.interpret.response", "schema_valid": true, "semantic": "negative_zero"}, {"file": "examples/invalid/buyer_request_interpret.request.farm_id_forbidden.json", "target": "buyer_request.interpret.request", "schema_valid": false}, {"file": "examples/invalid/buyer_request_match.request.missing_references.json", "target": "buyer_request.match.request", "schema_valid": false}, {"file": "examples/invalid/buyer_request_match.request.need_actor_missing.json", "target": "buyer_request.match.request", "schema_valid": false}, - {"file": "examples/invalid/buyer_request_match.request.unknown_selector.json", "target": "buyer_request.match.request", "schema_valid": false} + {"file": "examples/invalid/buyer_request_match.request.unknown_selector.json", "target": "buyer_request.match.request", "schema_valid": false, "semantic": "unknown_selector"} ] } diff --git a/schemas/hyf_ops_v2/examples/invalid/evidence.span_with_record_fields.json b/schemas/hyf_ops_v2/examples/invalid/evidence.span_with_record_fields.json @@ -0,0 +1,9 @@ +{ + "kind": "span", + "source_id": "src-farm-1", + "revision": "rev-7", + "start": 11, + "end": 21, + "method": "span", + "record_id": "record-100" +} diff --git a/schemas/hyf_ops_v2/examples/invalid/farm_update_interpret.response.negative_zero_quantity.json b/schemas/hyf_ops_v2/examples/invalid/farm_update_interpret.response.negative_zero_quantity.json @@ -3,6 +3,15 @@ "request_id": "farm-interpret-req-1", "ok": true, "output": { + "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" + }, "claims": [ { "claim_id": "claim-1", @@ -15,6 +24,6 @@ ], "proposed_changes": [], "review": {"required": false, "clarifications": []}, - "execution": {"status": "complete", "provider_calls": 0, "model": null, "question_bundle": null} + "execution": {"status": "complete", "provider_calls": 0, "model": null, "question_bundle": null, "degraded_reason": null} } } diff --git a/schemas/hyf_ops_v2/examples/valid/buyer_request_interpret.request.json b/schemas/hyf_ops_v2/examples/valid/buyer_request_interpret.request.json @@ -57,7 +57,8 @@ "source_id": "src-clar-2", "revision": "rev-1", "target_field": "conditions.window", - "text": "weekend includes Friday evening" + "text": "weekend includes Friday evening", + "expected_revision": "rev-3" } } } diff --git a/schemas/hyf_ops_v2/examples/valid/buyer_request_interpret.response.json b/schemas/hyf_ops_v2/examples/valid/buyer_request_interpret.response.json @@ -3,6 +3,15 @@ "request_id": "buyer-interpret-req-1", "ok": true, "output": { + "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" + }, "demand_lines": [ { "line_id": "line-1", @@ -14,7 +23,7 @@ "strength": "mandatory", "value": "pickup", "evidence": [ - {"source_id": "src-buyer-1", "revision": "rev-3", "start": 55, "end": 61, "method": "span"} + {"kind": "span", "source_id": "src-buyer-1", "revision": "rev-3", "start": 55, "end": 61, "method": "span"} ] }, { @@ -22,17 +31,17 @@ "strength": "preferred", "value": "2026-09-26/2026-09-27", "evidence": [ - {"source_id": "src-buyer-1", "revision": "rev-3", "start": 62, "end": 71, "method": "span"} + {"kind": "span", "source_id": "src-buyer-1", "revision": "rev-3", "start": 62, "end": 71, "method": "span"} ] } ], "evidence": [ - {"source_id": "src-buyer-1", "revision": "rev-3", "start": 12, "end": 39, "method": "span"} + {"kind": "span", "source_id": "src-buyer-1", "revision": "rev-3", "start": 12, "end": 39, "method": "span"} ], "review_required": false } ], "review": {"required": false, "clarifications": []}, - "execution": {"status": "complete", "provider_calls": 0, "model": null, "question_bundle": null} + "execution": {"status": "complete", "provider_calls": 0, "model": null, "question_bundle": null, "degraded_reason": null} } } diff --git a/schemas/hyf_ops_v2/examples/valid/buyer_request_match.request.json b/schemas/hyf_ops_v2/examples/valid/buyer_request_match.request.json @@ -37,12 +37,12 @@ "strength": "mandatory", "value": "pickup", "evidence": [ - {"source_id": "src-buyer-1", "revision": "rev-3", "start": 55, "end": 61, "method": "span"} + {"kind": "span", "source_id": "src-buyer-1", "revision": "rev-3", "start": 55, "end": 61, "method": "span"} ] } ], "evidence": [ - {"source_id": "src-buyer-1", "revision": "rev-3", "start": 12, "end": 39, "method": "span"} + {"kind": "span", "source_id": "src-buyer-1", "revision": "rev-3", "start": 12, "end": 39, "method": "span"} ], "review_required": false } diff --git a/schemas/hyf_ops_v2/examples/valid/buyer_request_match.response.json b/schemas/hyf_ops_v2/examples/valid/buyer_request_match.response.json @@ -3,6 +3,15 @@ "request_id": "buyer-match-req-1", "ok": true, "output": { + "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" + }, "assessments": [ { "candidate_id": "lot-1", @@ -44,6 +53,6 @@ "supported_mode": "single_supplier_compatible_lots", "unsupported": ["multi_supplier"] }, - "execution": {"status": "complete", "provider_calls": 0, "model": null, "question_bundle": null} + "execution": {"status": "complete", "provider_calls": 0, "model": null, "question_bundle": null, "degraded_reason": null} } } diff --git a/schemas/hyf_ops_v2/examples/valid/evidence.record_field.json b/schemas/hyf_ops_v2/examples/valid/evidence.record_field.json @@ -0,0 +1,8 @@ +{ + "kind": "record_field", + "source_id": "src-farm-1", + "revision": "rev-2", + "record_id": "record-100", + "field": "quantity", + "method": "trusted_record" +} diff --git a/schemas/hyf_ops_v2/examples/valid/farm_update_interpret.request.json b/schemas/hyf_ops_v2/examples/valid/farm_update_interpret.request.json @@ -62,13 +62,14 @@ } }, "prior_records": [ - {"record_id": "record-100", "revision": "rev-2", "role": "authorized_listing"} + {"record_id": "record-100", "revision": "rev-2", "role": "authorized_listing", "farm_id": "farm-1"} ], "clarification": { "source_id": "src-clar-1", "revision": "rev-1", "target_field": "quantity", - "text": "about 80 lb means at least 75 and at most 85 lb" + "text": "about 80 lb means at least 75 and at most 85 lb", + "expected_revision": "rev-7" } } } diff --git a/schemas/hyf_ops_v2/examples/valid/farm_update_interpret.response.json b/schemas/hyf_ops_v2/examples/valid/farm_update_interpret.response.json @@ -4,6 +4,15 @@ "trace_id": "farm-interpret-trace-1", "ok": true, "output": { + "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" + }, "claims": [ { "claim_id": "claim-1", @@ -11,7 +20,7 @@ "status": "offered", "quantity": {"state": "known", "value": "80.00", "unit": "lb", "qualifier": "approximate"}, "evidence": [ - {"source_id": "src-farm-1", "revision": "rev-7", "start": 11, "end": 21, "method": "span"} + {"kind": "span", "source_id": "src-farm-1", "revision": "rev-7", "start": 11, "end": 21, "method": "span"} ], "review_required": true } @@ -22,7 +31,7 @@ "operation": "remaining", "expected_revision": "rev-2", "evidence": [ - {"source_id": "src-farm-1", "revision": "rev-7", "start": 11, "end": 21, "method": "span"} + {"kind": "span", "source_id": "src-farm-1", "revision": "rev-7", "start": 11, "end": 21, "method": "span"} ] } ], @@ -34,7 +43,8 @@ "status": "complete", "provider_calls": 0, "model": null, - "question_bundle": null + "question_bundle": null, + "degraded_reason": null } } } diff --git a/schemas/hyf_ops_v2/farm_update_interpret.request.schema.json b/schemas/hyf_ops_v2/farm_update_interpret.request.schema.json @@ -169,83 +169,6 @@ ], "additionalProperties": false }, - "evidence_span": { - "type": "object", - "properties": { - "source_id": { - "type": "string", - "minLength": 1 - }, - "revision": { - "type": "string", - "minLength": 1 - }, - "start": { - "type": "integer", - "minimum": 0 - }, - "end": { - "type": "integer", - "minimum": 0 - }, - "method": { - "type": "string", - "minLength": 1 - } - }, - "required": ["source_id", "revision", "start", "end", "method"], - "additionalProperties": false - }, - "reported_quantity": { - "type": "object", - "properties": { - "state": { - "type": "string", - "enum": ["known", "unknown"] - }, - "value": { - "type": ["string", "null"], - "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]{1,9})?$" - }, - "unit": { - "type": ["string", "null"], - "minLength": 1 - }, - "qualifier": { - "type": "string", - "enum": ["exact", "approximate"] - } - }, - "required": ["state", "qualifier"], - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": {"state": {"const": "unknown"}}, - "required": ["state"] - }, - "then": { - "properties": {"value": {"type": "null"}, "unit": {"type": "null"}} - } - }, - { - "if": { - "properties": {"state": {"const": "known"}}, - "required": ["state"] - }, - "then": { - "properties": { - "value": { - "type": "string", - "pattern": "^-?(0|[1-9][0-9]*)(\\.[0-9]{1,9})?$" - }, - "unit": {"type": "string", "minLength": 1} - }, - "required": ["value", "unit"] - } - } - ] - }, "taxonomy_bundle": { "type": "object", "properties": { @@ -365,9 +288,10 @@ "properties": { "record_id": {"type": "string", "minLength": 1}, "revision": {"type": "string", "minLength": 1}, - "role": {"type": "string", "minLength": 1} + "role": {"type": "string", "minLength": 1}, + "farm_id": {"type": "string", "minLength": 1} }, - "required": ["record_id", "revision", "role"], + "required": ["record_id", "revision", "role", "farm_id"], "additionalProperties": false } }, @@ -377,9 +301,10 @@ "source_id": {"type": "string", "minLength": 1}, "revision": {"type": "string", "minLength": 1}, "target_field": {"type": "string", "minLength": 1}, - "text": {"type": "string", "minLength": 1} + "text": {"type": "string", "minLength": 1}, + "expected_revision": {"type": "string", "minLength": 1} }, - "required": ["source_id", "revision", "target_field", "text"], + "required": ["source_id", "revision", "target_field", "text", "expected_revision"], "additionalProperties": false }, "farm_input": { diff --git a/schemas/hyf_ops_v2/farm_update_interpret.response.schema.json b/schemas/hyf_ops_v2/farm_update_interpret.response.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "hyf_ops_v2/farm_update_interpret.response.schema.json", - "title": "hyf_ops_v2 farm_update.interpret strict response (ADR-0025 D45 CB02-CB04)", - "$comment": "Proposals only, never inventory mutation. quantity uses the D23 reported decimal-string projection. Envelope version stays 1.", + "title": "hyf_ops_v2 farm_update.interpret strict response (ADR-0025 D45 CB01-CB04, ADR-0026 D46 CR01-CR02)", + "$comment": "Exact pre-activation wire algebra: ok:true requires output and forbids error; ok:false requires error and forbids output and meta. Proposals only, never inventory mutation. quantity uses the D23 reported decimal-string projection. Envelope version stays 1. output.versions echoes the requested seven axes; execution.model is the actual model use.", "type": "object", "properties": { "version": { @@ -10,11 +10,11 @@ "const": 1 }, "request_id": { - "type": "string", - "minLength": 1 + "type": "string" }, "trace_id": { - "type": "string" + "type": "string", + "minLength": 1 }, "ok": { "type": "boolean" @@ -32,28 +32,109 @@ }, "required": ["version", "request_id", "ok"], "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": {"ok": {"const": true}}, + "required": ["ok"] + }, + "then": { + "required": ["output"], + "properties": {"request_id": {"type": "string", "minLength": 1}}, + "not": {"required": ["error"]} + } + }, + { + "if": { + "properties": {"ok": {"const": false}}, + "required": ["ok"] + }, + "then": { + "required": ["error"], + "not": {"anyOf": [{"required": ["output"]}, {"required": ["meta"]}]} + } + } + ], "$defs": { "wire_error": { "type": "object", "properties": { - "code": {"type": "string", "minLength": 1}, + "code": { + "type": "string", + "enum": [ + "invalid_request", + "unsupported_capability", + "capability_disabled", + "capability_unavailable", + "internal_error" + ] + }, "message": {"type": "string"} }, "required": ["code", "message"], "additionalProperties": false }, + "output_versions": { + "type": "object", + "properties": { + "schema": {"type": "string", "const": "hyf_ops_v2"}, + "taxonomy": {"type": "string", "minLength": 1}, + "normalization": {"type": "string", "minLength": 1}, + "review_policy": {"type": "string", "minLength": 1}, + "ranking_policy": {"type": "string", "minLength": 1}, + "question_bundle": {"type": "string", "minLength": 1}, + "model": {"type": "string", "minLength": 1} + }, + "required": [ + "schema", + "taxonomy", + "normalization", + "review_policy", + "ranking_policy", + "question_bundle", + "model" + ], + "additionalProperties": false + }, "evidence_span": { "type": "object", "properties": { + "kind": {"type": "string", "const": "span"}, "source_id": {"type": "string", "minLength": 1}, "revision": {"type": "string", "minLength": 1}, "start": {"type": "integer", "minimum": 0}, "end": {"type": "integer", "minimum": 0}, "method": {"type": "string", "minLength": 1} }, - "required": ["source_id", "revision", "start", "end", "method"], + "required": ["kind", "source_id", "revision", "start", "end", "method"], + "additionalProperties": false + }, + "record_field_evidence": { + "type": "object", + "properties": { + "kind": {"type": "string", "const": "record_field"}, + "source_id": {"type": "string", "minLength": 1}, + "revision": {"type": "string", "minLength": 1}, + "record_id": {"type": "string", "minLength": 1}, + "field": {"type": "string", "minLength": 1}, + "method": {"type": "string", "const": "trusted_record"} + }, + "required": [ + "kind", + "source_id", + "revision", + "record_id", + "field", + "method" + ], "additionalProperties": false }, + "evidence_ref": { + "oneOf": [ + {"$ref": "#/$defs/evidence_span"}, + {"$ref": "#/$defs/record_field_evidence"} + ] + }, "reported_quantity": { "type": "object", "properties": { @@ -117,7 +198,7 @@ "quantity": {"$ref": "#/$defs/reported_quantity"}, "evidence": { "type": "array", - "items": {"$ref": "#/$defs/evidence_span"} + "items": {"$ref": "#/$defs/evidence_ref"} }, "review_required": {"type": "boolean"} }, @@ -150,11 +231,44 @@ "expected_revision": {"type": ["string", "null"], "minLength": 1}, "evidence": { "type": "array", - "items": {"$ref": "#/$defs/evidence_span"} + "items": {"$ref": "#/$defs/evidence_ref"} } }, "required": ["target", "operation", "evidence"], - "additionalProperties": false + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "target": { + "properties": {"id": {"type": "string", "minLength": 1}}, + "required": ["id"] + } + }, + "required": ["target"] + }, + "then": { + "required": ["expected_revision"], + "properties": { + "expected_revision": {"type": "string", "minLength": 1} + } + } + }, + { + "if": { + "properties": { + "target": { + "properties": {"id": {"type": "null"}}, + "required": ["id"] + } + }, + "required": ["target"] + }, + "then": { + "properties": {"expected_revision": {"type": "null"}} + } + } + ] }, "review": { "type": "object", @@ -182,14 +296,48 @@ "status": {"type": "string", "enum": ["complete", "degraded", "failed"]}, "provider_calls": {"type": "integer", "minimum": 0}, "model": {"type": ["string", "null"]}, - "question_bundle": {"type": ["string", "null"]} + "question_bundle": {"type": ["string", "null"]}, + "degraded_reason": {"type": ["string", "null"], "minLength": 1} }, - "required": ["status", "provider_calls", "model", "question_bundle"], - "additionalProperties": false + "required": [ + "status", + "provider_calls", + "model", + "question_bundle", + "degraded_reason" + ], + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": {"status": {"const": "degraded"}}, + "required": ["status"] + }, + "then": { + "required": ["degraded_reason"], + "properties": {"degraded_reason": {"type": "string", "minLength": 1}} + } + }, + { + "if": { + "properties": {"status": {"enum": ["complete", "failed"]}}, + "required": ["status"] + }, + "then": {"properties": {"degraded_reason": {"type": "null"}}} + }, + { + "if": { + "properties": {"provider_calls": {"const": 0}}, + "required": ["provider_calls"] + }, + "then": {"properties": {"model": {"type": "null"}}} + } + ] }, "farm_output": { "type": "object", "properties": { + "versions": {"$ref": "#/$defs/output_versions"}, "claims": { "type": "array", "items": {"$ref": "#/$defs/claim"} @@ -201,7 +349,13 @@ "review": {"$ref": "#/$defs/review"}, "execution": {"$ref": "#/$defs/execution"} }, - "required": ["claims", "proposed_changes", "review", "execution"], + "required": [ + "versions", + "claims", + "proposed_changes", + "review", + "execution" + ], "additionalProperties": false } } diff --git a/schemas/hyf_ops_v2/semantic-cases.json b/schemas/hyf_ops_v2/semantic-cases.json @@ -30,6 +30,8 @@ {"id": "taxonomy_catalogue_id_duplicate", "path": "input.references.taxonomy.products[].catalogue_id unique", "expect": "reject", "wire": "invalid_request", "owner": "C023", "enforced_by": "C004 labelled semantic script; C023 typed consistency"}, {"id": "normalization_unit_duplicate", "path": "input.references.normalization.units[].unit unique", "expect": "reject", "wire": "invalid_request", "owner": "C023", "enforced_by": "C004 labelled semantic script; C023 typed consistency"}, {"id": "coverage_count_disagreement", "path": "input.coverage evaluated/excluded counts disagree with actual inputs/results", "expect": "reject", "wire": "invalid_request", "owner": "C023", "enforced_by": "supplied_only coverage accounting; C023 typed consistency"}, - {"id": "deterministic_path_reports_model_non_use", "path": "context.versions.model present with deterministic execution", "expect": "accepted_no_call", "wire": "capability_unavailable (pre-activation)", "owner": "C042-C046", "enforced_by": "C004 unavailability guard issues zero provider calls"} + {"id": "deterministic_path_reports_model_non_use", "path": "context.versions.model present with deterministic execution", "expect": "accepted_no_call", "wire": "capability_unavailable (pre-activation)", "owner": "C042-C046", "enforced_by": "C004 unavailability guard issues zero provider calls"}, + {"id": "prior_record_farm_ownership", "path": "input.prior_records[].farm_id equals context.farm_id", "expect": "reject", "wire": "invalid_request", "owner": "C023", "enforced_by": "C004 binds the required per-record farm_id and executes a labelled cross-reference script; C023 owns typed farm_id equality with context.farm_id"}, + {"id": "proposal_expected_revision_linkage", "path": "output.proposed_changes[].expected_revision ties to the supplied target record when target.id is non-null", "expect": "reject", "wire": "invalid_request", "owner": "C023", "enforced_by": "C004 schema conditional (non-null id requires non-null expected_revision; id null forbids an invented revision) and a labelled cross-reference script; C023 owns typed linkage"} ] } diff --git a/src/hyf_core/operation_context.mojo b/src/hyf_core/operation_context.mojo @@ -7,7 +7,7 @@ # 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 std.collections import Dict, List, Optional from json import Value from json.deserialize import get_bool, get_int, get_string @@ -52,13 +52,15 @@ def _require_object(value: Value, context: String) raises: 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] + "'" - ) + # ADR-0026 D46 CR04: bounded linear seen-key admission. The previous + # all-pairs scan was quadratic in the decoded key count; this visits each + # entry once and rejects the first repeated decoded key. + var seen = Dict[String, Bool]() + for item in value.object_items(): + var key = String(item[0]) + if key in seen: + raise Error(context + " contains duplicate field '" + key + "'") + seen[key] = True def _require_allowed_keys( @@ -204,6 +206,10 @@ def operation_context_selects_v2(context_json: Value) raises -> Bool: 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. + + ADR-0026 D46 CR04: this first-wins lookup is only used after the envelope + root duplicate gate has rejected any ambiguous v2-targeting envelope; use + ``context_selects_v2_any`` when duplicate-aware inspection is required. """ if not context_json.is_object(): return False @@ -220,6 +226,32 @@ def operation_context_selects_v2(context_json: Value) raises -> Bool: return String(schema.string_value()) == OPERATION_CONTRACT_SCHEMA_V2 +def context_selects_v2_any(context_json: Value) raises -> Bool: + """Duplicate-aware v2 selector inspection for the envelope admission gate. + + Scans every decoded ``versions`` member and every decoded ``schema`` entry + instead of the first match, so a duplicate key cannot hide a v2 selector + behind an earlier legacy value. Not a substitute for unambiguous parsing: + the caller rejects the whole envelope when duplicate root keys are found. + """ + if not context_json.is_object(): + return False + for item in context_json.object_items(): + if item[0] != "versions": + continue + var versions = item[1].copy() + if not versions.is_object(): + continue + for member in versions.object_items(): + if member[0] == "schema" and member[1].is_string(): + if ( + String(member[1].string_value()) + == OPERATION_CONTRACT_SCHEMA_V2 + ): + return True + return False + + def parse_operation_context( json: Value, capability: String ) raises -> OperationContext: diff --git a/src/hyf_stdio/codec.mojo b/src/hyf_stdio/codec.mojo @@ -31,6 +31,16 @@ def _extract_optional_string(value: Value, key: String) -> Optional[String]: return None +def _root_key_occurrences(value: Value, key: String) -> Int: + if not value.is_object(): + return 0 + var count = 0 + for candidate in value.object_keys(): + if candidate == key: + count += 1 + return count + + def decode_request(line: String) raises -> WireRequest: if line == "": raise Error("request line must not be empty") @@ -48,11 +58,20 @@ def extract_request_correlation(line: String) -> RequestCorrelation: try: var json = loads(line) + # ADR-0026 D46 CR04: only a single unambiguous correlation key is + # trusted. A duplicated request_id/trace_id is ambiguous, so recovery + # uses the existing no-trustworthy-correlation behavior instead of + # first/last-wins correlation. var extracted_request_id = _extract_optional_string(json, "request_id") - if extracted_request_id: + if ( + extracted_request_id + and _root_key_occurrences(json, "request_id") == 1 + ): request_id = extracted_request_id.value() - trace_id = _extract_optional_string(json, "trace_id") + var extracted_trace_id = _extract_optional_string(json, "trace_id") + if _root_key_occurrences(json, "trace_id") == 1: + trace_id = extracted_trace_id except e: pass diff --git a/src/hyf_stdio/dispatch_sentinel.mojo b/src/hyf_stdio/dispatch_sentinel.mojo @@ -0,0 +1,32 @@ +# ADR-0026 D46 CR04 — bounded local dispatch sentinel. +# +# Test observation hook only. When ``HYF_DISPATCH_SENTINEL`` names a file path, +# every legacy/shortcut business-capability dispatch attempt appends one line +# naming the capability. The variable is unset in normal runs, so the hook is +# inert there. It exists so the pre-activation guard's zero-dispatch obligation +# can be proven with an executed counter (positive control: a legacy dispatch +# records a line; negative control: a recognized v2 request records nothing) +# rather than by inferring from the absence of shortcut output alone. +# +# This does not add a provider or credential path, and it never records request +# or source payloads; only the capability name is written. + +from std.os import getenv + + +comptime _HYF_DISPATCH_SENTINEL_ENV = "HYF_DISPATCH_SENTINEL" + + +def hyf_dispatch_sentinel_env_name() -> String: + return _HYF_DISPATCH_SENTINEL_ENV + + +def record_business_dispatch_attempt(capability: String): + var path = getenv(_HYF_DISPATCH_SENTINEL_ENV, "") + if path == "": + return + try: + with open(path, "a") as sentinel: + sentinel.write("dispatch " + capability + "\n") + except: + pass diff --git a/src/hyf_stdio/envelope.mojo b/src/hyf_stdio/envelope.mojo @@ -1,4 +1,4 @@ -from std.collections import Optional +from std.collections import Dict, Optional from json import Value, loads from json.deserialize import Deserializable, get_string @@ -6,6 +6,7 @@ from json.deserialize import Deserializable, get_string from hyf_core.metadata import hyf_protocol_version from hyf_core.operation_context import ( OperationContext, + context_selects_v2_any, is_corrected_operation, operation_context_selects_v2, parse_operation_context, @@ -52,6 +53,37 @@ def _has_key(value: Value, key: String) -> Bool: return False +def _first_duplicate_root_key(value: Value) raises -> Optional[String]: + # ADR-0026 D46 CR04: decoded-key identity across every raw object entry, so + # escaped equivalents (`\u0063ontext`) and repeated values are both found + # rather than only the first `value[key]` lookup. + var seen = Dict[String, Bool]() + for item in value.object_items(): + var key = String(item[0]) + if key in seen: + return Optional[String](key) + seen[key] = True + return None + + +def envelope_targets_corrected_v2(json: Value) raises -> Bool: + """True when an envelope contains a corrected-op capability value or a + duplicate-aware v2 context selector. + + Scans every raw root entry, so a duplicated `capability` or `context` key + cannot hide a corrected-operation value or a v2 selector behind an earlier + legacy entry. + """ + for item in json.object_items(): + if item[0] == "capability" and item[1].is_string(): + if is_corrected_operation(String(item[1].string_value())): + return True + elif item[0] == "context": + if context_selects_v2_any(item[1]): + return True + return False + + def _require_protocol_version(json: Value) raises -> Int: if not _has_key(json, "version"): raise Error("request envelope field 'version' is required") @@ -106,6 +138,18 @@ struct WireRequest(Copyable, Deserializable, Movable): @staticmethod def from_json(json: Value) raises -> Self: _require_object(json, "request envelope") + # ADR-0026 D46 CR04: reject ambiguous duplicate top-level keys before + # first-wins lookup/selector dispatch for any envelope that targets a + # corrected-operation capability or a v2 context selector. Unrelated + # legacy capabilities keep their existing admission behavior. + if envelope_targets_corrected_v2(json): + var duplicate = _first_duplicate_root_key(json) + if duplicate: + raise Error( + "request envelope contains duplicate top-level field '" + + duplicate.value() + + "'" + ) _require_request_keys(json) var version = _require_protocol_version(json) diff --git a/src/hyf_stdio/server.mojo b/src/hyf_stdio/server.mojo @@ -30,6 +30,7 @@ from hyf_stdio.codec import ( encode_success, extract_request_correlation, ) +from hyf_stdio.dispatch_sentinel import record_business_dispatch_attempt from hyf_stdio.control.capabilities import ( build_capabilities_output_with_runtime_context, ) @@ -203,6 +204,11 @@ def _route_business_capability( if request.operation_context: return encode_error(_unavailable_response(request)) + # ADR-0026 D46 CR04: every path below is a real dispatch attempt. The + # bounded local sentinel records it only when explicitly enabled, so the + # guard above can be proven to short-circuit before this point. + record_business_dispatch_attempt(String(request.capability)) + 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 @@ -41,7 +41,11 @@ from hyf_stdio.control.capabilities import build_capabilities_output from hyf_stdio.codec import decode_request, encode_error, encode_success from hyf_stdio.envelope import WireErrorResponse, WireSuccessResponse from hyf_stdio.errors import WireError -from hyf_runtime.startup import RuntimeStartupInput, resolve_startup_context +from hyf_runtime.startup import ( + RuntimeStartupContext, + RuntimeStartupInput, + resolve_startup_context, +) from hyf_stdio.server import ( handle_request_line_with_runtime_context, ) @@ -1916,3 +1920,331 @@ def test_c004_operation_v2_whitespace_only_identity_is_rejected() raises: '"model":"m"},"actor_id":"farm-1","farm_id":"farm-1"' ) assert_true(_decode_error_message(blank_version).find("blank") >= 0) + + +# ADR-0026 D46 CR04: unambiguous duplicate admission, safe correlation and an +# executed zero-dispatch sentinel for all three corrected operations. +from hyf_stdio.dispatch_sentinel import hyf_dispatch_sentinel_env_name + + +def _v2_farm_request_minimal(request_id: String) -> String: + return ( + '{"version":1,"request_id":"' + + request_id + + '","capability":"farm_update.interpret",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"},"input":{}}' + ) + + +def _v2_buyer_request_minimal(capability: String, request_id: String) -> String: + return ( + '{"version":1,"request_id":"' + + request_id + + '","capability":"' + + capability + + '","context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"buyer-7"},"input":{}}' + ) + + +def _temp_runtime_context(temp_dir: String) raises -> RuntimeStartupContext: + return resolve_startup_context( + RuntimeStartupInput( + env_paths_profile="repo_local", + env_repo_local_base_root=temp_dir, + user_home="/home/unused", + argv=List[String](), + ) + ) + + +def test_c004_cr04_duplicate_capability_cannot_hide_corrected_operation() raises: + # Corrected capability first, legacy capability second. + var v2_first = ( + '{"version":1,"request_id":"dup-cap-a",' + '"capability":"farm_update.interpret","capability":"query_rewrite",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"},"input":{}}' + ) + assert_true(_decode_error_message(v2_first).find("duplicate") >= 0) + + # Legacy capability first, corrected capability second: the corrected value + # must still make the envelope v2-targeting and therefore ambiguous. + var v2_second = ( + '{"version":1,"request_id":"dup-cap-b",' + '"capability":"query_rewrite","capability":"buyer_request.match",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"buyer-7"},"input":{}}' + ) + assert_true(_decode_error_message(v2_second).find("duplicate") >= 0) + + +def test_c004_cr04_duplicate_context_cannot_hide_v2_selector() raises: + # Legacy context first, v2 context second. + var legacy_first = ( + '{"version":1,"request_id":"dup-ctx-a",' + '"capability":"farm_update.interpret",' + '"context":{"consumer":"cli"},' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"},"input":{}}' + ) + assert_true(_decode_error_message(legacy_first).find("duplicate") >= 0) + + # v2 context first, legacy context second. + var v2_first = ( + '{"version":1,"request_id":"dup-ctx-b",' + '"capability":"buyer_request.interpret",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"buyer-7"},"context":{"consumer":"cli"},"input":{}}' + ) + assert_true(_decode_error_message(v2_first).find("duplicate") >= 0) + + +def test_c004_cr04_duplicate_correlation_and_equal_values_are_rejected() raises: + var duplicate_request_id = ( + '{"version":1,"request_id":"dup-rid-a","request_id":"dup-rid-b",' + '"capability":"farm_update.interpret",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"},"input":{}}' + ) + assert_true( + _decode_error_message(duplicate_request_id).find("duplicate") >= 0 + ) + + # Equal values are still ambiguous duplicates. + var equal_values = ( + '{"version":1,"request_id":"dup-eq-a","request_id":"dup-eq-a",' + '"capability":"buyer_request.match",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"buyer-7"},"input":{}}' + ) + assert_true(_decode_error_message(equal_values).find("duplicate") >= 0) + + # Escaped equivalent of `capability` is a decoded-key duplicate. + var escaped_capability = ( + '{"version":1,"request_id":"dup-esc",' + '"capability":"farm_update.interpret","\\u0063apability":"query_rewrite",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"},"input":{}}' + ) + assert_true( + _decode_error_message(escaped_capability).find("duplicate") >= 0 + ) + + +def test_c004_cr04_unrelated_legacy_duplicate_keeps_existing_admission() raises: + # An unrelated legacy capability is not v2-targeting, so the new duplicate + # gate deliberately does not apply and the existing legacy admission is + # unchanged (first-wins envelope parse proceeds). + var legacy_duplicate = ( + '{"version":1,"request_id":"legacy-dup",' + '"capability":"query_rewrite","capability":"query_rewrite",' + '"input":{"query":"eggs"}}' + ) + var request = decode_request(legacy_duplicate) + assert_true(not request.operation_context) + assert_equal(request.capability, "query_rewrite") + + +def test_c004_cr04_ambiguous_duplicate_correlation_is_untrusted() raises: + with SafeTempDir() as temp_dir: + var runtime_context = _temp_runtime_context(temp_dir) + var line = ( + '{"version":1,"request_id":"dup-cor-a","request_id":"dup-cor-b",' + '"trace_id":"dup-trace-a","trace_id":"dup-trace-b",' + '"capability":"farm_update.interpret",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"},"input":{}}' + ) + var response = loads( + handle_request_line_with_runtime_context(line, runtime_context) + ) + assert_equal(response["ok"].bool_value(), False) + assert_equal( + response["error"]["code"].string_value(), "invalid_request" + ) + # Ambiguous duplicates are never first/last-wins correlation. + assert_equal(response["request_id"].string_value(), "") + assert_true(not _has_key(response, "trace_id")) + + # A single unambiguous correlation is still preserved on the same error. + var unambiguous = ( + '{"version":1,"request_id":"dup-cor-ok","trace_id":"trace-ok",' + '"capability":"farm_update.interpret",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1"},"input":{}}' + ) + var preserved = loads( + handle_request_line_with_runtime_context( + unambiguous, runtime_context + ) + ) + assert_equal(preserved["ok"].bool_value(), False) + assert_equal(preserved["request_id"].string_value(), "dup-cor-ok") + assert_equal(preserved["trace_id"].string_value(), "trace-ok") + + +def test_c004_cr04_context_admission_is_bounded_and_linear() raises: + # A large repeated allowed key is one decoded-key duplicate and is rejected + # by the linear seen-key scan rather than an all-pairs comparison. + var repeated = List[String]() + for _ in range(256): + repeated.append('"actor_id":"farm-1"') + var many_duplicates = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + "," + + ",".join(repeated) + + ',"farm_id":"farm-1"' + ) + assert_true(_decode_error_message(many_duplicates).find("duplicate") >= 0) + + # An unknown key is still rejected by the fixed allowed-key set. + var unknown_and_dup = _v2_farm_request_with_context( + '"evaluation_time":"2026-09-24T09:00:00-07:00","planner":"strict",' + + _v2_versions_json() + + ',"actor_id":"farm-1","actor_id":"farm-2","farm_id":"farm-1"' + ) + var message = _decode_error_message(unknown_and_dup) + assert_true( + message.find("duplicate") >= 0 or message.find("unexpected") >= 0 + ) + + +def test_c004_cr04_zero_dispatch_sentinel_executed_controls() raises: + with SafeTempDir() as temp_dir: + var sentinel_path = temp_dir + "/hyf-dispatch-sentinel.log" + var runtime_context = _temp_runtime_context(temp_dir) + runtime_context.config.effective.runtime.enable_farm_update_interpret = ( + True + ) + runtime_context.config.effective.runtime.enable_buyer_request_interpret = ( + True + ) + runtime_context.config.effective.runtime.enable_buyer_request_match = ( + True + ) + with ScopedEnvVar(hyf_dispatch_sentinel_env_name(), sentinel_path): + # Negative controls: every recognized v2 request for all three + # corrected operations returns capability_unavailable and performs + # zero dispatch, including with the legacy flags enabled. + var v2_requests = List[String]() + v2_requests.append(_v2_farm_request_minimal("sentry-farm")) + v2_requests.append( + _v2_buyer_request_minimal( + "buyer_request.interpret", "sentry-interpret" + ) + ) + v2_requests.append( + _v2_buyer_request_minimal("buyer_request.match", "sentry-match") + ) + for line in v2_requests: + assert_true( + _operation_enabled( + runtime_context.config, + loads(line)["capability"].string_value(), + ) + ) + var response = loads( + handle_request_line_with_runtime_context( + line, runtime_context + ) + ) + assert_equal(response["ok"].bool_value(), False) + assert_equal( + response["error"]["code"].string_value(), + "capability_unavailable", + ) + assert_true( + not exists(sentinel_path), + ) + + # Positive control: an actual legacy dispatch does record a line, + # proving the sentinel observes dispatch rather than nothing. + var legacy = ( + '{"version":1,"request_id":"legacy-sentry",' + '"capability":"farm_update.interpret","input":{' + + _v2_farm_source_json() + + "}}" + ) + var legacy_response = loads( + handle_request_line_with_runtime_context( + legacy, runtime_context + ) + ) + assert_equal(legacy_response["ok"].bool_value(), True) + assert_true(exists(sentinel_path)) + var recorded = Path(sentinel_path).read_text() + assert_true(recorded.find("dispatch farm_update.interpret") >= 0) + + +def test_c004_cr04_large_unknown_key_context_is_bounded() raises: + # ADR-0026 D46 CR04 asks for large unknown-key contexts and field-count + # edge cases: a 128-key unknown context must be rejected by the fixed + # allowed-key set without an unbounded scan. + var unknown = List[String]() + for index in range(128): + unknown.append('"unknown_' + String(index) + '":"x"') + var large_unknown = _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",' + + ",".join(unknown) + ) + assert_true(_decode_error_message(large_unknown).find("unexpected") >= 0) + + # A duplicated unknown key is rejected by the bounded seen-key scan. + var duplicate_unknown = _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",' + + '"unknown_x":"a","unknown_x":"b"' + ) + var message = _decode_error_message(duplicate_unknown) + assert_true( + message.find("duplicate") >= 0 or message.find("unexpected") >= 0 + ) + + +def test_c004_cr04_duplicate_version_input_and_trace_fields() raises: + # CR04 requires every duplicate correlation/envelope field to fail, not only + # capability/context: version, input, trace_id and request_id. + var base = ( + '{"version":1,"trace_id":"t1","request_id":"dup-fields",' + '"capability":"farm_update.interpret",' + '"context":{"evaluation_time":"2026-09-24T09:00:00-07:00",' + + _v2_versions_json() + + ',"actor_id":"farm-1","farm_id":"farm-1"},"input":{}}' + ) + var duplicate_trace = base.replace( + '"trace_id":"t1"', '"trace_id":"t1","trace_id":"t2"' + ) + assert_true(_decode_error_message(duplicate_trace).find("duplicate") >= 0) + + var duplicate_version = base.replace( + '"version":1', '"version":1,"version":1' + ) + assert_true(_decode_error_message(duplicate_version).find("duplicate") >= 0) + + var duplicate_input = base.replace('"input":{}', '"input":{},"input":{}') + assert_true(_decode_error_message(duplicate_input).find("duplicate") >= 0) + + var duplicate_request_id = base.replace( + '"request_id":"dup-fields"', + '"request_id":"dup-fields","request_id":"dup-fields-2"', + ) + assert_true( + _decode_error_message(duplicate_request_id).find("duplicate") >= 0 + )