From eef58b9ed607ac2251adb87fbb44e57322ced349 Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Sat, 19 Sep 2026 13:02:31 -0700 Subject: [PATCH 1/7] spec: define Hyperlex Q1 epistemic qualification --- .../008-epistemic-interchange/HYPERLEX-Q1.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 specs/008-epistemic-interchange/HYPERLEX-Q1.md diff --git a/specs/008-epistemic-interchange/HYPERLEX-Q1.md b/specs/008-epistemic-interchange/HYPERLEX-Q1.md new file mode 100644 index 00000000..2bf1295e --- /dev/null +++ b/specs/008-epistemic-interchange/HYPERLEX-Q1.md @@ -0,0 +1,132 @@ +# Hyperlex EIC Qualification — HYPERLEX-Q1 + +Status: `CANON-SHADOW` +Version: `0.1.0` +Upstream reference: Noesis `specs/EPISTEMIC-INTERCHANGE.md` + +## Purpose + +Qualify the canonical Hyperlex work/train repository as a deterministic transformation producer for controlled epistemic-interchange testing. + +Passing HYPERLEX-Q1 means Hyperlex can describe what it transformed, what it intended to preserve/change, and how the output was produced without claiming that the declared invariant actually survived in a model representation. + +It does **not** validate Hyperlexical, EXP-001, latent semantics, or ecosystem readiness. + +## Repository authority + +`scrimshawlife-ctrl/Hyperlex` is the canonical train/work remote. `Zero-State-LLC/Hyperlex` is a company mirror and may lag. Qualification evidence MUST bind the canonical work remote revision. + +## Doctrine + +1. Hyperlex owns transformation generation and transformation provenance. +2. Hyperlex may declare `semantic_intent`, `expected_invariants`, and `expected_changed_attributes` as experiment inputs. +3. These declarations are hypotheses, not observations of latent truth. +4. Hyperlex MUST NOT emit `CALIBRATED` or `SETTLED` merely because a transform completed. +5. A deterministic transform MUST bind source hash, output hash, transform identity/revision, parameters, and runtime/model identity when applicable. +6. Contamination, fallback, nondeterminism, missing model binding, or unresolved provenance MUST be explicit. +7. Missing required evidence yields `NOT_COMPUTABLE` or a typed rejection. +8. Existing Hyperlex provenance semantics remain valid: `OBSERVED`, `INFERRED`, `SPECULATIVE`, `NOT_COMPUTABLE`. +9. Brier/calibration behavior remains governed by existing settlement rules; HYPERLEX-Q1 does not create Brier evidence. + +## Journey + +### J-HQ1-001 — Export a controlled transform + +An experiment runner provides a source fixture and registered transform request. Hyperlex produces an output plus sufficient provenance for Noesis to test the declared invariant independently. + +## Workflow + +### WF-HQ1-001 — Produce EIC transform fixture + +**Actors:** experiment runner, Hyperlex transform adapter. +**Trigger:** registered controlled-transform request. +**Preconditions:** source bytes/text hashable; transform identity known; required runtime/model binding known or explicitly unavailable. +**Inputs:** source, transform class, parameters, declared semantic intent, expected invariants, expected changed attributes. + +**Happy path:** +1. Hash source. +2. Resolve transform implementation and revision. +3. Freeze parameters/seed where applicable. +4. Execute transform. +5. Hash output. +6. Record model/runtime revision where applicable. +7. Record declared invariants and changed attributes as declarations. +8. Record contamination/fallback/nondeterminism metadata. +9. Emit transport fixture. + +**Failure paths:** +- missing source identity -> reject; +- missing required transform revision -> `NOT_COMPUTABLE`; +- nondeterministic path without declared seed/envelope -> `NOT_COMPUTABLE`; +- fallback route -> explicit fallback provenance; +- contamination detected -> emit contaminated result, never silently clean it; +- transform provider attempts to assert latent truth -> reject contract. + +**Terminal states:** `PRODUCED`, `NOT_COMPUTABLE`, `REJECTED`. + +**Invariant:** successful generation does not prove semantic preservation. + +## Qualification fixture classes + +HYPERLEX-Q1 MUST test: + +1. deterministic identity transform; +2. deterministic surface rewrite with declared invariant; +3. changed-meaning negative control; +4. false invariance declaration; +5. missing model/runtime binding when binding is required; +6. contaminated source/output; +7. stale transform revision; +8. repeated execution with same registered inputs; +9. fallback execution; +10. malformed or absent provenance. + +## Acceptance — AC-HQ1 + +PASS requires all: + +- source/output hashes are stable for deterministic fixtures; +- transform identity/revision and parameters are explicit; +- same deterministic request reproduces the registered output hash; +- declared invariants remain declarations, not observations; +- false-invariance fixture is exportable as a hypothesis but cannot become latent truth; +- contamination and fallback remain visible; +- missing required binding fails closed; +- no HYPERLEX-Q1 output silently gains `CALIBRATED` or `SETTLED`; +- evidence receipt binds canonical repository commit and fixture hashes; +- automated tests pass; +- independent review is recorded or qualification remains provisional. + +## Tasks + +### T-HQ1-001 — Define transform export schema +Add a versioned Q1 transport schema compatible with the Noesis EIC boundary. + +### T-HQ1-002 — Build deterministic fixtures +Implement the ten qualification fixture classes. + +### T-HQ1-003 — Build validator +Reject latent-truth promotion, missing required provenance, and undeclared nondeterminism. + +### T-HQ1-004 — Qualification receipt +Bind commit, CI runs, fixture hashes, failures, limitations, reviewer, and UTC decision. + +## Verification + +- V-HQ1-001 schema validation; +- V-HQ1-002 repeat-output hash stability; +- V-HQ1-003 false-invariance declaration remains hypothesis-only; +- V-HQ1-004 contamination survives serialization; +- V-HQ1-005 fallback survives serialization; +- V-HQ1-006 missing binding fails closed; +- V-HQ1-007 stale revision rejected; +- V-HQ1-008 no epistemic promotion; +- V-HQ1-009 canonical remote/revision bound in receipt. + +## Downstream gate + +`Hyperlex -> Noesis` pairwise conformance remains BLOCKED until both: +- NOESIS-Q1 has qualifying evidence; and +- HYPERLEX-Q1 has qualifying evidence. + +Even after both pass, pairwise conformance tests the boundary only. It does not authorize ecosystem-wide EXP-001. From f08edff137501bbba48690df4f7aa20085c28fa4 Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Sat, 19 Sep 2026 13:04:16 -0700 Subject: [PATCH 2/7] feat: add HYPERLEX-Q1 transform contract --- .../hyperlex-q1-transform-result.schema.json | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 schemas/hyperlex-q1-transform-result.schema.json diff --git a/schemas/hyperlex-q1-transform-result.schema.json b/schemas/hyperlex-q1-transform-result.schema.json new file mode 100644 index 00000000..dcd421a6 --- /dev/null +++ b/schemas/hyperlex-q1-transform-result.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://zerostate.dev/schemas/hyperlex-q1-transform-result.schema.json", + "title": "HyperlexQ1TransformResult", + "type": "object", + "additionalProperties": false, + "required": ["schema_version","result_id","status","source_hash","transform","declared_invariants","expected_changed_attributes","provenance","epistemic_status"], + "properties": { + "schema_version": {"const":"0.1.0"}, + "result_id": {"type":"string","minLength":1}, + "status": {"enum":["PRODUCED","NOT_COMPUTABLE","REJECTED"]}, + "source_hash": {"type":"string","pattern":"^sha256:[0-9a-f]{64}$"}, + "output_hash": {"type":["string","null"],"pattern":"^sha256:[0-9a-f]{64}$"}, + "transform": { + "type":"object","additionalProperties":false, + "required":["transform_id","revision","parameters","deterministic"], + "properties":{ + "transform_id":{"type":"string","minLength":1}, + "revision":{"type":"string","minLength":1}, + "parameters":{"type":"object"}, + "deterministic":{"type":"boolean"}, + "seed":{"type":["integer","string","null"]} + } + }, + "declared_semantic_intent":{"type":["string","null"]}, + "declared_invariants":{"type":"array","items":{"type":"string","minLength":1}}, + "expected_changed_attributes":{"type":"array","items":{"type":"string","minLength":1}}, + "runtime_binding":{"type":["object","null"],"additionalProperties":true}, + "contamination":{"type":"array","items":{"type":"string","minLength":1}}, + "fallback":{"type":["object","null"],"additionalProperties":true}, + "provenance":{"enum":["OBSERVED","INFERRED","SPECULATIVE","NOT_COMPUTABLE"]}, + "epistemic_status":{"enum":["OBSERVED","NOT_COMPUTABLE"]}, + "limitations":{"type":"array","items":{"type":"string"}} + }, + "allOf":[ + {"if":{"properties":{"status":{"const":"PRODUCED"}}},"then":{"required":["output_hash"]}}, + {"if":{"properties":{"status":{"const":"NOT_COMPUTABLE"}}},"then":{"properties":{"epistemic_status":{"const":"NOT_COMPUTABLE"},"provenance":{"const":"NOT_COMPUTABLE"}}}} + ] +} From 958e7f89ca33e13eccf9eed0e434d6e590cce4ab Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Sat, 19 Sep 2026 13:04:17 -0700 Subject: [PATCH 3/7] feat: implement HYPERLEX-Q1 export boundary --- src/hyperlex/eic.py | 93 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/hyperlex/eic.py diff --git a/src/hyperlex/eic.py b/src/hyperlex/eic.py new file mode 100644 index 00000000..c83858f8 --- /dev/null +++ b/src/hyperlex/eic.py @@ -0,0 +1,93 @@ +"""HYPERLEX-Q1 controlled-transform export boundary. + +This module records transform provenance. It does not determine whether a +declared semantic invariant survived in a model representation. +""" +from __future__ import annotations + +import hashlib +import json +from copy import deepcopy +from pathlib import Path +from typing import Any + +_ALLOWED_EPISTEMIC = {"OBSERVED", "NOT_COMPUTABLE"} + + +def sha256_text(text: str) -> str: + return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest() + + +def canonical_json(value: Any) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + + +def validate_q1_result(payload: dict[str, Any], schema_path: str | Path | None = None) -> None: + try: + from jsonschema import Draft202012Validator, FormatChecker + except ImportError as exc: + raise RuntimeError("jsonschema is required for HYPERLEX-Q1 validation") from exc + + path = Path(schema_path) if schema_path else Path(__file__).resolve().parents[3] / "schemas" / "hyperlex-q1-transform-result.schema.json" + schema = json.loads(path.read_text(encoding="utf-8")) + errors = sorted( + Draft202012Validator(schema, format_checker=FormatChecker()).iter_errors(payload), + key=lambda error: list(error.path), + ) + if errors: + raise ValueError("HYPERLEX-Q1 contract violation: " + "; ".join(e.message for e in errors)) + + if payload["epistemic_status"] not in _ALLOWED_EPISTEMIC: + raise ValueError("Hyperlex Q1 cannot promote transform output beyond OBSERVED/NOT_COMPUTABLE") + transform = payload["transform"] + if transform["deterministic"] and transform.get("seed") is None and transform["parameters"].get("requires_seed"): + raise ValueError("declared deterministic seeded transform is missing seed") + if payload["status"] == "PRODUCED" and payload.get("runtime_binding") is None and transform["parameters"].get("requires_runtime_binding"): + raise ValueError("required runtime/model binding is missing") + + +def build_q1_result( + *, + result_id: str, + source: str, + output: str | None, + transform_id: str, + revision: str, + parameters: dict[str, Any] | None = None, + deterministic: bool = True, + seed: int | str | None = None, + declared_semantic_intent: str | None = None, + declared_invariants: list[str] | None = None, + expected_changed_attributes: list[str] | None = None, + runtime_binding: dict[str, Any] | None = None, + contamination: list[str] | None = None, + fallback: dict[str, Any] | None = None, + not_computable_reason: str | None = None, +) -> dict[str, Any]: + parameters = deepcopy(parameters or {}) + not_computable = not_computable_reason is not None + payload = { + "schema_version": "0.1.0", + "result_id": result_id, + "status": "NOT_COMPUTABLE" if not_computable else "PRODUCED", + "source_hash": sha256_text(source), + "output_hash": None if not_computable or output is None else sha256_text(output), + "transform": { + "transform_id": transform_id, + "revision": revision, + "parameters": parameters, + "deterministic": deterministic, + "seed": seed, + }, + "declared_semantic_intent": declared_semantic_intent, + "declared_invariants": list(declared_invariants or []), + "expected_changed_attributes": list(expected_changed_attributes or []), + "runtime_binding": deepcopy(runtime_binding), + "contamination": list(contamination or []), + "fallback": deepcopy(fallback), + "provenance": "NOT_COMPUTABLE" if not_computable else "OBSERVED", + "epistemic_status": "NOT_COMPUTABLE" if not_computable else "OBSERVED", + "limitations": [not_computable_reason] if not_computable else [], + } + validate_q1_result(payload) + return payload From b16bf5f40838c3903e69e77e92ed3a6967dd351c Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Sat, 19 Sep 2026 13:04:19 -0700 Subject: [PATCH 4/7] test: add HYPERLEX-Q1 adversarial suite --- tests/test_eic_q1.py | 92 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/test_eic_q1.py diff --git a/tests/test_eic_q1.py b/tests/test_eic_q1.py new file mode 100644 index 00000000..d04d82cd --- /dev/null +++ b/tests/test_eic_q1.py @@ -0,0 +1,92 @@ +import copy + +import pytest + +from hyperlex.eic import build_q1_result, sha256_text, validate_q1_result + + +def base(**overrides): + kwargs = dict( + result_id="hq1-identity-001", + source="hello world", + output="hello world", + transform_id="identity", + revision="test-v1", + parameters={}, + declared_semantic_intent="preserve all content", + declared_invariants=["meaning"], + expected_changed_attributes=[], + runtime_binding={"kind": "pure-python", "revision": "test"}, + ) + kwargs.update(overrides) + return build_q1_result(**kwargs) + + +def test_identity_transform_is_hash_stable(): + one = base() + two = base() + assert one["source_hash"] == sha256_text("hello world") + assert one["output_hash"] == two["output_hash"] == one["source_hash"] + + +def test_surface_rewrite_keeps_invariant_as_declaration_only(): + result = base( + output="hello, world!", + transform_id="punctuation-rewrite", + declared_invariants=["declared-semantic-intent"], + expected_changed_attributes=["punctuation"], + ) + assert result["epistemic_status"] == "OBSERVED" + assert result["declared_invariants"] == ["declared-semantic-intent"] + + +def test_false_invariance_declaration_does_not_become_truth(): + result = base( + output="goodbye world", + transform_id="semantic-negative-control", + declared_invariants=["meaning"], + expected_changed_attributes=["lexical-form"], + ) + assert result["epistemic_status"] == "OBSERVED" + assert "meaning" in result["declared_invariants"] + + +def test_missing_required_runtime_binding_fails_closed(): + with pytest.raises(ValueError, match="runtime/model binding"): + base(runtime_binding=None, parameters={"requires_runtime_binding": True}) + + +def test_missing_required_seed_fails_closed(): + with pytest.raises(ValueError, match="missing seed"): + base(parameters={"requires_seed": True}, seed=None) + + +def test_contamination_and_fallback_survive_serialization(): + result = base( + contamination=["fixture-seen-during-training:unknown"], + fallback={"used": True, "route": "mock"}, + ) + assert result["contamination"] == ["fixture-seen-during-training:unknown"] + assert result["fallback"]["route"] == "mock" + + +def test_not_computable_is_explicit(): + result = base(output=None, not_computable_reason="model binding unavailable") + assert result["status"] == "NOT_COMPUTABLE" + assert result["epistemic_status"] == "NOT_COMPUTABLE" + assert result["provenance"] == "NOT_COMPUTABLE" + + +def test_calibrated_or_settled_epistemic_status_is_rejected(): + result = base() + for status in ("CALIBRATED", "SETTLED", "INTERPRETED"): + invalid = copy.deepcopy(result) + invalid["epistemic_status"] = status + with pytest.raises(ValueError): + validate_q1_result(invalid) + + +def test_changed_revision_changes_declared_transform_identity(): + one = base(revision="v1") + two = base(revision="v2") + assert one["transform"]["revision"] != two["transform"]["revision"] From 5e89bfbcaaa47fba58c04ddf15d5bbe12b80db5f Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Sat, 19 Sep 2026 13:05:38 -0700 Subject: [PATCH 5/7] fix: resolve Q1 schema from repository root --- src/hyperlex/eic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperlex/eic.py b/src/hyperlex/eic.py index c83858f8..0f687b46 100644 --- a/src/hyperlex/eic.py +++ b/src/hyperlex/eic.py @@ -28,7 +28,7 @@ def validate_q1_result(payload: dict[str, Any], schema_path: str | Path | None = except ImportError as exc: raise RuntimeError("jsonschema is required for HYPERLEX-Q1 validation") from exc - path = Path(schema_path) if schema_path else Path(__file__).resolve().parents[3] / "schemas" / "hyperlex-q1-transform-result.schema.json" + path = Path(schema_path) if schema_path else Path(__file__).resolve().parents[2] / "schemas" / "hyperlex-q1-transform-result.schema.json" schema = json.loads(path.read_text(encoding="utf-8")) errors = sorted( Draft202012Validator(schema, format_checker=FormatChecker()).iter_errors(payload), From e322ad02cb2516e317f11237167f9dd21bfbe810 Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Sat, 19 Sep 2026 16:02:06 -0700 Subject: [PATCH 6/7] evidence: record HYPERLEX-Q1 implementation state as UNQUALIFIED --- .../HYPERLEX-Q1-2026-09-19-001.json | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 evidence/qualifications/HYPERLEX-Q1-2026-09-19-001.json diff --git a/evidence/qualifications/HYPERLEX-Q1-2026-09-19-001.json b/evidence/qualifications/HYPERLEX-Q1-2026-09-19-001.json new file mode 100644 index 00000000..bfc79071 --- /dev/null +++ b/evidence/qualifications/HYPERLEX-Q1-2026-09-19-001.json @@ -0,0 +1,40 @@ +{ + "receipt_id": "HYPERLEX-Q1-2026-09-19-001", + "qualification": "HYPERLEX-Q1", + "decision": "UNQUALIFIED", + "scope": "controlled transform-export fixtures only", + "repository": "scrimshawlife-ctrl/Hyperlex", + "implementation_revision": "5e89bfbcaaa47fba58c04ddf15d5bbe12b80db5f", + "issued_utc": "2026-09-19T23:05:00Z", + "reviewer": { + "class": "SHADOW_ADVISORY", + "actor": "Abraxas Orchestrator / Grok", + "independent": false + }, + "evidence": [ + {"kind": "github_workflow", "name": "Hyperlex Skill Validation", "run_id": 35466262293, "run_number": 566, "head_sha": "b16bf5f40838c3903e69e77e92ed3a6967dd351c", "conclusion": "failure", "classification": "path-resolution defect; retained"}, + {"kind": "github_workflow", "name": "Hyperlex Skill Validation", "run_id": 35466333294, "run_number": 567, "head_sha": "5e89bfbcaaa47fba58c04ddf15d5bbe12b80db5f", "conclusion": "success"} + ], + "verified_on_head": [ + "schema path resolution from repository root", + "deterministic source/output hashing", + "epistemic_status restricted to OBSERVED or NOT_COMPUTABLE", + "missing required runtime binding fails closed", + "missing required seed fails closed", + "contamination and fallback survive serialization" + ], + "limitations": [ + "AC-HQ1 not met", + "No independent reviewer", + "Stale-revision fixture currently asserts identity change, not rejection", + "No fixture-hash ledger in this receipt", + "Does not assign latent truth", + "Does not unlock Hyperlex -> Noesis pairwise", + "Does not authorize EXP-001" + ], + "next_permitted_work": [ + "Harden stale-revision and contamination fixture classes to AC-HQ1", + "Issue a later receipt only after independent review or explicit remain-provisional operator decision", + "Pairwise remains BLOCKED" + ] +} From dcc5d79e2efdb2ee88a1a94741f107f0050b77f6 Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Sat, 19 Sep 2026 16:11:49 -0700 Subject: [PATCH 7/7] fix: reject stale transform revision against registered set --- specs/008-epistemic-interchange/HYPERLEX-Q1.md | 7 +++++-- src/hyperlex/eic.py | 11 +++++++++-- tests/test_eic_q1.py | 12 ++++++++---- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/specs/008-epistemic-interchange/HYPERLEX-Q1.md b/specs/008-epistemic-interchange/HYPERLEX-Q1.md index 2bf1295e..8e20c109 100644 --- a/specs/008-epistemic-interchange/HYPERLEX-Q1.md +++ b/specs/008-epistemic-interchange/HYPERLEX-Q1.md @@ -27,6 +27,7 @@ It does **not** validate Hyperlexical, EXP-001, latent semantics, or ecosystem r 7. Missing required evidence yields `NOT_COMPUTABLE` or a typed rejection. 8. Existing Hyperlex provenance semantics remain valid: `OBSERVED`, `INFERRED`, `SPECULATIVE`, `NOT_COMPUTABLE`. 9. Brier/calibration behavior remains governed by existing settlement rules; HYPERLEX-Q1 does not create Brier evidence. +10. When `parameters.accepted_revisions` is supplied, a transform revision absent from that list is stale and MUST be rejected. Absence of the list does not invent a global registry. ## Journey @@ -57,6 +58,7 @@ An experiment runner provides a source fixture and registered transform request. **Failure paths:** - missing source identity -> reject; - missing required transform revision -> `NOT_COMPUTABLE`; +- stale revision against `accepted_revisions` -> reject; - nondeterministic path without declared seed/envelope -> `NOT_COMPUTABLE`; - fallback route -> explicit fallback provenance; - contamination detected -> emit contaminated result, never silently clean it; @@ -76,7 +78,7 @@ HYPERLEX-Q1 MUST test: 4. false invariance declaration; 5. missing model/runtime binding when binding is required; 6. contaminated source/output; -7. stale transform revision; +7. stale transform revision rejected against a registered accepted-revision set; 8. repeated execution with same registered inputs; 9. fallback execution; 10. malformed or absent provenance. @@ -92,6 +94,7 @@ PASS requires all: - false-invariance fixture is exportable as a hypothesis but cannot become latent truth; - contamination and fallback remain visible; - missing required binding fails closed; +- stale revision against `accepted_revisions` is rejected; - no HYPERLEX-Q1 output silently gains `CALIBRATED` or `SETTLED`; - evidence receipt binds canonical repository commit and fixture hashes; - automated tests pass; @@ -106,7 +109,7 @@ Add a versioned Q1 transport schema compatible with the Noesis EIC boundary. Implement the ten qualification fixture classes. ### T-HQ1-003 — Build validator -Reject latent-truth promotion, missing required provenance, and undeclared nondeterminism. +Reject latent-truth promotion, missing required provenance, undeclared nondeterminism, and stale revision against a registered set. ### T-HQ1-004 — Qualification receipt Bind commit, CI runs, fixture hashes, failures, limitations, reviewer, and UTC decision. diff --git a/src/hyperlex/eic.py b/src/hyperlex/eic.py index 0f687b46..816520e9 100644 --- a/src/hyperlex/eic.py +++ b/src/hyperlex/eic.py @@ -40,10 +40,17 @@ def validate_q1_result(payload: dict[str, Any], schema_path: str | Path | None = if payload["epistemic_status"] not in _ALLOWED_EPISTEMIC: raise ValueError("Hyperlex Q1 cannot promote transform output beyond OBSERVED/NOT_COMPUTABLE") transform = payload["transform"] - if transform["deterministic"] and transform.get("seed") is None and transform["parameters"].get("requires_seed"): + parameters = transform.get("parameters") or {} + if transform["deterministic"] and transform.get("seed") is None and parameters.get("requires_seed"): raise ValueError("declared deterministic seeded transform is missing seed") - if payload["status"] == "PRODUCED" and payload.get("runtime_binding") is None and transform["parameters"].get("requires_runtime_binding"): + if payload["status"] == "PRODUCED" and payload.get("runtime_binding") is None and parameters.get("requires_runtime_binding"): raise ValueError("required runtime/model binding is missing") + accepted = parameters.get("accepted_revisions") + if accepted is not None: + if not isinstance(accepted, list) or not accepted: + raise ValueError("accepted_revisions must be a non-empty list when supplied") + if transform["revision"] not in accepted: + raise ValueError("stale transform revision") def build_q1_result( diff --git a/tests/test_eic_q1.py b/tests/test_eic_q1.py index d04d82cd..5c860316 100644 --- a/tests/test_eic_q1.py +++ b/tests/test_eic_q1.py @@ -86,7 +86,11 @@ def test_calibrated_or_settled_epistemic_status_is_rejected(): validate_q1_result(invalid) -def test_changed_revision_changes_declared_transform_identity(): - one = base(revision="v1") - two = base(revision="v2") - assert one["transform"]["revision"] != two["transform"]["revision"] +def test_registered_revision_is_accepted(): + result = base(revision="v2", parameters={"accepted_revisions": ["v2", "v3"]}) + assert result["transform"]["revision"] == "v2" + + +def test_stale_revision_is_rejected(): + with pytest.raises(ValueError, match="stale transform revision"): + base(revision="v1", parameters={"accepted_revisions": ["v2", "v3"]})