diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2fbdbc..28d7888 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,13 +53,13 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: fruwehq/determa-state-conformance - ref: affe3fe3bcc4d13fa7c5374471568e94af36f0d1 + ref: c6949440f5b9fd6e06ea5571f4e01681dd59ee04 path: .pinned/determa-state-conformance - name: Check out pinned specification uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: fruwehq/determa-state-spec - ref: 86bb88dd21cb1f799eefe5020b6e49dabf6e7225 + ref: 77c0a2e60cd0771a6d44ae170a079ddd51d7d9f0 path: .pinned/determa-state-spec - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: @@ -88,13 +88,13 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: fruwehq/determa-state-conformance - ref: affe3fe3bcc4d13fa7c5374471568e94af36f0d1 + ref: c6949440f5b9fd6e06ea5571f4e01681dd59ee04 path: .pinned/determa-state-conformance - name: Check out pinned specification uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: fruwehq/determa-state-spec - ref: 86bb88dd21cb1f799eefe5020b6e49dabf6e7225 + ref: 77c0a2e60cd0771a6d44ae170a079ddd51d7d9f0 path: .pinned/determa-state-spec - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: @@ -126,13 +126,13 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: fruwehq/determa-state-conformance - ref: affe3fe3bcc4d13fa7c5374471568e94af36f0d1 + ref: c6949440f5b9fd6e06ea5571f4e01681dd59ee04 path: .pinned/determa-state-conformance - name: Check out pinned specification uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: fruwehq/determa-state-spec - ref: 86bb88dd21cb1f799eefe5020b6e49dabf6e7225 + ref: 77c0a2e60cd0771a6d44ae170a079ddd51d7d9f0 path: .pinned/determa-state-spec - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: diff --git a/AGENTS.md b/AGENTS.md index cacab48..eba3e97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,11 +11,11 @@ package so it can coexist with the umbrella `determa` launcher. The implementation is conformant only when it passes the language-neutral suite. The implementation uses these immutable inputs: -- specification: `86bb88dd21cb1f799eefe5020b6e49dabf6e7225`; -- conformance: `affe3fe3bcc4d13fa7c5374471568e94af36f0d1` (99 format-1 +- specification: `77c0a2e60cd0771a6d44ae170a079ddd51d7d9f0`; +- conformance: `c6949440f5b9fd6e06ea5571f4e01681dd59ee04` (99 format-1 core cases, 162 version-1 vectors, 142 durable-host vectors, 49 core inspection vectors, 47 extension negotiation vectors, 7 optional native-provider vectors, - 30 runtime-provider vectors, and 484 JSON artifacts). + 52 runtime-provider vectors, and 487 JSON artifacts). The package metadata is `0.3.0`. diff --git a/README.md b/README.md index fa3e486..596bd4e 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ Python implementation of [Determa State](https://github.com/fruwehq/determa-stat a language-agnostic statechart engine with a shared normative conformance suite. This implementation supports Determa State `format: 1` at specification -commit `86bb88dd21cb1f799eefe5020b6e49dabf6e7225`. Correctness is determined by +commit `77c0a2e60cd0771a6d44ae170a079ddd51d7d9f0`. Correctness is determined by 99 format-1 core cases, 162 version-1 vectors, 142 durable-host vectors, 49 core inspection vectors, 47 extension negotiation vectors, 7 native guard-provider -inspection vectors, 30 runtime-provider vectors, and 484 manifest artifacts at -conformance commit `affe3fe3bcc4d13fa7c5374471568e94af36f0d1`. The native +inspection vectors, 52 runtime-provider vectors, and 487 manifest artifacts at +conformance commit `c6949440f5b9fd6e06ea5571f4e01681dd59ee04`. The native runtime-provider profile is optional and is exercised through its production adapter. The package metadata is `0.3.0`. Artifact and checkpoint schema version 1 is the @@ -329,7 +329,11 @@ a failed compare-and-swap; the engine does not retry it automatically. `compile_language_source(source, registry, manifest=manifest)` verifies the exact source and compiler closure, compiles disjoint regions in order, strictly loads the generated format-1 bundle, and checks the manifest fingerprint and proved source -capabilities. A generated CEL bundle restores without an installed compiler. +capabilities. Only executable grammar slots can be compiled; metadata and variable +values are inert. Every successful result retains sealed version-1 source and +manifest artifacts in `bundle.source_compilation`, including when no manifest was +supplied. That evidence discloses historical source guarantees independently of +the generated runtime profile. A generated CEL bundle restores without an installed compiler. Semantic inspection calls only a separately proved, bounded `inspect_guard` method; structural inspection never invokes a native provider. diff --git a/conformance/pins.py b/conformance/pins.py index c083a0c..0f9c49f 100644 --- a/conformance/pins.py +++ b/conformance/pins.py @@ -4,8 +4,8 @@ from pathlib import Path -CONFORMANCE_COMMIT = "affe3fe3bcc4d13fa7c5374471568e94af36f0d1" -SPEC_COMMIT = "86bb88dd21cb1f799eefe5020b6e49dabf6e7225" +CONFORMANCE_COMMIT = "c6949440f5b9fd6e06ea5571f4e01681dd59ee04" +SPEC_COMMIT = "77c0a2e60cd0771a6d44ae170a079ddd51d7d9f0" ROOT = Path(__file__).resolve().parent.parent CONFORMANCE_CACHE = ROOT / ".cache" / f"determa-state-conformance-{CONFORMANCE_COMMIT[:12]}" diff --git a/conformance/runtime_provider_adapter.py b/conformance/runtime_provider_adapter.py index 5d8cf90..ad32eaf 100644 --- a/conformance/runtime_provider_adapter.py +++ b/conformance/runtime_provider_adapter.py @@ -26,6 +26,7 @@ RuntimeProviderError, RuntimeProviderRegistry, SourceClosure, + ValidationError, admit, compile_language_source, create, @@ -62,7 +63,7 @@ def _empty() -> dict[str, Any]: "result": "rejected", "code": None, "value": None, - "calls": {"guard": 0, "actions": 0, "inspect_guard": 0, "external": 0}, + "calls": {"guard": 0, "actions": 0, "inspect_guard": 0, "compile_region": 0, "external": 0}, "external_effects": [], "irreversible_side_effects": 0, "determa_state_committed": False, @@ -112,19 +113,11 @@ def _installed( ) descriptors: list[dict[str, Any]] = [] - def scan(value: Any) -> None: - if type(value) is dict: - if type(value.get("guard")) is dict and "provider" in value["guard"]: - descriptors.append({"kind": "guard", "binding": value["guard"]["provider"]}) - if "provider_actions" in value: - descriptors.append({"kind": "actions", "binding": value["provider_actions"]}) - for child in value.values(): - scan(child) - elif type(value) is list: - for child in value: - scan(child) - - scan(bundle_document) + from determa.state.runtime_providers import _runtime_bindings + + descriptors = [ + {"kind": kind, "binding": binding} for kind, binding in _runtime_bindings(bundle_document) + ] names = ( {"example.native-common", "example.guard-compiler"} if compiler @@ -156,7 +149,9 @@ def scan(value: Any) -> None: module.compile_region, closure, capability_proof=lambda selected: ( - _PURE_CLAIMS if selected is module.compile_region else frozenset() + _PURE_CLAIMS + if selected is module.compile_region and not arguments.get("weak_compiler", False) + else frozenset() ), ) else: @@ -187,6 +182,13 @@ def scan(value: Any) -> None: "invalid": bool(arguments.get("invalid_output", False)), "fail": bool(arguments.get("action_fail", False)), "external_io": bool(arguments.get("action_external_io", False)), + **({"repeat_send": True} if arguments.get("repeat_send") else {}), + **({"mixed_send": True} if arguments.get("mixed_send") else {}), + **( + {"environment_send": arguments["environment_send"]} + if "environment_send" in arguments + else {} + ), } ), ) @@ -208,7 +210,11 @@ def _state(aggregate: dict[str, Any]) -> dict[str, Any]: item for item in aggregate["runtimes"] if item["runtime_id"] == aggregate["root_runtime_id"] ) return { - "active_leaf": runtime["active_leaf_state_definition_pointers"][-1], + "active_leaf": ( + runtime["active_leaf_state_definition_pointers"][-1] + if runtime["active_leaf_state_definition_pointers"] + else None + ), "status": runtime["status"], "variables": { item["variable_declaration_pointer"].split("/")[-1]: item["value"] @@ -220,6 +226,24 @@ def _state(aggregate: dict[str, Any]) -> dict[str, Any]: } +def _component_states(aggregate: dict[str, Any]) -> dict[str, Any]: + result = {} + for runtime in aggregate["runtimes"]: + component = runtime["target_identity"].get("component") + if component is not None: + observed = _state({**aggregate, "root_runtime_id": runtime["runtime_id"]}) + result[component["component_id"]] = { + key: observed[key] + for key in ( + "status", + "variables", + "ready_mailbox_length", + "deferred_mailbox_length", + ) + } + return result + + def _run_step( bundle: Any, registry: RuntimeProviderRegistry, @@ -251,7 +275,14 @@ def _run_step( observation["stages"].append("admit") observation["state_before"] = _state(aggregate) observation["stages"].append("evaluate_cel") - result = step(aggregate, setup["target_runtime_id"], resolver) + result = step( + aggregate, + setup["target_runtime_id"], + resolver, + _include_host_evidence=bool( + request["arguments"].get("repeat_send") or request["arguments"].get("mixed_send") + ), + ) _provider_counts(registry, observation) if observation["calls"]["guard"]: observation["stages"].append("evaluate_guard") @@ -263,8 +294,18 @@ def _run_step( observation["result"] = "faulted" observation["code"] = result["fault"]["code"] observation["state_after"] = observation["state_before"] - if request["arguments"].get("invalid_output"): + if "environment_send" in request["arguments"]: + observation["value"] = { + "source_locator": result["fault"]["source_locator"], + "component_states_before": _component_states(aggregate), + "component_states_after": _component_states(result["state"]), + } + if request["arguments"].get("invalid_output") or request["arguments"].get( + "destroyed_write" + ): observation["value"] = {"boundary_code": "runtime_provider_output_invalid"} + if request["arguments"].get("destroyed_write"): + observation["value"]["source_locator"] = result["fault"]["source_locator"] return observation["stages"].append("commit") observation["result"] = ( @@ -272,10 +313,111 @@ def _run_step( ) observation["determa_state_committed"] = True observation["state_after"] = _state(result["state"]) - observation["value"] = { - "accepted": observation["state_after"]["variables"]["accepted"], - "emissions": len(result["emissions"]), - } + observation["value"] = {"emissions": len(result["emissions"])} + if "environment_send" in request["arguments"]: + before_child = next( + item + for item in aggregate["runtimes"] + if item.get("target_identity", {}).get("component", {}).get("component_id") == "replica" + ) + after_child = next( + item + for item in result["state"]["runtimes"] + if item.get("target_identity", {}).get("component", {}).get("component_id") == "replica" + ) + internal = next( + item for item in result["emissions"] if item.get("kind") == "internal_mailbox" + ) + emission = next( + item["envelope"] + for item in after_child["ready_mailbox"] + if item["envelope"]["event_id"] == internal["event_id"] + ) + observation["value"].update( + forwarded_event={ + "event": emission["event"], + "component_id": after_child["target_identity"]["component"]["component_id"], + "payload": emission["payload"], + }, + component_variables_before=_state( + {**aggregate, "root_runtime_id": before_child["runtime_id"]} + )["variables"], + component_ready_before_delivery=len(after_child["ready_mailbox"]), + ) + delivered = step(result["state"], after_child["runtime_id"], resolver) + observation["stages"].append("deliver_env") + refreshed = next( + item + for item in delivered["state"]["runtimes"] + if item["runtime_id"] == after_child["runtime_id"] + ) + observation["value"].update( + component_variables_after=_state( + {**delivered["state"], "root_runtime_id": refreshed["runtime_id"]} + )["variables"], + component_ready_after_delivery=len(refreshed["ready_mailbox"]), + ) + if "accepted" in observation["state_after"]["variables"]: + observation["value"]["accepted"] = observation["state_after"]["variables"]["accepted"] + if observation["state_after"]["status"] == "completed": + observation["value"].update( + status="completed", exit_correlation=result["emissions"][-1]["correlation_id"] + ) + if request["arguments"].get("repeat_send") or request["arguments"].get("mixed_send"): + observation["value"]["emission_identities"] = [ + ( + { + "event_id": item["event_id"], + "emission_index": item["emission_index"], + "acceptance_sequence": item["acceptance_sequence"], + "queue_sequence": item["queue_sequence"], + } + if "kind" in item + else { + "effect_id": item["effect_id"], + "sequence": item["sequence"], + "emission_index": item["_determa_v1_emission_index"], + } + ) + for item in result["emissions"] + ] + + if request["arguments"].get("capture_snapshot"): + for active in registry._active.values(): + for key in ("guard_snapshot", "action_snapshot"): + captured = getattr(active.provider, key, None) + if captured is not None: + observation["value"][key] = captured + + +def _run_create( + bundle: Any, + registry: RuntimeProviderRegistry, + request: dict[str, Any], + observation: dict[str, Any], +) -> None: + creation = request["setup"]["create_request"] + result = create( + bundle, + creation["machine_id"], + creation["root_instance_id"], + creation["creation_id"], + creation["bindings"], + ) + observation["stages"].append("create") + _provider_counts(registry, observation) + if observation["calls"]["actions"]: + observation["stages"].extend(("evaluate_actions", "validate_output")) + observation["state_after"] = _state(result["state"]) + observation["result"] = result["status"] + observation["code"] = result["fault"]["code"] if result["fault"] else None + if result["fault"]: + observation["value"] = { + "boundary_code": "runtime_provider_output_invalid", + "source_locator": result["fault"]["source_locator"], + "emissions": len(result["emissions"]), + "status": result["status"], + } class _ConflictStore(MemoryExecutionStore): @@ -356,17 +498,17 @@ def _run_host_commit( entry = runtime["ready_mailbox"][0] store.conflict = request["arguments"]["cas_conflict"] store.replace_attempts = 0 + process_arguments = { + "expected_revision": checkpoint["revision"], + "expected_checkpoint_digest": checkpoint["execution_checkpoint_digest"], + "event_id": entry["envelope"]["event_id"], + "envelope_digest": entry["envelope_digest"], + "acceptance_sequence": entry["acceptance_sequence"], + "queue_sequence": entry["queue_sequence"], + } try: - host.process_ready_v1( - root_id, - setup["target_runtime_id"], - expected_revision=checkpoint["revision"], - expected_checkpoint_digest=checkpoint["execution_checkpoint_digest"], - event_id=entry["envelope"]["event_id"], - envelope_digest=entry["envelope_digest"], - acceptance_sequence=entry["acceptance_sequence"], - queue_sequence=entry["queue_sequence"], - ) + first = host.process_ready_v1(root_id, setup["target_runtime_id"], **process_arguments) + except ExecutionHostError as exc: _provider_counts(registry, observation) if observation["calls"]["guard"]: @@ -381,7 +523,58 @@ def _run_host_commit( after = host.read_checkpoint(root_id) assert after is not None and after.source_bytes == admitted.source_bytes return - raise AssertionError("expected one real store compare-and-swap conflict") + if request["arguments"]["cas_conflict"]: + raise AssertionError("expected one real store compare-and-swap conflict") + _provider_counts(registry, observation) + if observation["calls"]["guard"]: + observation["stages"].append("evaluate_guard") + if observation["calls"]["actions"]: + observation["stages"].extend(("evaluate_actions", "validate_output")) + observation["stages"].extend(("compare_and_swap", "commit")) + committed = host.read_checkpoint(root_id) + assert committed is not None and store.replace_attempts == 1 + observation["state_after"] = _state(committed.document["root_record"]["aggregate_state"]) + receipt = first["receipt"] + references = receipt["emission_references"] + intents = committed.document["pending_outbox_intents"] + observation["value"] = { + "accepted": observation["state_after"]["variables"]["accepted"], + "emissions": len(first["core_result"]["emissions"]), + "emission_identities": [ + { + "effect_id": item["effect_id"], + "emission_index": next( + reference["emission_index"] + for reference in references + if reference.get("effect_id") == item["effect_id"] + ), + "sequence": item["sequence"], + } + for item in first["core_result"]["emissions"] + ], + "checkpoint_revision": committed.document["revision"], + "retained_effect_references": copy.deepcopy(references), + "pending_outbox_entries": copy.deepcopy(intents), + } + providers_before = [ + copy.deepcopy(active.provider.__dict__) for active in registry._active.values() + ] + replay = host.process_ready_v1(root_id, setup["target_runtime_id"], **process_arguments) + after_replay = host.read_checkpoint(root_id) + assert after_replay is not None + observation["value"].update( + replay_receipt_equal=replay == receipt, + replay_checkpoint_unchanged=after_replay.source_bytes == committed.source_bytes, + replay_provider_calls_unchanged=providers_before + == [active.provider.__dict__ for active in registry._active.values()], + ) + observation["stages"].append("replay") + observation["result"] = ( + "handled_now" + if receipt["outcome"]["disposition"] == "handled" + else receipt["outcome"]["disposition"] + ) + observation["determa_state_committed"] = True def _run_inspect( @@ -484,9 +677,27 @@ def execute(payload: dict[str, Any]) -> dict[str, Any]: source["artifact_digest"] = hash_value( [source["artifact_format"], "1", typed_value(source["content"])] ) - from determa.state.compilation import _artifact + if "invalid_slot" in arguments: + content = source["content"] + region = content["regions"][0] + if arguments["invalid_slot"] == "metadata_guard": + content["template"]["meta"] = {"guard": "true"} + region["locator"] = "/meta/guard" + else: + content["template"]["machines"][0]["root"]["variables"] = { + "data": {"type": "map", "init": {"action": []}} + } + region.update( + kind="actions", locator="/machines/0/root/variables/data/init/action" + ) + from determa.state.wire import hash_value, typed_value + + source["artifact_digest"] = hash_value( + [source["artifact_format"], "1", typed_value(content)] + ) + from determa.state.compilation import _source_preflight, _verify_compilation_manifest - _artifact(source, "language-source-v1.schema.json", "determa.language_source") + _source_preflight(source) observation["stages"].append("resolve_compiler_closure") registry, _module, loaded = _installed( root, payload, installed, arguments, compiler=True @@ -505,17 +716,37 @@ def execute(payload: dict[str, Any]) -> dict[str, Any]: manifest["content"]["generated_validated_bundle_fingerprint"] = arguments[ "manifest_fingerprint_override" ] + from determa.state.wire import hash_value, typed_value + + manifest["artifact_digest"] = hash_value( + [manifest["artifact_format"], "1", typed_value(manifest["content"])] + ) else: manifest = None - bundle = compile_language_source( - source, - registry, - maximum_compilation_steps=arguments.get("maximum_compilation_steps", 1000), - ) - observation["stages"].append("strict_load") - if manifest is not None: - observation["stages"].append("verify_manifest") - bundle = compile_language_source(source, registry, manifest=manifest) + previous_profile = sys.getprofile() + + def trace(frame, event, arg): + if event == "call": + if frame.f_code is _module.compile_region.__code__: + observation["calls"]["compile_region"] += 1 + elif ( + frame.f_code is load_bundle.__code__ + and frame.f_back.f_code is compile_language_source.__code__ + ): + observation["stages"].append("strict_load") + elif frame.f_code is _verify_compilation_manifest.__code__: + observation["stages"].append("verify_manifest") + + sys.setprofile(trace) + try: + bundle = compile_language_source( + source, + registry, + manifest=manifest, + maximum_compilation_steps=arguments.get("maximum_compilation_steps", 1000), + ) + finally: + sys.setprofile(previous_profile) if "generated_bundle_file" in arguments: supplied = load_bundle( json.loads((root / arguments["generated_bundle_file"]).read_text()) @@ -528,9 +759,37 @@ def execute(payload: dict[str, Any]) -> dict[str, Any]: "on_events" ]["submit"]["guard"] } - observation["effective_capabilities"] = registry.compiler_capabilities( - source["content"]["regions"][0]["provider_reference"] - ) + assert bundle.source_compilation is not None + evidence = bundle.source_compilation["manifest"]["content"] + observation["effective_capabilities"] = evidence["source_capabilities"] + if arguments.get("without_manifest") and arguments.get("weak_compiler"): + generated_registry = RuntimeProviderRegistry() + generated = load_bundle(bundle.raw, runtime_providers=generated_registry) + created = create(generated, "order", "compiled-restore-root", "compiled-create", {}) + before_restore_calls = observation["calls"]["compile_region"] + sys.setprofile(trace) + try: + restored = restore_aggregate( + created["state"], + MemoryArtifactResolver(definitions={generated.fingerprint: generated}), + ) + finally: + sys.setprofile(previous_profile) + observation["value"].update( + source_artifact_digest=evidence["source_artifact_digest"], + compiler_providers=evidence["compiler_providers"], + generated_validated_bundle_fingerprint=evidence[ + "generated_validated_bundle_fingerprint" + ], + generated_runtime_capabilities=generated_registry.effective_capabilities( + generated.raw + ), + restored_runtime_capabilities=generated_registry.effective_capabilities( + restored.bundle.raw + ), + restore_compiler_calls=observation["calls"]["compile_region"] + - before_restore_calls, + ) else: observation["stages"].append( "resolve_runtime_closure" if operation == "restore" else "resolve_closure" @@ -565,6 +824,8 @@ def execute(payload: dict[str, Any]) -> dict[str, Any]: observation["result"] = "accepted" elif operation == "step": _run_step(bundle, registry, request, observation) + elif operation == "create": + _run_create(bundle, registry, request, observation) elif operation == "host_commit": _run_host_commit(bundle, registry, request, observation) elif operation == "inspect": @@ -583,6 +844,12 @@ def execute(payload: dict[str, Any]) -> dict[str, Any]: observation["effective_capabilities"] = _profile() else: _provider_counts(registry, observation) + except ValidationError as exc: + # Static validation precedes provider capability verification inside the loader. + if observation["stages"][-1:] == ["verify_capabilities"]: + observation["stages"].pop() + observation["stages"].append("load") + observation["code"] = exc.code except RuntimeProviderError as exc: observation["code"] = exc.code return { diff --git a/conformance/test_conformance.py b/conformance/test_conformance.py index ddbed0b..c648fa0 100644 --- a/conformance/test_conformance.py +++ b/conformance/test_conformance.py @@ -148,7 +148,7 @@ def test_suite_present() -> None: assert len(core_cases()) == 99 assert len(version1_vectors()) == 162 assert len(durable_host_vectors()) == 142 - assert len(_manifest_artifacts()) == 484 + assert len(_manifest_artifacts()) == 487 @pytest.mark.parametrize("stored", [b"mutated", None]) diff --git a/scripts/sync_schema.py b/scripts/sync_schema.py index 0374681..28250cb 100644 --- a/scripts/sync_schema.py +++ b/scripts/sync_schema.py @@ -20,14 +20,41 @@ ROOT = Path(__file__).resolve().parent.parent DEST = ROOT / "src" / "determa" / "state" / "data" SCHEMAS = ( + "aggregate-state-package-v1.schema.json", + "aggregate-state-v1.schema.json", + "archive-export-request-v1.schema.json", + "archive-import-request-v1.schema.json", + "archive-participant-v1.schema.json", + "archive-result-v1.schema.json", + "archive-v1.schema.json", + "compilation-manifest-v1.schema.json", + "core-step-result-v1.schema.json", + "effect-cancellation-request-v1.schema.json", + "effect-cancellation-response-v1.schema.json", + "effect-result-request-v1.schema.json", + "effect-result-response-v1.schema.json", + "execution-checkpoint-v1.schema.json", + "extension-capability-report-v1.schema.json", + "extension-capability-requirement-v1.schema.json", + "extension-descriptor-v1.schema.json", + "host-authority-operation-v1.schema.json", + "host-authority-profile-report-v1.schema.json", + "host-effect-claim-v1.schema.json", + "host-effect-journal-v1.schema.json", + "inspection-v1.schema.json", + "language-source-v1.schema.json", "machine.schema.json", - "aggregate-state-v2.schema.json", - "migration-descriptor-v2.schema.json", - "aggregate-state-package-v2.schema.json", - "execution-checkpoint-v2.schema.json", - "core-step-result-v2.schema.json", + "migration-descriptor-v1.schema.json", + "provider-reference-v1.schema.json", + "public-host-request-v1.schema.json", + "public-host-response-v1.schema.json", + "recovery-operation-v1.schema.json", + "recovery-record-v1.schema.json", + "runtime-action-output-v1.schema.json", + "runtime-provider-descriptor-v1.schema.json", + "timer-helper-operation-v1.schema.json", ) -SPEC_COMMIT = "6796b554b976627c68fc746ec5629629aaf2b38e" +SPEC_COMMIT = "77c0a2e60cd0771a6d44ae170a079ddd51d7d9f0" def _fetch(name: str) -> str: diff --git a/src/determa/state/compilation.py b/src/determa/state/compilation.py index ce711f6..8b5d6af 100644 --- a/src/determa/state/compilation.py +++ b/src/determa/state/compilation.py @@ -5,11 +5,17 @@ import copy import json from collections.abc import Mapping +from dataclasses import replace from pathlib import Path from typing import Any from .definition import Bundle, load_bundle -from .runtime_providers import RuntimeProviderError, RuntimeProviderRegistry, _reference_key +from .runtime_providers import ( + RuntimeProviderError, + RuntimeProviderRegistry, + _executable_slots, + _reference_key, +) from .wire import _schema_registry, hash_value, typed_value @@ -64,14 +70,7 @@ def _slot(template: dict[str, Any], tokens: tuple[str, ...]) -> tuple[Any, str]: return parent, key -def compile_language_source( - source: Mapping[str, Any], - registry: RuntimeProviderRegistry, - *, - manifest: Mapping[str, Any] | None = None, - maximum_compilation_steps: int = 1000, -) -> Bundle: - """Verify provenance, compile disjoint regions, and strictly load the generated bundle.""" +def _source_preflight(source: Mapping[str, Any]) -> tuple[dict[str, Any], list[tuple[str, ...]]]: source_doc = _artifact( dict(source), "language-source-v1.schema.json", "determa.language_source" ) @@ -82,6 +81,48 @@ def compile_language_source( a != b and (a[: len(b)] == b or b[: len(a)] == a) for a in locations for b in locations ): raise RuntimeProviderError("language_compilation_failed") + generated = content["template"] + try: + slots = {location: (kind, value) for kind, location, value in _executable_slots(generated)} + except (AttributeError, TypeError, ValueError) as exc: + raise RuntimeProviderError("language_compilation_failed") from exc + for location, region in zip(locations, regions, strict=True): + slot = slots.get(location) + if ( + slot is None + or slot[0] != region["kind"] + or ( + type(slot[1]) is not str if region["kind"] == "guard" else type(slot[1]) is not list + ) + ): + raise RuntimeProviderError("language_compilation_failed") + return source_doc, locations + + +def _verify_compilation_manifest(manifest: Mapping[str, Any], computed: dict[str, Any]) -> None: + provided = _artifact( + dict(manifest), "compilation-manifest-v1.schema.json", "determa.compilation_manifest" + ) + if provided != computed: + raise RuntimeProviderError("language_compilation_failed") + + +def compile_language_source( + source: Mapping[str, Any], + registry: RuntimeProviderRegistry, + *, + manifest: Mapping[str, Any] | None = None, + maximum_compilation_steps: int = 1000, +) -> Bundle: + """Compile grammar slots and retain sealed version-1 source/manifest evidence. + + ``Bundle.source_compilation`` describes historical source guarantees; the + generated bundle keeps its independently verified runtime capability profile. + """ + source_doc, locations = _source_preflight(source) + content = source_doc["content"] + regions = content["regions"] + generated = copy.deepcopy(content["template"]) dependencies = content["dependencies"] if dependencies != sorted(dependencies, key=_reference_key) or len( {_reference_key(item) for item in dependencies} @@ -96,21 +137,15 @@ def compile_language_source( or not closure.manifest_verified() ): raise RuntimeProviderError("runtime_provider_unavailable") - generated = copy.deepcopy(content["template"]) - for location, region in zip(locations, regions, strict=True): - parent, key = _slot(generated, location) - value = parent[_array_index(key)] if isinstance(parent, list) else parent[key] - if (region["kind"] == "guard" and (key != "guard" or type(value) is not str)) or ( - region["kind"] == "actions" - and (key not in {"action", "entry", "exit"} or type(value) is not list) - ): - raise RuntimeProviderError("language_compilation_failed") compilers = [region["provider_reference"] for region in regions] - compiled = [registry.compiler(region["provider_reference"]) for region in regions] - for location, region, compiler in zip(locations, regions, compiled, strict=True): + for region in regions: + registry.compiler(region["provider_reference"]) + compiler_claims_before = [registry.compiler_capabilities(reference) for reference in compilers] + for location, region in zip(locations, regions, strict=True): if maximum_compilation_steps <= 0: raise RuntimeProviderError("language_compilation_limit_exceeded") maximum_compilation_steps -= 1 + compiler = registry.compiler(region["provider_reference"]) try: replacement = compiler(region["source"]) except Exception as exc: @@ -124,48 +159,43 @@ def compile_language_source( bundle = load_bundle(generated, runtime_providers=registry) except Exception as exc: raise RuntimeProviderError("language_compilation_failed") from exc - if manifest is not None: - manifest_doc = _artifact( - dict(manifest), "compilation-manifest-v1.schema.json", "determa.compilation_manifest" - ) - record = manifest_doc["content"] - expected_closure = sorted( - { - *(_reference_key(item) for item in dependencies), - *(_reference_key(item) for item in compilers), - } - ) - effective = dict.fromkeys( - ( - "deterministic", - "pure", - "portable", - "semantically_introspectable", - "process_contained", - ), - True, - ) - effective["external_io_capable"] = False - for reference in compilers: - claims = registry.compiler_capabilities(reference) - for name in effective: - effective[name] = ( - effective[name] or claims[name] - if name == "external_io_capable" - else effective[name] and claims[name] - ) - runtime_claims = registry.effective_capabilities(bundle.raw) + expected_closure = sorted( + { + *(_reference_key(item) for item in dependencies), + *(_reference_key(item) for item in compilers), + } + ) + effective = dict.fromkeys( + ("deterministic", "pure", "portable", "semantically_introspectable", "process_contained"), + True, + ) + effective["external_io_capable"] = False + for claims in [ + *compiler_claims_before, + *(registry.compiler_capabilities(reference) for reference in compilers), + registry.effective_capabilities(bundle.raw), + ]: for name in effective: effective[name] = ( - effective[name] or runtime_claims[name] + effective[name] or claims[name] if name == "external_io_capable" - else effective[name] and runtime_claims[name] + else effective[name] and claims[name] ) - if ( - record["source_artifact_digest"] != source_doc["artifact_digest"] - or [_reference_key(item) for item in record["compiler_providers"]] != expected_closure - or record["generated_validated_bundle_fingerprint"] != bundle.fingerprint - or record["source_capabilities"] != effective - ): - raise RuntimeProviderError("language_compilation_failed") - return bundle + record = { + "source_artifact_digest": source_doc["artifact_digest"], + "compiler_providers": [ + dict(zip(("identifier", "version", "content_digest"), item, strict=True)) + for item in expected_closure + ], + "generated_validated_bundle_fingerprint": bundle.fingerprint, + "source_capabilities": effective, + } + manifest_doc = { + "artifact_format": "determa.compilation_manifest", + "artifact_schema_version": 1, + "content": record, + "artifact_digest": hash_value(["determa.compilation_manifest", "1", typed_value(record)]), + } + if manifest is not None: + _verify_compilation_manifest(manifest, manifest_doc) + return replace(bundle, source_compilation={"source": source_doc, "manifest": manifest_doc}) diff --git a/src/determa/state/definition.py b/src/determa/state/definition.py index d16cf6a..a40a91e 100644 --- a/src/determa/state/definition.py +++ b/src/determa/state/definition.py @@ -163,6 +163,9 @@ class Bundle: required_capabilities: frozenset[str] = dataclass_field( default=frozenset(), compare=False, repr=False ) + source_compilation: dict[str, Any] | None = dataclass_field( + default=None, compare=False, repr=False + ) def verify_runtime_policy(self) -> None: """Recheck exact provider closure, current health, and host requirements.""" @@ -238,13 +241,6 @@ def load_bundle( def _has_runtime_provider(value: Any) -> bool: - if isinstance(value, dict): - return ( - "provider_actions" in value - or isinstance(value.get("guard"), dict) - and "provider" in value["guard"] - or any(_has_runtime_provider(child) for child in value.values()) - ) - if isinstance(value, list): - return any(_has_runtime_provider(child) for child in value) - return False + from .runtime_providers import _runtime_bindings + + return next(_runtime_bindings(value), None) is not None diff --git a/src/determa/state/engine.py b/src/determa/state/engine.py index f8ecbbe..0e2faa5 100644 --- a/src/determa/state/engine.py +++ b/src/determa/state/engine.py @@ -392,6 +392,7 @@ def _dispatch( delivery: _Delivery = None, *, _capture_emission_provenance: bool = False, + _provider_envelope: dict[str, Any] | None = None, ) -> _Result: """Validate and process at most one envelope against an aggregate copy.""" validated = _coerce_bundle(bundle) @@ -458,6 +459,9 @@ def _dispatch( assert declaration is not None normalized_envelope["payload"] = _normalize_payload(declaration, envelope.get("payload")) execution.event = normalized_envelope + if _provider_envelope is not None: + execution.provider_event = copy.deepcopy(_provider_envelope) + execution.provider_event["payload"] = copy.deepcopy(normalized_envelope["payload"]) execution.cause_id = str(envelope["event_id"]) before = copy.deepcopy(state) try: @@ -1498,6 +1502,7 @@ class _Execution: emissions: list[dict[str, Any]] cause_id: str event: dict[str, Any] | None + provider_event: dict[str, Any] | None capture_emission_provenance: bool def __init__( @@ -1516,6 +1521,7 @@ def __init__( self.emissions = [] self.cause_id = "" self.event = None + self.provider_event = None self.capture_emission_provenance = capture_emission_provenance self._provider_writes: list[tuple[str, str]] = [] @@ -1686,6 +1692,7 @@ def enter_state( return if state.type == "composite" and descend: initial = state.raw["initial"] + write_start = len(self._provider_writes) target, history = self.resolve_compound_transition( runtime, machine, @@ -1695,6 +1702,7 @@ def enter_state( event_visible=False, ) assert target is not None + self.validate_provider_writes(runtime, machine, state, target, False, write_start) self.enter_path(runtime, machine, state, target, history=history) def initialize_variables( @@ -1788,7 +1796,8 @@ def provider_snapshot( ) -> dict[str, Any]: from .runtime_providers import guard_snapshot - return guard_snapshot(activation, self.event, binding) + event = (self.provider_event or self.event) if "event" in activation else None + return guard_snapshot(activation, event, binding) def allocate_components( self, runtime: dict[str, Any], machine: MachineModel, state: StateNode @@ -2143,6 +2152,7 @@ def run_provider_proposals( from .runtime_providers import RuntimeProviderError from .wire import decoded_typed_value + ordinals = {"internal": 0, "external": 0} for proposal in proposals: try: if "assign" in proposal: @@ -2165,6 +2175,7 @@ def run_provider_proposals( pointer, event_visible=self.event is not None, literal_values=True, + native_ordinals=ordinals, ) except (DetermaError, ValueError, KeyError, TypeError) as exc: raise RuntimeProviderError("runtime_provider_output_invalid") from exc @@ -2179,6 +2190,7 @@ def send( *, event_visible: bool, literal_values: bool = False, + native_ordinals: dict[str, int] | None = None, ) -> None: activation = self.activation(runtime, machine, state, event_visible=event_visible) declaration = self.event_declaration(runtime, send["event"]) @@ -2188,12 +2200,22 @@ def send( if literal_values: from .wire import decoded_typed_value - if send["event"] == "env" or declaration is None: + if declaration is None and send["event"] != "env": raise StepFault(FaultCode.ACTION_FAULT, pointer) decoded = decoded_typed_value(send["payload"]) if not isinstance(decoded, dict): raise StepFault(FaultCode.ACTION_FAULT, pointer) payload_values = decoded + if send["event"] == "env" and ( + "targets" in send + or "correlation_id" in send + or not isinstance(send.get("to"), dict) + or set(send["to"]) != {"component"} + or set(payload_values) != {"changed"} + or not isinstance(payload_values["changed"], dict) + or not payload_values["changed"] + ): + raise StepFault(FaultCode.ACTION_FAULT, pointer) elif send["event"] == "env": changed_expression = payload_expressions["changed"] payload_values["changed"] = self.evaluate( @@ -2251,6 +2273,24 @@ def send( self.resolve_send_target(runtime, target_spec, value, pointer, index, "targets" in send) for index, (target_spec, value) in enumerate(evaluated_targets) ] + if literal_values and send["event"] == "env": + component_target = resolved[0] + if not isinstance(component_target, dict): + raise StepFault(FaultCode.ACTION_FAULT, pointer) + child = self.state["runtimes"][_target_runtime_id(component_target)] + root = _pointer_get(self.bundle.raw, child["root_pointer"]) + external = { + name: item + for name, item in (root.get("variables") or {}).items() + if item.get("external") is True + } + changed = normalized_payload["changed"] + if set(changed) - set(external): + raise StepFault(FaultCode.ACTION_FAULT, pointer) + normalized_payload["changed"] = { + name: _normalize_value(value, str(external[name]["type"])) + for name, value in changed.items() + } if ( literal_values and declaration is not None @@ -2262,6 +2302,10 @@ def send( ): raise StepFault(FaultCode.ACTION_FAULT, pointer) for index, target in enumerate(resolved): + if native_ordinals is not None: + kind = "external" if target == "external" else "internal" + index = native_ordinals[kind] + native_ordinals[kind] += 1 if target == "external": sequence = int(self.state["next_output_sequence"]) self.state["next_output_sequence"] = sequence + 1 @@ -2570,6 +2614,7 @@ def descend_composite( self.enter_path(runtime, machine, state, destination, history=False) return initial = state.raw["initial"] + write_start = len(self._provider_writes) target, target_history = self.resolve_compound_transition( runtime, machine, @@ -2579,6 +2624,7 @@ def descend_composite( event_visible=False, ) assert target is not None + self.validate_provider_writes(runtime, machine, state, target, False, write_start) self.enter_path(runtime, machine, state, target, history=target_history) def exit_state(self, runtime: dict[str, Any], machine: MachineModel, state: StateNode) -> None: diff --git a/src/determa/state/inspection.py b/src/determa/state/inspection.py index 8c87e66..57ae6da 100644 --- a/src/determa/state/inspection.py +++ b/src/determa/state/inspection.py @@ -245,7 +245,15 @@ def inspect_candidate( from .runtime_providers import RuntimeProviderError, guard_snapshot binding = guard["provider"] - snapshot = guard_snapshot(activation, candidate, binding) + snapshot = guard_snapshot( + activation, + { + **candidate, + "source": copy.deepcopy(envelope["source"]), + "cause_id": envelope["cause_id"], + }, + binding, + ) try: value, _charged, cost = runtime_providers.inspect_guard( binding, snapshot, guard_count + 1, remaining diff --git a/src/determa/state/queueing.py b/src/determa/state/queueing.py index 0fac8ce..18b6a92 100644 --- a/src/determa/state/queueing.py +++ b/src/determa/state/queueing.py @@ -883,6 +883,7 @@ def step_aggregate_v1( restored.state, {selected["delivery_mode"]: native}, _capture_emission_provenance=True, + _provider_envelope=selected["envelope"], ) disposition = result["disposition"] if disposition == DispositionCode.REJECTED.value: diff --git a/src/determa/state/runtime_providers.py b/src/determa/state/runtime_providers.py index f458224..b38e6ab 100644 --- a/src/determa/state/runtime_providers.py +++ b/src/determa/state/runtime_providers.py @@ -14,13 +14,67 @@ import json import sys import types -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Iterator, Mapping, Sequence from dataclasses import dataclass from pathlib import Path from typing import Any from .extensions import ExtensionError, ExtensionRegistry + +def _runtime_bindings( + definition: Mapping[str, Any], +) -> Iterator[tuple[str, Mapping[str, Any]]]: + """Visit only executable grammar slots; metadata and values remain inert.""" + + for kind, _, value in _executable_slots(definition): + if kind == "guard" and isinstance(value, Mapping) and "provider" in value: + yield kind, value["provider"] + elif kind == "actions" and isinstance(value, list): + for action in value: + if isinstance(action, Mapping) and "provider_actions" in action: + yield kind, action["provider_actions"] + + +def _executable_slots( + definition: Mapping[str, Any], +) -> Iterator[tuple[str, tuple[str, ...], Any]]: + """Enumerate grammar-owned guards and action blocks, never arbitrary values.""" + + def transitions( + value: Any, path: tuple[str, ...] + ) -> Iterator[tuple[str, tuple[str, ...], Any]]: + entries = enumerate(value) if isinstance(value, list) else [(None, value)] + for index, transition in entries: + if not isinstance(transition, Mapping): + continue + current = path if index is None else (*path, str(index)) + if "guard" in transition: + yield "guard", (*current, "guard"), transition["guard"] + if "action" in transition: + yield "actions", (*current, "action"), transition["action"] + + def state(value: Any, path: tuple[str, ...]) -> Iterator[tuple[str, tuple[str, ...], Any]]: + if not isinstance(value, Mapping): + return + for name in ("entry", "exit"): + if name in value: + yield "actions", (*path, name), value[name] + for name in ("initial", "choice"): + yield from transitions(value.get(name), (*path, name)) + for name, handler in (value.get("on_events") or {}).items(): + yield from transitions(handler, (*path, "on_events", name)) + for name, child in (value.get("states") or {}).items(): + yield from state(child, (*path, "states", name)) + for index, component in enumerate(value.get("components") or []): + if isinstance(component, Mapping) and "root" in component: + yield from state(component["root"], (*path, "components", str(index), "root")) + + for index, machine in enumerate(definition.get("machines") or []): + if isinstance(machine, Mapping): + yield from state(machine.get("root"), ("machines", str(index), "root")) + + SourceIdentityVerifier = Callable[[Path, Any], bool] @@ -1079,19 +1133,8 @@ def resolve(self, kind: str, binding: Mapping[str, Any]) -> ResolvedRuntimeProvi def resolve_definition(self, definition: Mapping[str, Any]) -> None: """Preflight every executable slot, including unreachable declarations.""" - def visit(value: Any) -> None: - if isinstance(value, dict): - if isinstance(value.get("guard"), dict) and "provider" in value["guard"]: - self.resolve("guard", value["guard"]["provider"]) - if "provider_actions" in value: - self.resolve("actions", value["provider_actions"]) - for child in value.values(): - visit(child) - elif isinstance(value, list): - for child in value: - visit(child) - - visit(definition) + for kind, binding in _runtime_bindings(definition): + self.resolve(kind, binding) def effective_capabilities(self, definition: Mapping[str, Any]) -> dict[str, bool]: """Compose verified provider guarantees for one complete definition.""" @@ -1105,18 +1148,6 @@ def effective_capabilities(self, definition: Mapping[str, Any]) -> dict[str, boo report = dict.fromkeys(names, True) report["external_io_capable"] = False - def visit(value: Any) -> None: - if isinstance(value, dict): - if isinstance(value.get("guard"), dict) and "provider" in value["guard"]: - combine(self.resolve("guard", value["guard"]["provider"]).capabilities) - if "provider_actions" in value: - combine(self.resolve("actions", value["provider_actions"]).capabilities) - for child in value.values(): - visit(child) - elif isinstance(value, list): - for child in value: - visit(child) - def combine(claims: Mapping[str, bool]) -> None: for name in names: report[name] = report[name] and claims.get(name) is True @@ -1124,7 +1155,8 @@ def combine(claims: Mapping[str, bool]) -> None: report["external_io_capable"] or claims.get("external_io_capable") is True ) - visit(definition) + for kind, binding in _runtime_bindings(definition): + combine(self.resolve(kind, binding).capabilities) return report def invoke_guard(self, binding: Mapping[str, Any], snapshot: Mapping[str, Any]) -> bool: diff --git a/tests/test_runtime_providers.py b/tests/test_runtime_providers.py index 2e50244..5f9b576 100644 --- a/tests/test_runtime_providers.py +++ b/tests/test_runtime_providers.py @@ -196,11 +196,68 @@ def test_exact_compilation_and_manifest_fingerprint() -> None: manifest = json.loads((_PROFILE / "source-manifest.json").read_text()) bundle = compile_language_source(source, registry, manifest=manifest) assert bundle.fingerprint == manifest["content"]["generated_validated_bundle_fingerprint"] + assert bundle.source_compilation == {"source": source, "manifest": manifest} manifest["content"]["generated_validated_bundle_fingerprint"] = "sha256:" + "0" * 64 + manifest["artifact_digest"] = hash_value( + [manifest["artifact_format"], "1", typed_value(manifest["content"])] + ) with pytest.raises(RuntimeProviderError, match="language_compilation_failed"): compile_language_source(source, registry, manifest=manifest) +@pytest.mark.parametrize("slot", ["metadata", "value"]) +def test_compiler_rejects_inert_slot_before_resolution(slot: str) -> None: + _, source = _installed_compiler(_PROFILE) + region = source["content"]["regions"][0] + template = source["content"]["template"] + if slot == "metadata": + template["meta"] = {"guard": "true"} + region["locator"] = "/meta/guard" + else: + template["machines"][0]["root"]["variables"] = { + "data": {"type": "map", "init": {"action": []}} + } + region.update(kind="actions", locator="/machines/0/root/variables/data/init/action") + source["artifact_digest"] = hash_value( + [source["artifact_format"], "1", typed_value(source["content"])] + ) + # No dependency/compiler is installed: grammar rejection must precede resolution. + with pytest.raises(RuntimeProviderError, match="language_compilation_failed"): + compile_language_source(source, RuntimeProviderRegistry()) + + +def test_weak_compiler_retains_source_evidence_without_manifest() -> None: + registry, source = _installed_compiler(_PROFILE) + key = tuple( + source["content"]["regions"][0]["provider_reference"][name] + for name in ("identifier", "version", "content_digest") + ) + registry._compiler_proofs[key] = None + bundle = compile_language_source(source, registry) + assert bundle.source_compilation is not None + evidence = bundle.source_compilation + assert evidence["source"] == source + assert evidence["manifest"]["content"]["source_capabilities"] == { + "deterministic": False, + "pure": False, + "portable": False, + "semantically_introspectable": False, + "process_contained": False, + "external_io_capable": True, + } + assert ( + evidence["manifest"]["content"]["generated_validated_bundle_fingerprint"] + == bundle.fingerprint + ) + generated = load_bundle(bundle.raw) + created = create(generated, "order", "generated-root", "generated-create", {}) + restored = restore_aggregate( + created["state"], MemoryArtifactResolver(definitions={generated.fingerprint: generated}) + ) + assert restored.bundle.source_compilation is None + assert restored.bundle.fingerprint == bundle.fingerprint + + def test_compiler_source_drift_refused_before_callback(tmp_path: Path) -> None: shutil.copytree(_PROFILE, tmp_path / "fixture") root = tmp_path / "fixture" @@ -211,6 +268,50 @@ def test_compiler_source_drift_refused_before_callback(tmp_path: Path) -> None: compile_language_source(source, registry) +def test_compiler_rebinding_between_regions_refused_before_substitute_runs(tmp_path: Path) -> None: + body = (_PROFILE / "provider/test_provider.py").read_text().split("def compile_region(")[0] + body += """ +calls = [] +def substitute(source): + calls.append('substitute') + return 'event.payload.approved' +def compile_region(source): + calls.append('original') + compile_region.__code__ = substitute.__code__ + return 'event.payload.approved' +""" + bundle, registry = _modified_runtime_bundle(tmp_path, provider_source=body) + active = registry.resolve("guard", _fixture_guard_binding(bundle)).provider + module = sys.modules[type(active).__module__] + closure = SourceClosure( + tmp_path / "fixture", + ("provider/test_provider.py", "provider/test_provider.rs"), + "provider-closure.json", + _DOMAIN, + "provider/test_provider.py", + ) + reference = { + "identifier": "test-mutating-compiler", + "version": "1.0.0", + "content_digest": closure.digest(), + } + registry.register_compiler(reference, module.compile_region, closure) + source = json.loads((_PROFILE / "source-package.json").read_text()) + content = source["content"] + content["dependencies"] = [] + second = json.loads(json.dumps(content["template"]["machines"][0])) + second["machine_id"] = "second" + content["template"]["machines"].append(second) + content["regions"][0]["provider_reference"] = reference + region = json.loads(json.dumps(content["regions"][0])) + region["locator"] = region["locator"].replace("/machines/0/", "/machines/1/") + content["regions"].append(region) + source["artifact_digest"] = hash_value([source["artifact_format"], "1", typed_value(content)]) + with pytest.raises(RuntimeProviderError, match="runtime_provider_unavailable"): + compile_language_source(source, registry) + assert module.calls == ["original"] + + def test_compiler_manifest_drift_refused_at_use(tmp_path: Path) -> None: shutil.copytree(_PROFILE, tmp_path / "fixture") root = tmp_path / "fixture" @@ -409,13 +510,13 @@ def test_semantic_and_ordinary_guards_see_same_native_event(tmp_path: Path) -> N source_path.read_text() .replace( "self.ordinary_calls += 1\n return True", - 'self.ordinary_calls += 1\n return "source" not in snapshot["event"] ' - 'and "cause_id" not in snapshot["event"]', + 'self.ordinary_calls += 1\n return snapshot["event"]["source"]["host"] is True ' + 'and snapshot["event"]["cause_id"] == snapshot["event"]["event_id"]', ) .replace( "return True, 1, 2", - 'return "source" not in snapshot["event"] ' - 'and "cause_id" not in snapshot["event"], 1, 2', + 'return snapshot["event"]["source"]["host"] is True ' + 'and snapshot["event"]["cause_id"] == snapshot["event"]["event_id"], 1, 2', ) ) source_path.write_text(source) @@ -829,7 +930,7 @@ def test_source_declared_slots_do_not_hide_weak_provider(tmp_path: Path) -> None "class Provider:", "class Provider:\n" " __slots__ = ('guard_calls', 'action_calls', 'external_calls', " - "'irreversible_effects', 'external_effect_log')", + "'irreversible_effects', 'external_effect_log', 'guard_snapshot', 'action_snapshot')", ) ) bundle, registry = _modified_runtime_bundle(tmp_path, provider_source=source)