From 2bc3003f5195fc2d9abbce576615cec5d4279337 Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 8 Aug 2026 18:03:26 -0800 Subject: [PATCH] Integrate RAVEL 0.6 transactions and lifecycle surfaces --- README.md | 7 +- docs/ARCHITECTURE.md | 40 ++- docs/PROJECT_MAP.md | 8 + docs/PROVIDER_RUNTIME.md | 8 + ravel_versions/0.6/Makefile | 26 +- .../0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md | 54 ++-- ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md | 21 +- ravel_versions/0.6/ravel-0.6-limitations.md | 4 +- .../0.6/ravel-0.6-transaction.schema.json | 39 +++ src/ravel/__init__.py | 15 +- src/ravel/c_observations.py | 124 +++++++++ src/ravel/checkpoint.py | 55 ++++ src/ravel/experience.py | 69 +++++ src/ravel/lifecycle.py | 227 ++++++++++++++++ src/ravel/mechanism_state.py | 34 +++ src/ravel/planning.py | 48 ++++ src/ravel/transition.py | 39 +++ src/ravel/world.py | 72 +++++ tests/test_components.py | 49 ++++ tests/test_lifecycle_experience.py | 89 ++++++ tests/test_ravel_0_6_provenance.py | 3 + tests/test_ravel_0_6_transaction.py | 90 +++++++ tools/ravel_0_6_build.py | 23 ++ tools/ravel_0_6_seed_candidate.py | 46 ++++ tools/ravel_0_6_transaction_surface.py | 253 ++++++++++++++++++ 25 files changed, 1402 insertions(+), 41 deletions(-) create mode 100644 ravel_versions/0.6/ravel-0.6-transaction.schema.json create mode 100644 src/ravel/c_observations.py create mode 100644 src/ravel/checkpoint.py create mode 100644 src/ravel/experience.py create mode 100644 src/ravel/lifecycle.py create mode 100644 src/ravel/mechanism_state.py create mode 100644 src/ravel/planning.py create mode 100644 src/ravel/transition.py create mode 100644 src/ravel/world.py create mode 100644 tests/test_components.py create mode 100644 tests/test_lifecycle_experience.py create mode 100644 tests/test_ravel_0_6_transaction.py create mode 100644 tools/ravel_0_6_transaction_surface.py diff --git a/README.md b/README.md index b655144..8f54b1a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ RAVEL — the **Recursive Adaptive Vector Execution Lattice** — is an experime RAVEL operates beneath the technical authority of the Machine-Native Complexity Standard (MNCS) and the Machine-Native Complexity Development Standard (MNCDS). It is not intended to replace a language model, compiler, static analyzer, test framework, or the MNCS Forge. Its role is to decide what evidence should be gathered, what action should follow, and what experience should be retained for later use without redefining the governing status of that evidence. -> **Project status:** RAVEL is research software. Historical RAVEL 0.4 and 0.5 results remain development `FAIL`; RAVEL 0.6 candidate-001 now has a reproducible development build and behavioral harness, but has not been evaluated, frozen, selected, or promoted. Formal MNCS/MNCDS conformance, independent attestation, protected custody, production safety, and general recursive self-improvement remain `UNKNOWN`. +> **Project status:** RAVEL is research software. Historical RAVEL 0.4 and 0.5 results remain development `FAIL`; RAVEL 0.6 candidate-001 now has a reproducible build, an integrated development transaction path, behavioral fixtures, modular Python surfaces, and lifecycle scaffolding. It has not been frozen, selection-evaluated, independently evaluated, or promoted. Formal MNCS/MNCDS conformance, independent attestation, protected custody, production safety, and general recursive self-improvement remain `UNKNOWN`. ## Place in the MNCS ecosystem @@ -106,7 +106,10 @@ The current 0.6 implementation status is recorded in [`ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md`](ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md). Optional provider/resource protocols in `src/ravel/providers.py` and `src/ravel/resources.py` record scoped execution observations without claiming -algorithmic superiority. +algorithmic superiority. The bounded component surfaces in `src/ravel/world.py`, +`src/ravel/transition.py`, `src/ravel/planning.py`, `src/ravel/checkpoint.py`, +and `src/ravel/mechanism_state.py` provide deterministic provider substitution +and checkpoint fixtures; they do not replace the historical 0.5 source. ## Non-goals diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7ba1508..da0fcbe 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -142,6 +142,22 @@ The repository now contains small, dependency-free foundations beneath the - `ravel.memory` retains versioned source identities, explicit scope contracts, negative-aware full-text retrieval, append-only proposal lifecycle events, atomic batches, and rebuildable relation projections. +- `ravel.world`, `ravel.transition`, and `ravel.planning` define replaceable + environment, deterministic transition-compilation, and bounded planning + surfaces. `ToyBranchingWorld` and `ToyRingWorld` are independent fixtures; + provider identity is carried into the compiled projection. +- `ravel.mechanism_state` and `ravel.checkpoint` keep mechanism state separate + from evaluation and provide canonical round-trip identities with corruption + detection. +- `ravel.lifecycle` records development candidate state transitions in an + append-only hash chain. `ravel.experience` turns scoped raw execution into + advisory episodic or negative memory without promoting status. + +The generated candidate-001 C source now has an explicit copy, hard-gate, raw +observation, and rollback surface around the existing adaptation primitive. The +historical monolithic C translation unit has not yet been physically extracted +into separate C translation units; the Python surfaces are the first tested +decomposition boundary, not a claim of completed R6-04 C decomposition. These modules are tested scaffolding and are not claims that RAVEL 0.6 has been evaluated, selected, independently evaluated, certified, promoted, or made @@ -254,15 +270,15 @@ The architectural invariant is: > **RAVEL may remember any accurately typed experience, but only MNCS/MNCDS-governed evidence can establish the status and permitted use of that experience.** -## Initial implementation order - -1. Define versioned MNCS/MNCDS-governed evidence and experience schemas. -2. Build an append-only local experience store. -3. Add a small Forge adapter for a handful of micro-verifiers. -4. Implement deterministic evidence-gap and verifier-selection rules. -5. Add causal hypothesis and intervention records. -6. Add negative-memory retrieval. -7. Compare static and adaptive routing under equal budgets. -8. Add transfer gates before broad strategy reuse. -9. Add distributed scheduling only after local semantics are stable. -10. Consider learned routing models only after the rule-based baseline is measurable. +## Current implementation order + +1. Physically extract the tested C transaction, provider, planner, checkpoint, + and observation surfaces without changing candidate behavior. +2. Extend the tested `ravel.c_observations` cross-language adapter with a full + C/Python negative-reason matrix without making either side authoritative. +3. Use the append-only candidate ledger for development-only candidate freezes; + do not consume selection partitions in this repository-local loop. +4. Connect accepted and rejected execution records to the append-only memory + store and benchmark negative/contradiction retrieval. +5. Use the inspected Forge CLI/provider boundary only through explicit, + dependency-injected adapters; unavailable capabilities remain `UNKNOWN`. diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index c0dd9c4..4bc059a 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -53,6 +53,14 @@ regenerating the frozen records. - `src/ravel/adaptation.py` provides the tested retention-transaction boundary. - `src/ravel/providers.py` and `src/ravel/resources.py` provide optional, replaceable Forge/runtime protocols without heavy ML dependencies. +- `src/ravel/world.py`, `src/ravel/transition.py`, and `src/ravel/planning.py` + provide deterministic provider/compiler/planner surfaces with two toy worlds. +- `src/ravel/mechanism_state.py` and `src/ravel/checkpoint.py` define the + evaluator-free state and canonical checkpoint boundary. +- `src/ravel/lifecycle.py` provides development-only append-only candidate + freeze/selection infrastructure; it has not consumed selection data. +- `src/ravel/experience.py` binds scoped execution outcomes to advisory memory, + retaining negative and `UNKNOWN` outcomes. - [`MIGRATION.md`](MIGRATION.md) records the standalone extraction provenance. ## Build and verification entry points diff --git a/docs/PROVIDER_RUNTIME.md b/docs/PROVIDER_RUNTIME.md index 9713aa5..36d43e2 100644 --- a/docs/PROVIDER_RUNTIME.md +++ b/docs/PROVIDER_RUNTIME.md @@ -32,3 +32,11 @@ status, evidence quality, and verifier outcome before any strategy reuse. Standalone RAVEL tests use fake providers and resource backends. Forge is not a mandatory dependency for the core package, and lack of a GPU does not make the normal test suite fail. + +The local Forge checkout was inspected for this iteration. Its current CLI +exposes typed project, provider, verifier, candidate, and lifecycle operations; +the available provider listing reports the project micro-verifier provider and +the precedence `FAIL > UNKNOWN > PASS`. RAVEL does not invoke that checkout as +a mandatory test dependency. `src/ravel/providers.py` remains the narrow +dependency-injected adapter boundary, so a missing or malformed Forge result +remains `UNKNOWN` rather than being synthesized as `PASS`. diff --git a/ravel_versions/0.6/Makefile b/ravel_versions/0.6/Makefile index d9e1470..049ba7f 100644 --- a/ravel_versions/0.6/Makefile +++ b/ravel_versions/0.6/Makefile @@ -1,7 +1,7 @@ CC ?= cc CFLAGS ?= -std=c11 -O3 -Wall -Wextra -Werror -pedantic -.PHONY: build behavioral-test +.PHONY: build behavioral-test transaction-test component-test compiler-matrix sanitizers build: @set -e; out=$$(mktemp -d); python3 tools/ravel_0_6_build.py build --output-dir "$$out"; \ @@ -9,3 +9,27 @@ build: behavioral-test: python3 tools/ravel_0_6_behavioral_fixtures.py + +transaction-test: + python3 -m unittest tests/test_ravel_0_6_transaction.py + +component-test: + python3 -m unittest tests/test_components.py tests/test_lifecycle_experience.py + +compiler-matrix: + @set -eu; for compiler in gcc clang; do \ + if command -v "$$compiler" >/dev/null 2>&1; then \ + out=$$(mktemp -d); CC="$$compiler" python3 tools/ravel_0_6_build.py build --output-dir "$$out" >/dev/null; \ + "$$out/ravel_0_6_candidate_001" --self-test >/dev/null; \ + find "$$out" -type f -delete; rmdir "$$out"; \ + printf '%s: PASS\n' "$$compiler"; \ + else printf '%s: UNAVAILABLE\n' "$$compiler"; fi; \ + done + +sanitizers: + @set -eu; if command -v clang >/dev/null 2>&1; then \ + out=$$(mktemp -d); python3 tools/ravel_0_6_seed_candidate.py --output "$$out/candidate.c" >/dev/null; \ + clang -std=c11 -O1 -g -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined "$$out/candidate.c" -lm -o "$$out/candidate"; \ + ASAN_OPTIONS=detect_leaks=0 "$$out/candidate" --self-test >/dev/null; \ + find "$$out" -type f -delete; rmdir "$$out"; printf 'clang ASAN/UBSAN: PASS\n'; \ + else printf 'clang ASAN/UBSAN: UNAVAILABLE\n'; fi diff --git a/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md b/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md index f3b5fa4..ea93d42 100644 --- a/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md +++ b/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md @@ -9,26 +9,50 @@ This is a development status record, not RAVEL 0.6 evaluation evidence. frozen source, generator, generated source, compiler/version/argv, selected environment-key identities, worktree status, stdout, stderr, and exit status. Generated source is explicitly development-only. -- **R6-02 transaction foundation:** the Python adaptation surface evaluates - independent hard constraints over a copied checkpoint and restores the - preceding bytes on any rejection. Rejection codes cover objective epsilon, - retention floors, prediction degradation, transition support, topology, - replay, update, and compute budgets. This surface is tested with one - negative fixture per constraint; the generated C candidate has not yet - consumed this generic interface as a final evaluator. +- **R6-02 integrated development transaction:** candidate-001 now wraps its + existing adaptation primitive in a copy-before-evaluate transaction. The + trial path records raw objective, retention, representation, prediction, + transition-support, topology, replay, pass, and compute observations, then + commits only when every hard gate passes. Rejection reasons use the Python + vocabulary where the surfaces overlap, and rejected candidates restore the + previous checkpoint byte-for-byte. This is a development mechanism + disposition, not MNCS/MNCDS authority or a final evaluator. - **R6-03 behavioral fixtures:** a compiled harness reaches a goal only through transition slot one and checks that a born child has only spawning-event support. The candidate passes; frozen 0.5 and each reverted correction fail. - Outputs are integer facts plus a stable checksum. + Outputs are integer facts plus stable checksums. A C transaction mutation + fixture also forces a retention gate failure and observes rollback. +- **R6-04 bounded surfaces:** `world`, `transition`, `planning`, + `mechanism_state`, and `checkpoint` provide dependency-free, deterministic + interfaces with two independently defined toy providers. These surfaces + reproduce the tested slot-one/unknown-route behaviors and detect checkpoint + corruption. The generated C candidate now has an explicit transaction and + observation surface, but its historical monolithic translation unit has not + been physically split into multiple C translation units; R6-04 is therefore + partial rather than complete. +- **R6-05 lifecycle infrastructure:** `ravel.lifecycle.CandidateLedger` is an + append-only, hash-chained, gap-resistant development ledger with the + preregistered candidate limit, freeze identities, partition separation, + rejection retention, and a guard preventing selection feedback from entering + the same candidate. No selection partition has been consumed. +- **Execution experience:** `ravel.experience.ExperienceRecord` converts raw, + scoped execution outcomes into advisory episodic or negative memory records. + It preserves `UNKNOWN`, rejection reasons, resource observations, and + provenance rather than promoting remembered outcomes. -## Not yet implemented +## Not yet implemented or externally unavailable -- R6-02 is not a completed 0.6 trial/evaluator pipeline. No development, - selection, or final material evaluation has been executed. -- R6-04 has not been claimed: the large generated C mechanism has not yet been - split into independent mechanism, provider, planning, checkpoint, and - observation units. -- R6-05 candidate ledgers and selection partition consumption are not present. +- The C-side trial is connected to `ravel.c_observations` through the versioned + JSON record and shared reason vocabulary, with an accepted-path cross-check. + The parser/evaluator remains advisory and does not create formal evidence + status. The C trial has no matched-compute comparator yet, so that condition + is emitted as unavailable and remains `UNKNOWN`; a complete cross-language + negative matrix is still pending. +- The large generated C mechanism has not yet been physically split into + independent translation units. The Python component surfaces are scaffolding + and cross-provider fixtures, not a claim that C decomposition is complete. +- R6-05 selection evaluation and promotion logic have not been consumed. The + ledger is infrastructure only; no candidate is frozen or selected by it. - R6-06 external final custody/evaluation remains unavailable and `UNKNOWN`. The 0.6 candidate remains unfrozen, unselected, unpromoted, and unauthorized diff --git a/ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md b/ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md index fc6265e..6d141fb 100644 --- a/ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md +++ b/ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md @@ -40,19 +40,22 @@ identity and requires every transformation to match exactly once. Tests verify source identity, transformation semantics, deterministic output, read-only checking, and strict C11 compilation. -These corrections produce development source only. Candidate 001 has not been -integrated into the 0.6 evidence pipeline, selected, frozen, or evaluated. +These corrections produce development source only. Candidate 001 is now +integrated into a local development trial path, but remains unselected, +unfrozen, and unevaluated for preregistered selection/final purposes. ## Current bounded implementation status Candidate-001 is now integrated as a reproducible temporary development build -by `tools/ravel_0_6_build.py`. A compiled behavioral harness verifies both -corrections and fails against frozen 0.5 behavior and bounded reverted -mutations. A separate Python transaction foundation tests all declared hard -constraint classes with byte-identical rollback. These are implementation and -mutation observations only; the C candidate is not yet a complete 0.6 trial -evaluator, and no selection or final material has been consumed. See -`RAVEL_0_6_IMPLEMENTATION_STATUS.md` for the exact boundary. +by `tools/ravel_0_6_build.py`. Its trial path wraps the existing adaptation +primitive in an all-hard-gates copy/observe/commit transaction and emits raw +structured observations with stable rejection reasons. A compiled behavioral +harness verifies both corrections and fails against frozen 0.5 behavior and +bounded reverted mutations. Python component surfaces provide two independent +toy providers, deterministic planning, canonical checkpoints, and an +append-only development candidate ledger. These are implementation and local +development observations only; no selection or final material has been +consumed. See `RAVEL_0_6_IMPLEMENTATION_STATUS.md` for the exact boundary. ## Codex next steps diff --git a/ravel_versions/0.6/ravel-0.6-limitations.md b/ravel_versions/0.6/ravel-0.6-limitations.md index 46a9311..4ecccea 100644 --- a/ravel_versions/0.6/ravel-0.6-limitations.md +++ b/ravel_versions/0.6/ravel-0.6-limitations.md @@ -1,6 +1,8 @@ # RAVEL 0.6 preregistration limitations -- No RAVEL 0.6 candidate has been implemented, frozen, selected, or evaluated. +- Candidate-001 has a reproducible development implementation and a local + development trial path, but it has not been frozen, selection-evaluated, + independently evaluated, or promoted. - Development and selection seeds are repository-visible operator-controlled material, not protected evidence. - Future final seed material does not yet exist in this checkout. diff --git a/ravel_versions/0.6/ravel-0.6-transaction.schema.json b/ravel_versions/0.6/ravel-0.6-transaction.schema.json new file mode 100644 index 0000000..f2521d3 --- /dev/null +++ b/ravel_versions/0.6/ravel-0.6-transaction.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ravel.dev/schema/ravel-0.6-transaction/0.1", + "title": "RAVEL 0.6 development adaptation transaction", + "type": "object", + "additionalProperties": false, + "required": ["committed", "threshold_identity", "rejection_reason", "failed_constraint_mask", "rollback_byte_identical", "raw"], + "properties": { + "committed": {"type": "boolean"}, + "threshold_identity": {"type": "string", "minLength": 1}, + "rejection_reason": {"type": "string"}, + "failed_constraint_mask": {"type": "integer", "minimum": 0}, + "rollback_byte_identical": {"type": "boolean"}, + "raw": { + "type": "object", + "additionalProperties": false, + "required": ["objective_before_q20", "objective_after_q20", "base_accuracy_before_q20", "base_accuracy_after_q20", "representation_before_q20", "representation_after_q20", "prediction_rmse_before_q20", "prediction_rmse_after_q20", "transition_support_losses", "expert_count", "births", "retirements", "replay_records", "update_passes", "compute_evaluations", "matched_compute_evaluations", "matched_compute_reference_available"], + "properties": { + "objective_before_q20": {"type": "integer", "minimum": 0}, + "objective_after_q20": {"type": "integer", "minimum": 0}, + "base_accuracy_before_q20": {"type": "integer", "minimum": 0}, + "base_accuracy_after_q20": {"type": "integer", "minimum": 0}, + "representation_before_q20": {"type": "integer", "minimum": 0}, + "representation_after_q20": {"type": "integer", "minimum": 0}, + "prediction_rmse_before_q20": {"type": "integer", "minimum": 0}, + "prediction_rmse_after_q20": {"type": "integer", "minimum": 0}, + "transition_support_losses": {"type": "integer", "minimum": 0}, + "expert_count": {"type": "integer", "minimum": 0}, + "births": {"type": "integer", "minimum": 0}, + "retirements": {"type": "integer", "minimum": 0}, + "replay_records": {"type": "integer", "minimum": 0}, + "update_passes": {"type": "integer", "minimum": 0}, + "compute_evaluations": {"type": "integer", "minimum": 0}, + "matched_compute_evaluations": {"type": "integer", "minimum": 0}, + "matched_compute_reference_available": {"type": "boolean"} + } + } + } +} diff --git a/src/ravel/__init__.py b/src/ravel/__init__.py index cc1fbb6..c684a7a 100644 --- a/src/ravel/__init__.py +++ b/src/ravel/__init__.py @@ -1,3 +1,16 @@ """RAVEL executable research components.""" -__all__ = ["memory"] +__all__ = [ + "adaptation", + "checkpoint", + "c_observations", + "experience", + "lifecycle", + "memory", + "mechanism_state", + "planning", + "providers", + "resources", + "transition", + "world", +] diff --git a/src/ravel/c_observations.py b/src/ravel/c_observations.py new file mode 100644 index 0000000..893ebc1 --- /dev/null +++ b/src/ravel/c_observations.py @@ -0,0 +1,124 @@ +"""Portable parser/evaluator for candidate-001 C transaction observations.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Mapping + +from .adaptation import ( + ConstraintReport, + RawObservation, + RetentionConstraintPolicy, + evaluate_constraints, +) + + +Q20 = 1_048_576.0 + + +@dataclass(frozen=True, slots=True) +class CTransactionObservation: + previous: RawObservation + proposed: RawObservation + committed: bool + threshold_identity: str + matched_compute_reference_available: bool + rejection_reason: str + failed_constraint_mask: int + rollback_byte_identical: bool + + @classmethod + def from_json(cls, value: Mapping[str, Any]) -> "CTransactionObservation": + raw = value.get("raw") + if not isinstance(raw, Mapping): + raise ValueError("C transaction raw observation is missing") + + def q20(name: str) -> float: + number = raw.get(name) + if not isinstance(number, int) or number < 0: + raise ValueError(f"{name} must be a non-negative integer") + return number / Q20 + + def count(name: str) -> int: + number = raw.get(name) + if not isinstance(number, int) or number < 0: + raise ValueError(f"{name} must be a non-negative integer") + return number + + before_representation = q20("representation_before_q20") + after_representation = q20("representation_after_q20") + previous_representation_score = 1.0 / (1.0 + before_representation) + proposed_representation_score = 1.0 / (1.0 + after_representation) + previous = RawObservation( + adaptation_objective=q20("objective_before_q20"), + base_accuracy=q20("base_accuracy_before_q20"), + representation_score=previous_representation_score, + original_prediction_degradation=0.0, + transition_support_losses=0, + expert_count=count("expert_count"), + births=0, + retirements=0, + replay_records=0, + update_passes=0, + compute_evaluations=0, + matched_compute_evaluations=0, + ) + before_prediction = q20("prediction_rmse_before_q20") + after_prediction = q20("prediction_rmse_after_q20") + proposed = RawObservation( + adaptation_objective=q20("objective_after_q20"), + base_accuracy=q20("base_accuracy_after_q20"), + representation_score=proposed_representation_score, + original_prediction_degradation=max(0.0, after_prediction - before_prediction), + transition_support_losses=count("transition_support_losses"), + expert_count=count("expert_count"), + births=count("births"), + retirements=count("retirements"), + replay_records=count("replay_records"), + update_passes=count("update_passes"), + compute_evaluations=count("compute_evaluations"), + matched_compute_evaluations=count("matched_compute_evaluations"), + ) + committed = value.get("committed") + rollback = value.get("rollback_byte_identical") + reason = value.get("rejection_reason") + threshold_identity = value.get("threshold_identity") + matched_compute_reference_available = raw.get("matched_compute_reference_available") + mask = value.get("failed_constraint_mask") + if not isinstance(committed, bool) or not isinstance(rollback, bool): + raise ValueError("C transaction disposition fields must be boolean") + if not isinstance(threshold_identity, str) or not threshold_identity: + raise ValueError("C transaction threshold identity is malformed") + if not isinstance(matched_compute_reference_available, bool): + raise ValueError("C transaction compute-reference flag is malformed") + if not isinstance(reason, str) or not isinstance(mask, int) or mask < 0: + raise ValueError("C transaction reason fields are malformed") + return cls( + previous, + proposed, + committed, + threshold_identity, + matched_compute_reference_available, + reason, + mask, + rollback, + ) + + def evaluate(self) -> ConstraintReport: + """Apply the existing Python evaluator to the C observation pair.""" + + policy = RetentionConstraintPolicy( + adaptation_improvement_epsilon=105.0 / Q20, + base_accuracy_floor=891290.0 / Q20, + representation_floor=self.previous.representation_score, + original_prediction_degradation_bound=1.0, + maximum_transition_support_losses=0, + maximum_experts=80, + maximum_births=16, + maximum_retirements=4, + maximum_replay_records=256, + maximum_update_passes=4, + maximum_compute_evaluations=2_000_000, + maximum_compute_ratio=1.0, + ) + return evaluate_constraints(self.previous, self.proposed, policy) diff --git a/src/ravel/checkpoint.py b/src/ravel/checkpoint.py new file mode 100644 index 0000000..afb279c --- /dev/null +++ b/src/ravel/checkpoint.py @@ -0,0 +1,55 @@ +"""Canonical checkpoint codec for decomposed development state.""" + +from __future__ import annotations + +from dataclasses import asdict +import hashlib +import json +from typing import Any + +from .mechanism_state import ExpertState, MechanismState + + +class CheckpointError(ValueError): + """Raised for malformed or identity-inconsistent checkpoints.""" + + +def _canonical(value: Any) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() + + +class CheckpointCodec: + schema = "ravel-0.6-mechanism-checkpoint/0.1" + + def encode(self, state: MechanismState) -> bytes: + payload = {"schema": self.schema, "state": asdict(state)} + return _canonical(payload) + + def identity(self, checkpoint: bytes) -> str: + return "sha256:" + hashlib.sha256(checkpoint).hexdigest() + + def decode(self, checkpoint: bytes) -> MechanismState: + try: + payload = json.loads(checkpoint) + if payload["schema"] != self.schema: + raise CheckpointError("checkpoint schema mismatch") + raw = payload["state"] + experts = tuple( + ExpertState( + lineage=expert["lineage"], + labels=tuple(expert["labels"]), + supported_actions=tuple(expert["supported_actions"]), + ) + for expert in raw["experts"] + ) + state = MechanismState( + experts=experts, + epoch=raw["epoch"], + births=raw["births"], + retirements=raw["retirements"], + ) + except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error: + raise CheckpointError("checkpoint is malformed") from error + if self.encode(state) != checkpoint: + raise CheckpointError("checkpoint is not canonical") + return state diff --git a/src/ravel/experience.py b/src/ravel/experience.py new file mode 100644 index 0000000..0bf6fc4 --- /dev/null +++ b/src/ravel/experience.py @@ -0,0 +1,69 @@ +"""Scoped execution experience records for advisory RAVEL memory.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Mapping + +from .memory import MemoryClass, MemoryRecord + + +@dataclass(frozen=True, slots=True) +class ExperienceRecord: + candidate_id: str + context_identity: str + task_environment: str + requested_strategy: str + provider_id: str + verifier_id: str + raw_result: Mapping[str, Any] + formal_disposition: str | None = None + adaptation_decision: str | None = None + rejection_reason: str | None = None + checkpoint_before: str | None = None + checkpoint_after: str | None = None + resource_observations: Mapping[str, Any] = field(default_factory=dict) + provenance: Mapping[str, str] = field(default_factory=dict) + applicability_scope: Mapping[str, str] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.candidate_id or not self.context_identity or not self.task_environment: + raise ValueError("candidate, context, and task identities are required") + if self.formal_disposition not in {None, "PASS", "FAIL", "UNKNOWN"}: + raise ValueError("formal disposition must be PASS, FAIL, UNKNOWN, or absent") + + @property + def negative(self) -> bool: + return self.formal_disposition in {"FAIL", "UNKNOWN"} or self.adaptation_decision == "rejected" + + @property + def record_id(self) -> str: + return f"experience:{self.candidate_id}:{self.context_identity}" + + def to_memory_record(self, *, created_at: str) -> MemoryRecord: + scope = dict(self.applicability_scope) + scope.update({"candidate": self.candidate_id, "context": self.context_identity}) + statement = ( + f"{self.requested_strategy} on {self.task_environment} returned " + f"{self.formal_disposition or 'unadjudicated'} via {self.provider_id}." + ) + relations = {"contradicts": ()} if self.negative else {} + return MemoryRecord( + record_id=self.record_id, + memory_class=MemoryClass.NEGATIVE if self.negative else MemoryClass.EPISODIC, + statement=statement, + scope=scope, + created_at=created_at, + producer_id="ravel-experience", + authority_class="advisory", + tags=(self.task_environment, self.requested_strategy, self.verifier_id), + relations=relations, + metadata={ + "raw_result": dict(self.raw_result), + "adaptation_decision": self.adaptation_decision, + "rejection_reason": self.rejection_reason, + "resource_observations": dict(self.resource_observations), + }, + evidence_identity=self.provenance.get("evidence_identity"), + experience_identity=self.record_id, + ) diff --git a/src/ravel/lifecycle.py b/src/ravel/lifecycle.py new file mode 100644 index 0000000..2e41056 --- /dev/null +++ b/src/ravel/lifecycle.py @@ -0,0 +1,227 @@ +"""Append-only RAVEL 0.6 candidate-development lifecycle. + +This ledger is development infrastructure only. It freezes identities and +records selection outcomes; it never performs selection and never authorizes +R6-06 custody or promotion. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import json +from pathlib import Path +from typing import Any, Mapping + + +class LedgerError(ValueError): + """Raised when an append-only lifecycle invariant would be violated.""" + + +class CandidateState: + CREATED = "created" + DEVELOPMENT = "development" + FROZEN = "candidate_frozen" + SELECTION = "selection_evaluation" + SELECTED = "selected" + REJECTED = "rejected" + + +_TRANSITIONS = { + CandidateState.CREATED: {CandidateState.DEVELOPMENT}, + CandidateState.DEVELOPMENT: {CandidateState.FROZEN}, + CandidateState.FROZEN: {CandidateState.SELECTION}, + CandidateState.SELECTION: {CandidateState.SELECTED, CandidateState.REJECTED}, + CandidateState.SELECTED: set(), + CandidateState.REJECTED: set(), +} + + +def _canonical(value: Mapping[str, Any]) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + + +def candidate_id(number: int) -> str: + if number < 1: + raise LedgerError("candidate number must be positive") + return f"ravel-0.6-candidate-{number:03d}" + + +@dataclass(frozen=True, slots=True) +class CandidateRecord: + candidate_id: str + number: int + state: str + source_identity: str | None = None + evaluator_identity: str | None = None + threshold_identity: str | None = None + development_partition: str | None = None + selection_partition: str | None = None + selection_result_ref: str | None = None + rejection_reasons: tuple[str, ...] = () + contamination_flag: bool = False + + +class CandidateLedger: + """A deterministic JSONL event stream with validated state transitions.""" + + def __init__(self, path: str | Path, *, maximum_candidates: int = 8) -> None: + if maximum_candidates < 1: + raise LedgerError("maximum_candidates must be positive") + self.path = Path(path) + self.maximum_candidates = maximum_candidates + + def _events(self) -> list[dict[str, Any]]: + if not self.path.exists(): + return [] + lines = self.path.read_text(encoding="utf-8").splitlines() + events: list[dict[str, Any]] = [] + previous = "0" * 64 + for expected, line in enumerate(lines, start=1): + try: + event = json.loads(line) + except json.JSONDecodeError as error: + raise LedgerError("ledger contains malformed JSON") from error + if event.get("sequence") != expected: + raise LedgerError("ledger sequence has a gap or mutation") + if event.get("previous_digest") != previous: + raise LedgerError("ledger hash chain is broken") + unsigned = dict(event) + digest = unsigned.pop("record_digest", None) + expected_digest = hashlib.sha256(_canonical(unsigned).encode()).hexdigest() + if digest != expected_digest: + raise LedgerError("ledger record digest mismatch") + previous = digest + events.append(event) + return events + + def _current(self) -> dict[str, CandidateRecord]: + current: dict[str, CandidateRecord] = {} + for event in self._events(): + payload = event["payload"] + record = current.get(event["candidate_id"]) + current[event["candidate_id"]] = CandidateRecord( + candidate_id=event["candidate_id"], + number=payload.get("number", record.number if record else 0), + state=event["state"], + source_identity=payload.get("source_identity", record.source_identity if record else None), + evaluator_identity=payload.get("evaluator_identity", record.evaluator_identity if record else None), + threshold_identity=payload.get("threshold_identity", record.threshold_identity if record else None), + development_partition=payload.get("development_partition", record.development_partition if record else None), + selection_partition=payload.get("selection_partition", record.selection_partition if record else None), + selection_result_ref=payload.get("selection_result_ref", record.selection_result_ref if record else None), + rejection_reasons=tuple(payload.get("rejection_reasons", record.rejection_reasons if record else ())), + contamination_flag=bool(payload.get("contamination_flag", record.contamination_flag if record else False)), + ) + return current + + def records(self) -> tuple[CandidateRecord, ...]: + return tuple(sorted(self._current().values(), key=lambda record: record.number)) + + def get(self, identifier: str) -> CandidateRecord: + record = self._current().get(identifier) + if record is None: + raise LedgerError(f"unknown candidate: {identifier}") + return record + + def _append(self, identifier: str, state: str, payload: Mapping[str, Any]) -> None: + events = self._events() + current = self._current() + previous_record = current.get(identifier) + if previous_record is not None and state != previous_record.state and state not in _TRANSITIONS[previous_record.state]: + raise LedgerError(f"invalid candidate transition: {previous_record.state}->{state}") + if previous_record is None and state != CandidateState.CREATED: + raise LedgerError("candidate must be created before a state transition") + event = { + "schema": "ravel-0.6-candidate-ledger/0.1", + "sequence": len(events) + 1, + "candidate_id": identifier, + "state": state, + "payload": dict(payload), + "previous_digest": events[-1]["record_digest"] if events else "0" * 64, + } + event["record_digest"] = hashlib.sha256(_canonical(event).encode()).hexdigest() + self.path.parent.mkdir(parents=True, exist_ok=True) + with self.path.open("a", encoding="utf-8", newline="\n") as stream: + stream.write(_canonical(event) + "\n") + + def create(self, *, development_partition: str, created_at: str) -> CandidateRecord: + records = self.records() + number = len(records) + 1 + if number > self.maximum_candidates: + raise LedgerError("candidate limit exceeded") + identifier = candidate_id(number) + if records and records[-1].number != number - 1: + raise LedgerError("candidate numbering is not gap-resistant") + self._append( + identifier, + CandidateState.CREATED, + {"number": number, "development_partition": development_partition, "created_at": created_at}, + ) + return self.get(identifier) + + def begin_development(self, identifier: str) -> CandidateRecord: + if self.get(identifier).state != CandidateState.CREATED: + raise LedgerError("candidate cannot re-enter development") + self._append(identifier, CandidateState.DEVELOPMENT, {}) + return self.get(identifier) + + def freeze( + self, + identifier: str, + *, + source_identity: str, + evaluator_identity: str, + threshold_identity: str, + selection_partition: str, + ) -> CandidateRecord: + values = { + "source_identity": source_identity, + "evaluator_identity": evaluator_identity, + "threshold_identity": threshold_identity, + "selection_partition": selection_partition, + } + if any(not value for value in values.values()): + raise LedgerError("freeze requires all immutable identities") + record = self.get(identifier) + if record.development_partition == selection_partition: + raise LedgerError("development and selection partitions must differ") + self._append(identifier, CandidateState.FROZEN, values) + return self.get(identifier) + + def start_selection(self, identifier: str) -> CandidateRecord: + return self._transition(identifier, CandidateState.SELECTION) + + def record_selection( + self, + identifier: str, + *, + selected: bool, + result_ref: str, + rejection_reasons: tuple[str, ...] = (), + contamination_flag: bool = False, + ) -> CandidateRecord: + if not result_ref: + raise LedgerError("selection result reference is required") + state = CandidateState.SELECTED if selected else CandidateState.REJECTED + self._append( + identifier, + state, + { + "selection_result_ref": result_ref, + "rejection_reasons": list(rejection_reasons), + "contamination_flag": contamination_flag, + }, + ) + return self.get(identifier) + + def append_development_feedback(self, identifier: str, *, result_ref: str) -> None: + if self.get(identifier).state != CandidateState.DEVELOPMENT: + raise LedgerError("selection or frozen evidence cannot feed the same candidate") + if not result_ref: + raise LedgerError("development result reference is required") + self._append(identifier, CandidateState.DEVELOPMENT, {"feedback_ref": result_ref}) + + def _transition(self, identifier: str, state: str) -> CandidateRecord: + self._append(identifier, state, {}) + return self.get(identifier) diff --git a/src/ravel/mechanism_state.py b/src/ravel/mechanism_state.py new file mode 100644 index 0000000..4e3f89a --- /dev/null +++ b/src/ravel/mechanism_state.py @@ -0,0 +1,34 @@ +"""State surface that deliberately excludes evaluators and authority.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class ExpertState: + lineage: str + labels: tuple[int, ...] + supported_actions: tuple[int, ...] + + +@dataclass(frozen=True, slots=True) +class MechanismState: + experts: tuple[ExpertState, ...] + epoch: int = 0 + births: int = 0 + retirements: int = 0 + + def __post_init__(self) -> None: + if self.epoch < 0 or self.births < 0 or self.retirements < 0: + raise ValueError("mechanism counters must be non-negative") + if len({expert.lineage for expert in self.experts}) != len(self.experts): + raise ValueError("expert lineage must be unique") + + def proposed(self, *, experts: tuple[ExpertState, ...], births: int = 0, retirements: int = 0) -> "MechanismState": + return MechanismState( + experts=experts, + epoch=self.epoch + 1, + births=self.births + births, + retirements=self.retirements + retirements, + ) diff --git a/src/ravel/planning.py b/src/ravel/planning.py new file mode 100644 index 0000000..1b9fef8 --- /dev/null +++ b/src/ravel/planning.py @@ -0,0 +1,48 @@ +"""Bounded deterministic planner over compiled transition interfaces.""" + +from __future__ import annotations + +from collections import deque +from dataclasses import dataclass +from typing import Literal + +from .transition import CompiledTransitions + + +PlanStatus = Literal["PASS", "UNKNOWN"] + + +@dataclass(frozen=True, slots=True) +class PlanResult: + status: PlanStatus + actions: tuple[int, ...] + visited: tuple[int, ...] + reason: str + + +def plan( + graph: CompiledTransitions, + *, + start: int, + goal: int, + maximum_steps: int = 32, +) -> PlanResult: + if maximum_steps < 0: + raise ValueError("maximum_steps must be non-negative") + queue: deque[tuple[int, tuple[int, ...]]] = deque([(start, ())]) + seen = {start} + visited: list[int] = [] + while queue: + state, actions = queue.popleft() + visited.append(state) + if state == goal: + return PlanResult("PASS", actions, tuple(visited), "route_found") + if len(actions) >= maximum_steps: + continue + for action in sorted({edge.action for edge in graph.edges if edge.source == state}): + for edge in graph.outgoing(state, action): + if edge.target in seen: + continue + seen.add(edge.target) + queue.append((edge.target, actions + (action,))) + return PlanResult("UNKNOWN", (), tuple(visited), "route_unavailable") diff --git a/src/ravel/transition.py b/src/ravel/transition.py new file mode 100644 index 0000000..fda69fe --- /dev/null +++ b/src/ravel/transition.py @@ -0,0 +1,39 @@ +"""Deterministic transition compilation independent of a world implementation.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .world import WorldProvider, WorldTransition + + +@dataclass(frozen=True, slots=True) +class CompiledTransitions: + provider_id: str + edges: tuple[WorldTransition, ...] + + def outgoing(self, source: int, action: int) -> tuple[WorldTransition, ...]: + return tuple( + edge for edge in self.edges if edge.source == source and edge.action == action + ) + + +class TransitionCompiler: + """Compile and canonically order provider observations; no evaluator state.""" + + def compile(self, provider: WorldProvider) -> CompiledTransitions: + states = set(provider.states()) + actions = set(provider.actions()) + edges: list[WorldTransition] = [] + for state in sorted(states): + for action in sorted(actions): + for edge in provider.transitions(state, action): + if edge.source != state or edge.action != action or edge.target not in states: + raise ValueError("provider returned an out-of-domain transition") + if edge.support < 1: + raise ValueError("transition support must be positive") + edges.append(edge) + return CompiledTransitions( + provider_id=provider.provider_id, + edges=tuple(sorted(edges, key=lambda item: (-item.support, item.source, item.action, item.target))), + ) diff --git a/src/ravel/world.py b/src/ravel/world.py new file mode 100644 index 0000000..66b4f15 --- /dev/null +++ b/src/ravel/world.py @@ -0,0 +1,72 @@ +"""Bounded world-provider surface for RAVEL development fixtures.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + + +@dataclass(frozen=True, slots=True) +class WorldTransition: + source: int + action: int + target: int + support: int + + +class WorldProvider(Protocol): + provider_id: str + + def states(self) -> tuple[int, ...]: ... + + def actions(self) -> tuple[int, ...]: ... + + def observe(self, state: int) -> tuple[int, ...]: ... + + def transitions(self, state: int, action: int) -> tuple[WorldTransition, ...]: ... + + +class ToyBranchingWorld: + """Independent toy world with a lower-ranked edge required by one route.""" + + provider_id = "ravel-toy-branching/1" + _observations = {0: (0, 0), 1: (1, 0), 2: (2, 0), 3: (3, 0)} + _edges = { + (0, 0): (WorldTransition(0, 0, 2, 2), WorldTransition(0, 0, 1, 1)), + (1, 1): (WorldTransition(1, 1, 3, 2),), + (2, 2): (WorldTransition(2, 2, 2, 2),), + } + + def states(self) -> tuple[int, ...]: + return tuple(sorted(self._observations)) + + def actions(self) -> tuple[int, ...]: + return (0, 1, 2) + + def observe(self, state: int) -> tuple[int, ...]: + return self._observations[state] + + def transitions(self, state: int, action: int) -> tuple[WorldTransition, ...]: + return self._edges.get((state, action), ()) + + +class ToyRingWorld: + """A separately defined provider proving mechanism/provider substitution.""" + + provider_id = "ravel-toy-ring/1" + _observations = {0: (0, 1), 1: (1, 1), 2: (2, 1), 3: (3, 1), 4: (4, 1)} + + def states(self) -> tuple[int, ...]: + return tuple(sorted(self._observations)) + + def actions(self) -> tuple[int, ...]: + return (0, 1) + + def observe(self, state: int) -> tuple[int, ...]: + return self._observations[state] + + def transitions(self, state: int, action: int) -> tuple[WorldTransition, ...]: + if state not in self._observations or action not in self.actions(): + return () + target = (state + (1 if action == 0 else 2)) % len(self._observations) + return (WorldTransition(state, action, target, 1),) diff --git a/tests/test_components.py b/tests/test_components.py new file mode 100644 index 0000000..2cb7641 --- /dev/null +++ b/tests/test_components.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import unittest + +from ravel.checkpoint import CheckpointCodec, CheckpointError +from ravel.mechanism_state import ExpertState, MechanismState +from ravel.planning import plan +from ravel.transition import TransitionCompiler +from ravel.world import ToyBranchingWorld, ToyRingWorld + + +class ComponentTests(unittest.TestCase): + def test_branching_provider_requires_lower_ranked_transition(self) -> None: + graph = TransitionCompiler().compile(ToyBranchingWorld()) + result = plan(graph, start=0, goal=3) + self.assertEqual(result.status, "PASS") + self.assertEqual(result.actions, (0, 1)) + self.assertEqual(result.reason, "route_found") + + def test_provider_substitution_changes_identity_not_compiler(self) -> None: + compiler = TransitionCompiler() + first = compiler.compile(ToyBranchingWorld()) + second = compiler.compile(ToyRingWorld()) + self.assertNotEqual(first.provider_id, second.provider_id) + self.assertNotEqual(first.edges, second.edges) + self.assertEqual(plan(second, start=0, goal=3).status, "PASS") + + def test_unsupported_route_is_unknown(self) -> None: + graph = TransitionCompiler().compile(ToyBranchingWorld()) + result = plan(graph, start=2, goal=3, maximum_steps=1) + self.assertEqual(result.status, "UNKNOWN") + self.assertEqual(result.reason, "route_unavailable") + + def test_checkpoint_round_trip_is_canonical_and_detects_corruption(self) -> None: + state = MechanismState( + experts=(ExpertState("lineage-a", (1,), (0,)),), + epoch=2, + births=1, + ) + codec = CheckpointCodec() + checkpoint = codec.encode(state) + self.assertEqual(codec.decode(checkpoint), state) + self.assertEqual(codec.encode(codec.decode(checkpoint)), checkpoint) + with self.assertRaises(CheckpointError): + codec.decode(checkpoint + b" ") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_lifecycle_experience.py b/tests/test_lifecycle_experience.py new file mode 100644 index 0000000..a5009ea --- /dev/null +++ b/tests/test_lifecycle_experience.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import tempfile +import unittest + +from ravel.experience import ExperienceRecord +from ravel.lifecycle import CandidateLedger, CandidateState, LedgerError + + +class LifecycleTests(unittest.TestCase): + def test_candidate_numbers_are_sequential_and_freeze_is_immutable(self) -> None: + with tempfile.TemporaryDirectory() as directory: + ledger = CandidateLedger(f"{directory}/candidates.jsonl") + candidate = ledger.create(development_partition="dev-a", created_at="t0") + self.assertEqual(candidate.candidate_id, "ravel-0.6-candidate-001") + ledger.begin_development(candidate.candidate_id) + ledger.append_development_feedback(candidate.candidate_id, result_ref="dev-result-1") + frozen = ledger.freeze( + candidate.candidate_id, + source_identity="sha256:source", + evaluator_identity="sha256:evaluator", + threshold_identity="sha256:threshold", + selection_partition="selection-a", + ) + self.assertEqual(frozen.state, CandidateState.FROZEN) + with self.assertRaises(LedgerError): + ledger.append_development_feedback(candidate.candidate_id, result_ref="must-not-enter") + self.assertEqual(len(ledger.records()), 1) + + def test_selection_result_is_retained_without_same_candidate_feedback(self) -> None: + with tempfile.TemporaryDirectory() as directory: + ledger = CandidateLedger(f"{directory}/candidates.jsonl") + candidate = ledger.create(development_partition="dev", created_at="t0") + ledger.begin_development(candidate.candidate_id) + ledger.freeze( + candidate.candidate_id, + source_identity="sha256:source", + evaluator_identity="sha256:evaluator", + threshold_identity="sha256:threshold", + selection_partition="selection", + ) + ledger.start_selection(candidate.candidate_id) + rejected = ledger.record_selection( + candidate.candidate_id, + selected=False, + result_ref="selection-result", + rejection_reasons=("base_accuracy_floor",), + ) + self.assertEqual(rejected.state, CandidateState.REJECTED) + self.assertEqual(rejected.rejection_reasons, ("base_accuracy_floor",)) + with self.assertRaises(LedgerError): + ledger.append_development_feedback(candidate.candidate_id, result_ref="selection-feedback") + + def test_ledger_mutation_and_candidate_limit_fail_closed(self) -> None: + with tempfile.TemporaryDirectory() as directory: + path = f"{directory}/candidates.jsonl" + ledger = CandidateLedger(path, maximum_candidates=1) + candidate = ledger.create(development_partition="dev", created_at="t0") + with self.assertRaises(LedgerError): + ledger.create(development_partition="dev-2", created_at="t1") + with open(path, "a", encoding="utf-8") as stream: + stream.write("{}\n") + with self.assertRaises(LedgerError): + ledger.get(candidate.candidate_id) + + +class ExperienceTests(unittest.TestCase): + def test_unknown_and_rejected_experience_becomes_negative_memory(self) -> None: + experience = ExperienceRecord( + candidate_id="ravel-0.6-candidate-001", + context_identity="toy-a", + task_environment="toy-environment", + requested_strategy="sequential-cpu", + provider_id="fake-provider", + verifier_id="forge-verifier", + raw_result={"oom": True}, + formal_disposition="UNKNOWN", + adaptation_decision="rejected", + rejection_reason="compute_budget", + applicability_scope={"hardware": "cpu-only"}, + ) + record = experience.to_memory_record(created_at="2026-08-08T00:00:00Z") + self.assertEqual(record.memory_class.value, "negative") + self.assertEqual(record.experience_identity, experience.record_id) + self.assertIn("UNKNOWN", record.statement) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ravel_0_6_provenance.py b/tests/test_ravel_0_6_provenance.py index 4ab9ffe..1768596 100644 --- a/tests/test_ravel_0_6_provenance.py +++ b/tests/test_ravel_0_6_provenance.py @@ -40,7 +40,10 @@ def test_build_record_binds_generator_compiler_and_raw_status(self) -> None: record = build(Path(directory)) self.assertEqual(record["candidate_id"], "ravel-0.6-candidate-001") self.assertTrue(record["generator"]["sha256"]) + self.assertTrue(record["generator"]["transaction_surface"]["sha256"]) + self.assertTrue(record["mechanism_components"]) self.assertTrue(record["compiler"]["argv"]) + self.assertEqual(record["execution"]["status"], "NOT_RUN") self.assertEqual(record["build"]["exit_status"], 0) self.assertFalse(record["authoritative_evidence"]) diff --git a/tests/test_ravel_0_6_transaction.py b/tests/test_ravel_0_6_transaction.py new file mode 100644 index 0000000..820abd9 --- /dev/null +++ b/tests/test_ravel_0_6_transaction.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import json +from pathlib import Path +import subprocess +import tempfile +import unittest + +from ravel.c_observations import CTransactionObservation +from tools.ravel_0_6_seed_candidate import FROZEN_SOURCE, build_candidate_source + + +ROOT = Path(__file__).resolve().parents[1] + + +def build_and_trial(source_text: str, directory: Path) -> dict[str, object]: + source = directory / "candidate.c" + binary = directory / "candidate" + source.write_text(source_text, encoding="utf-8") + built = subprocess.run( + ["cc", "-std=c11", "-O0", "-Wall", "-Wextra", "-Werror", "-pedantic", str(source), "-lm", "-o", str(binary)], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if built.returncode != 0: + raise AssertionError(built.stderr) + run = subprocess.run( + [str(binary), "--trial", "transaction-test", "--regime", "separated_state", "--seed", "0x1234"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if run.returncode != 0: + raise AssertionError(run.stderr) + return json.loads(run.stdout) + + +class CandidateTransactionTests(unittest.TestCase): + def test_trial_uses_all_constraint_transaction_and_emits_raw_observations(self) -> None: + with tempfile.TemporaryDirectory() as directory: + payload = build_and_trial( + build_candidate_source(FROZEN_SOURCE.read_bytes()), Path(directory) + ) + transaction = payload["candidate"]["adaptation_transaction"] + self.assertTrue(transaction["committed"]) + self.assertFalse(transaction["rollback_byte_identical"]) + self.assertEqual(transaction["threshold_identity"], "ravel-0.6-retention-gates/0.1") + self.assertEqual(transaction["failed_constraint_mask"], 0) + self.assertEqual(transaction["rejection_reason"], "none") + self.assertEqual(transaction["raw"]["transition_support_losses"], 0) + self.assertLessEqual(transaction["raw"]["births"], 16) + self.assertLessEqual(transaction["raw"]["retirements"], 4) + self.assertLessEqual(transaction["raw"]["replay_records"], 256) + parsed = CTransactionObservation.from_json(transaction) + self.assertFalse(parsed.matched_compute_reference_available) + self.assertFalse(parsed.evaluate().passed) + self.assertEqual( + parsed.evaluate().rejection_reasons, + ("matched_compute_reference_unavailable",), + ) + try: + import jsonschema + except ImportError: + self.skipTest("jsonschema is unavailable") + schema = json.loads( + (ROOT / "ravel_versions/0.6/ravel-0.6-transaction.schema.json").read_text() + ) + jsonschema.validate(transaction, schema) + + def test_mutated_hard_gate_rejects_and_rolls_back(self) -> None: + source = build_candidate_source(FROZEN_SOURCE.read_bytes()) + mutated = source.replace( + "#define RAVEL06_BASE_ACCURACY_FLOOR_Q20 UINT64_C(891290)", + "#define RAVEL06_BASE_ACCURACY_FLOOR_Q20 UINT64_MAX", + 1, + ) + with tempfile.TemporaryDirectory() as directory: + payload = build_and_trial(mutated, Path(directory)) + transaction = payload["candidate"]["adaptation_transaction"] + self.assertFalse(transaction["committed"]) + self.assertEqual(transaction["rejection_reason"], "base_accuracy_floor") + self.assertTrue(transaction["rollback_byte_identical"]) + self.assertNotEqual(transaction["failed_constraint_mask"], 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/ravel_0_6_build.py b/tools/ravel_0_6_build.py index fb6d581..4c18421 100644 --- a/tools/ravel_0_6_build.py +++ b/tools/ravel_0_6_build.py @@ -35,6 +35,16 @@ ROOT = Path(__file__).resolve().parents[1] GENERATOR = Path(__file__).resolve() +TRANSACTION_SURFACE = ROOT / "tools/ravel_0_6_transaction_surface.py" +COMPONENT_FILES = ( + "src/ravel/mechanism_state.py", + "src/ravel/world.py", + "src/ravel/transition.py", + "src/ravel/planning.py", + "src/ravel/checkpoint.py", + "src/ravel/lifecycle.py", + "src/ravel/experience.py", +) CANDIDATE_ID = "ravel-0.6-candidate-001" ENVIRONMENT_KEYS = ("CC", "CFLAGS", "CPPFLAGS", "LDFLAGS", "LC_ALL", "LANG") CANONICAL_FLAGS = ("-std=c11", "-O3", "-Wall", "-Wextra", "-Werror", "-pedantic") @@ -127,7 +137,15 @@ def build(output_dir: Path, *, require_clean_worktree: bool = False) -> dict[str "path": str(GENERATOR.relative_to(ROOT)), "sha256": sha256_file(GENERATOR), "seed_builder": "tools/ravel_0_6_seed_candidate.py", + "transaction_surface": { + "path": str(TRANSACTION_SURFACE.relative_to(ROOT)), + "sha256": sha256_file(TRANSACTION_SURFACE), + }, }, + "mechanism_components": [ + {"path": path, "sha256": sha256_file(ROOT / path)} + for path in COMPONENT_FILES + ], "generated_source": { "path": str(source_path), "sha256": sha256_bytes(source), @@ -153,6 +171,11 @@ def build(output_dir: Path, *, require_clean_worktree: bool = False) -> dict[str "exit_status": result.returncode, "binary_sha256": sha256_file(binary_path) if result.returncode == 0 else None, }, + "execution": { + "argv": None, + "status": "NOT_RUN", + "reason": "build record does not claim a trial execution", + }, } record_path.write_text(canonical_json(record) + "\n", encoding="utf-8") if version.returncode != 0: diff --git a/tools/ravel_0_6_seed_candidate.py b/tools/ravel_0_6_seed_candidate.py index 910efe6..4bfaf11 100644 --- a/tools/ravel_0_6_seed_candidate.py +++ b/tools/ravel_0_6_seed_candidate.py @@ -21,6 +21,11 @@ import sys from pathlib import Path +try: + from .ravel_0_6_transaction_surface import TRANSACTION_SURFACE +except ImportError: # direct script execution from the tools directory + from ravel_0_6_transaction_surface import TRANSACTION_SURFACE # type: ignore[no-redef] + RAVEL_DIR = Path(__file__).resolve().parents[1] FROZEN_SOURCE = RAVEL_DIR / "ravel_versions/0.5/ravel_0_5.c" FROZEN_SOURCE_SHA256 = "1a8466ea1805811873c461fb891aaeaec18f6c9e7491b5ea7bd09bf698be102d" @@ -138,6 +143,47 @@ def build_candidate_source(source_bytes: bytes) -> str: raise SeedError(f"{name}: expected one source match, found {count}") source = source.replace(old, new, 1) + observation_marker = "typedef struct {\n Model model;\n" + if source.count(observation_marker) != 1: + raise SeedError("transaction surface: expected one observation boundary") + source = source.replace( + observation_marker, TRANSACTION_SURFACE + "\n" + observation_marker, 1 + ) + + old_observation_type = " int adaptation_ok;\n} VariantObservation;" + new_observation_type = ( + " int adaptation_ok;\n" + " AdaptationTransaction transaction;\n" + "} VariantObservation;" + ) + if source.count(old_observation_type) != 1: + raise SeedError("transaction surface: expected one variant observation type") + source = source.replace(old_observation_type, new_observation_type, 1) + + old_observation_call = """ adapt_model(&out->model, base_train, adapt_train, config, + &out->adaptation_metric, &out->replay_metric, &out->topology);""" + new_observation_call = """ adapt_model_transaction(&out->model, base_train, adapt_train, + retention, RETENTION_N, config, + &out->adaptation_metric, &out->replay_metric, + &out->topology, &out->transaction);""" + if source.count(old_observation_call) != 1: + raise SeedError("transaction surface: expected one trial adaptation call") + source = source.replace(old_observation_call, new_observation_call, 1) + + old_candidate_output = ( + ' printf(",\\"topology\\":");\n' + ' print_topology_json(&candidate.topology, &candidate.adaptation_metric);' + ) + new_candidate_output = ( + ' printf(",\\"topology\\":");\n' + ' print_topology_json(&candidate.topology, &candidate.adaptation_metric);\n' + ' printf(",\\"adaptation_transaction\\":");\n' + ' print_adaptation_transaction_json(&candidate.transaction);' + ) + if source.count(old_candidate_output) != 1: + raise SeedError("transaction surface: expected one candidate JSON boundary") + source = source.replace(old_candidate_output, new_candidate_output, 1) + return source diff --git a/tools/ravel_0_6_transaction_surface.py b/tools/ravel_0_6_transaction_surface.py new file mode 100644 index 0000000..c6a08bd --- /dev/null +++ b/tools/ravel_0_6_transaction_surface.py @@ -0,0 +1,253 @@ +"""Generated C surface for the RAVEL 0.6 retention transaction. + +This is deliberately injected into candidate-001 rather than compiled into the +frozen 0.5 translation unit. The old adaptation primitive remains available as +the behavior baseline; the trial path adds a copy, raw-observation, and +all-hard-gates commit boundary. +""" + +TRANSACTION_SURFACE = r''' +/* RAVEL 0.6 transaction surface: raw observations plus bounded commit. */ +typedef struct { + uint32_t objective_before_q20; + uint32_t objective_after_q20; + uint64_t base_accuracy_before_q20; + uint64_t base_accuracy_after_q20; + uint64_t representation_before_q20; + uint64_t representation_after_q20; + uint64_t prediction_rmse_before_q20; + uint64_t prediction_rmse_after_q20; + uint32_t transition_support_losses; + uint32_t expert_count; + uint64_t births; + uint64_t retirements; + uint64_t replay_records; + uint32_t update_passes; + uint64_t compute_evaluations; + uint64_t matched_compute_evaluations; + uint8_t matched_compute_reference_available; + uint32_t failed_constraint_mask; + uint8_t committed; + uint8_t rollback_byte_identical; + const char *threshold_identity; + const char *rejection_reason; +} AdaptationTransaction; + +enum { + RAVEL06_FAIL_MECHANISM = 1u << 0, + RAVEL06_FAIL_OBJECTIVE = 1u << 1, + RAVEL06_FAIL_BASE_ACCURACY = 1u << 2, + RAVEL06_FAIL_REPRESENTATION = 1u << 3, + RAVEL06_FAIL_PREDICTION = 1u << 4, + RAVEL06_FAIL_TRANSITION_SUPPORT = 1u << 5, + RAVEL06_FAIL_EXPERT_BUDGET = 1u << 6, + RAVEL06_FAIL_BIRTH_BUDGET = 1u << 7, + RAVEL06_FAIL_RETIREMENT_BUDGET = 1u << 8, + RAVEL06_FAIL_REPLAY_BUDGET = 1u << 9, + RAVEL06_FAIL_UPDATE_BUDGET = 1u << 10, + RAVEL06_FAIL_COMPUTE_BUDGET = 1u << 11 +}; + +#define RAVEL06_BASE_ACCURACY_FLOOR_Q20 UINT64_C(891290) +#define RAVEL06_PREDICTION_DEGRADATION_BOUND_Q20 UINT64_C(1048576) +#define RAVEL06_MAX_UPDATE_PASSES 4u +#define RAVEL06_MAX_COMPUTE_EVALUATIONS UINT64_C(2000000) + +static uint64_t ravel06_accuracy_q20(const Eval *evaluation) { + if (evaluation->samples == 0u) return 0u; + return (evaluation->correct * UINT64_C(1048576)) / evaluation->samples; +} + +static uint64_t ravel06_representation_q20(const Eval *evaluation) { + if (evaluation->samples == 0u) return UINT64_MAX; + return evaluation->reconstruction_sse_q20 / evaluation->samples; +} + +static uint64_t ravel06_prediction_q20(const Eval *evaluation) { + if (evaluation->prediction_samples == 0u) return UINT64_MAX; + double value = prediction_rmse(evaluation); + if (!isfinite(value) || value < 0.0) return UINT64_MAX; + return (uint64_t)q20(value); +} + +static int ravel06_edge_present(const Model *model, uint64_t source_lineage, + uint32_t action, uint64_t target_lineage) { + for (uint16_t source = 0; source < model->n; ++source) { + if (model->e[source].lineage != source_lineage) continue; + for (uint32_t slot = 0; slot < TRANSITION_TOP_K; ++slot) { + uint16_t target = model->next_graph[source][action][slot]; + if (target < model->n && + model->next_graph_support[source][action][slot] >= + TRANSITION_SUPPORT_MIN && + model->e[target].lineage == target_lineage) return 1; + } + } + return 0; +} + +static uint32_t ravel06_transition_losses(const Model *previous, + const Model *proposed) { + uint32_t losses = 0u; + for (uint16_t source = 0; source < previous->n; ++source) { + for (uint32_t action = 0; action < ACTIONS; ++action) { + for (uint32_t slot = 0; slot < TRANSITION_TOP_K; ++slot) { + uint16_t target = previous->next_graph[source][action][slot]; + if (target >= previous->n || + previous->next_graph_support[source][action][slot] < + TRANSITION_SUPPORT_MIN) continue; + if (!ravel06_edge_present(proposed, previous->e[source].lineage, + action, previous->e[target].lineage)) { + ++losses; + } + } + } + } + return losses; +} + +static const char *ravel06_rejection_reason(uint32_t mask) { + if (mask & RAVEL06_FAIL_MECHANISM) return "adaptation_mechanism_failed"; + if (mask & RAVEL06_FAIL_OBJECTIVE) return "adaptation_improvement_below_epsilon"; + if (mask & RAVEL06_FAIL_BASE_ACCURACY) return "base_accuracy_floor"; + if (mask & RAVEL06_FAIL_REPRESENTATION) return "representation_floor"; + if (mask & RAVEL06_FAIL_PREDICTION) return "original_prediction_degradation_bound"; + if (mask & RAVEL06_FAIL_TRANSITION_SUPPORT) return "transition_support_preservation"; + if (mask & RAVEL06_FAIL_EXPERT_BUDGET) return "expert_capacity_budget"; + if (mask & RAVEL06_FAIL_BIRTH_BUDGET) return "birth_budget"; + if (mask & RAVEL06_FAIL_RETIREMENT_BUDGET) return "retirement_budget"; + if (mask & RAVEL06_FAIL_REPLAY_BUDGET) return "replay_budget"; + if (mask & RAVEL06_FAIL_UPDATE_BUDGET) return "update_pass_budget"; + if (mask & RAVEL06_FAIL_COMPUTE_BUDGET) return "compute_budget"; + return "none"; +} + +static int adapt_model_transaction( + Model *model, const Event *base_train, const Event *adapt_train, + const Event *retention, uint32_t retention_count, const VariantConfig *config, + TrainMetric *metric, ReplayMetric *replay, TopologyTrace *topology, + AdaptationTransaction *transaction) { + Model previous = *model; + Model proposed = *model; + memset(transaction, 0, sizeof *transaction); + transaction->threshold_identity = "ravel-0.6-retention-gates/0.1"; + int mechanism_ok = adapt_model(&proposed, base_train, adapt_train, config, + metric, replay, topology); + canonicalize_model(&proposed); + transaction->objective_before_q20 = model_objective_q20( + &previous, adapt_train, ADAPT_TRAIN_N, NULL, 0u); + transaction->objective_after_q20 = model_objective_q20( + &proposed, adapt_train, ADAPT_TRAIN_N, NULL, 0u); + transaction->expert_count = proposed.n; + transaction->births = metric->births; + transaction->retirements = metric->retired; + transaction->replay_records = replay->selected; + transaction->update_passes = config->matched_work ? 4u : 2u; + transaction->compute_evaluations = metric->expert_evaluations; + transaction->matched_compute_evaluations = 0u; + transaction->matched_compute_reference_available = 0u; + + const Event *gate_data = retention != NULL ? retention : base_train; + uint32_t gate_count = retention != NULL ? retention_count : BASE_TRAIN_N; + Eval previous_eval = evaluate(&previous, gate_data, gate_count, config->routed); + Eval proposed_eval = evaluate(&proposed, gate_data, gate_count, config->routed); + transaction->base_accuracy_before_q20 = ravel06_accuracy_q20(&previous_eval); + transaction->base_accuracy_after_q20 = ravel06_accuracy_q20(&proposed_eval); + transaction->representation_before_q20 = + ravel06_representation_q20(&previous_eval); + transaction->representation_after_q20 = + ravel06_representation_q20(&proposed_eval); + transaction->prediction_rmse_before_q20 = ravel06_prediction_q20(&previous_eval); + transaction->prediction_rmse_after_q20 = ravel06_prediction_q20(&proposed_eval); + transaction->transition_support_losses = + ravel06_transition_losses(&previous, &proposed); + + uint32_t failed = 0u; + if (!mechanism_ok) failed |= RAVEL06_FAIL_MECHANISM; + if (transaction->objective_after_q20 < + transaction->objective_before_q20 + TOPOLOGY_OBJECTIVE_MIN_Q20) { + failed |= RAVEL06_FAIL_OBJECTIVE; + } + if (transaction->base_accuracy_after_q20 < RAVEL06_BASE_ACCURACY_FLOOR_Q20) { + failed |= RAVEL06_FAIL_BASE_ACCURACY; + } + if (transaction->representation_after_q20 > + transaction->representation_before_q20) { + failed |= RAVEL06_FAIL_REPRESENTATION; + } + if (transaction->prediction_rmse_before_q20 != UINT64_MAX && + transaction->prediction_rmse_after_q20 != UINT64_MAX && + transaction->prediction_rmse_after_q20 > + transaction->prediction_rmse_before_q20 + + RAVEL06_PREDICTION_DEGRADATION_BOUND_Q20) { + failed |= RAVEL06_FAIL_PREDICTION; + } + if (transaction->transition_support_losses != 0u) { + failed |= RAVEL06_FAIL_TRANSITION_SUPPORT; + } + if (transaction->expert_count > MAXE) failed |= RAVEL06_FAIL_EXPERT_BUDGET; + if (transaction->births > MAX_ADAPT_BIRTHS) failed |= RAVEL06_FAIL_BIRTH_BUDGET; + if (transaction->retirements > MAX_ADAPT_RETIREMENTS) { + failed |= RAVEL06_FAIL_RETIREMENT_BUDGET; + } + if (transaction->replay_records > REPLAY_N) failed |= RAVEL06_FAIL_REPLAY_BUDGET; + if (transaction->update_passes > RAVEL06_MAX_UPDATE_PASSES) { + failed |= RAVEL06_FAIL_UPDATE_BUDGET; + } + if (transaction->compute_evaluations > RAVEL06_MAX_COMPUTE_EVALUATIONS) { + failed |= RAVEL06_FAIL_COMPUTE_BUDGET; + } + transaction->failed_constraint_mask = failed; + transaction->rejection_reason = ravel06_rejection_reason(failed); + if (failed == 0u) { + *model = proposed; + transaction->committed = 1u; + transaction->rollback_byte_identical = 0u; + return 1; + } + *model = previous; + ByteBuffer before_bytes, after_bytes; + transaction->rollback_byte_identical = + serialize_checkpoint(&previous, &before_bytes) && + serialize_checkpoint(model, &after_bytes) && + before_bytes.len == after_bytes.len && + bytes_equal(before_bytes.data, after_bytes.data, before_bytes.len); + return 0; +} + +static void print_adaptation_transaction_json( + const AdaptationTransaction *transaction) { + printf("{\"committed\":%s,\"threshold_identity\":\"%s\"," + "\"rejection_reason\":\"%s\"," + "\"failed_constraint_mask\":%u,\"rollback_byte_identical\":%s," + "\"raw\":{\"objective_before_q20\":%u," + "\"objective_after_q20\":%u,\"base_accuracy_before_q20\":%" PRIu64 + ",\"base_accuracy_after_q20\":%" PRIu64 + ",\"representation_before_q20\":%" PRIu64 + ",\"representation_after_q20\":%" PRIu64 + ",\"prediction_rmse_before_q20\":%" PRIu64 + ",\"prediction_rmse_after_q20\":%" PRIu64 + ",\"transition_support_losses\":%u,\"expert_count\":%u" + ",\"births\":%" PRIu64 ",\"retirements\":%" PRIu64 + ",\"replay_records\":%" PRIu64 ",\"update_passes\":%u" + ",\"compute_evaluations\":%" PRIu64 + ",\"matched_compute_evaluations\":%" PRIu64 + ",\"matched_compute_reference_available\":%s}}", + transaction->committed ? "true" : "false", + transaction->threshold_identity, + transaction->rejection_reason, transaction->failed_constraint_mask, + transaction->rollback_byte_identical ? "true" : "false", + transaction->objective_before_q20, transaction->objective_after_q20, + transaction->base_accuracy_before_q20, + transaction->base_accuracy_after_q20, + transaction->representation_before_q20, + transaction->representation_after_q20, + transaction->prediction_rmse_before_q20, + transaction->prediction_rmse_after_q20, + transaction->transition_support_losses, transaction->expert_count, + transaction->births, transaction->retirements, + transaction->replay_records, transaction->update_passes, + transaction->compute_evaluations, + transaction->matched_compute_evaluations, + transaction->matched_compute_reference_available ? "true" : "false"); +} +'''