From 2dfedd2a7edbab12b7e301228d56b1416f172f78 Mon Sep 17 00:00:00 2001 From: epi13 Date: Sat, 8 Aug 2026 18:59:47 -0800 Subject: [PATCH] Integrate frozen RAVEL 0.6 development contract --- .github/workflows/tests.yml | 14 ++ README.md | 5 +- docs/ARCHITECTURE.md | 13 +- docs/EVIDENCE_GUIDE.md | 7 + docs/PROJECT_MAP.md | 7 + ravel_versions/0.6/Makefile | 14 +- .../0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md | 39 ++- ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md | 41 +++- .../0.6/ravel-0.6-matched-compute.schema.json | 18 ++ .../0.6/ravel-0.6-transaction.schema.json | 5 +- ravel_versions/0.6/ravel_0_6/README.md | 22 ++ src/ravel/__init__.py | 2 + src/ravel/adaptation.py | 44 +++- src/ravel/c_observations.py | 54 ++-- src/ravel/experience.py | 45 +++- src/ravel/lifecycle.py | 4 + src/ravel/matched_compute.py | 79 ++++++ src/ravel/policy.py | 230 ++++++++++++++++++ tests/test_frozen_identities.py | 30 +++ tests/test_lifecycle_experience.py | 70 ++++++ tests/test_policy.py | 44 ++++ tests/test_ravel_0_6_decomposition.py | 100 ++++++++ tests/test_ravel_0_6_negative_matrix.py | 177 ++++++++++++++ tests/test_ravel_0_6_provenance.py | 4 + tests/test_ravel_0_6_transaction.py | 11 +- tools/ravel_0_6_build.py | 71 +++++- tools/ravel_0_6_decompose.py | 75 ++++++ tools/ravel_0_6_seed_candidate.py | 132 ++++++++++ tools/ravel_0_6_transaction_surface.py | 114 +++++++-- 29 files changed, 1396 insertions(+), 75 deletions(-) create mode 100644 ravel_versions/0.6/ravel-0.6-matched-compute.schema.json create mode 100644 ravel_versions/0.6/ravel_0_6/README.md create mode 100644 src/ravel/matched_compute.py create mode 100644 src/ravel/policy.py create mode 100644 tests/test_frozen_identities.py create mode 100644 tests/test_policy.py create mode 100644 tests/test_ravel_0_6_decomposition.py create mode 100644 tests/test_ravel_0_6_negative_matrix.py create mode 100644 tools/ravel_0_6_decompose.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 603ef6c..cf05054 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,3 +22,17 @@ jobs: - run: python -m pip install --upgrade pip - run: python -m pip install -e . - run: python -m unittest discover -s tests -v + + ravel-06-c: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: python -m pip install --upgrade pip + - run: python -m pip install -e . + - run: make -f ravel_versions/0.6/Makefile policy-test + - run: make -f ravel_versions/0.6/Makefile build behavioral-test transaction-test component-test decomposition-test negative-test compiler-matrix sanitizers diff --git a/README.md b/README.md index 8f54b1a..282d571 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 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`. +> **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 digest-bound frozen-policy loader, matched-compute development observations, a behavior-preserving generated C component extraction, two real C providers, negative parity fixtures, and lifecycle/memory integration. It remains unfrozen and has not been 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 @@ -104,6 +104,9 @@ The recommended extraction is history-preserving rather than a manual file copy. 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). +The frozen 0.6 policy is loaded by `src/ravel/policy.py` and generated into +candidate build constants; `src/ravel/matched_compute.py` evaluates the +development-only comparator without granting the candidate authority. Optional provider/resource protocols in `src/ravel/providers.py` and `src/ravel/resources.py` record scoped execution observations without claiming algorithmic superiority. The bounded component surfaces in `src/ravel/world.py`, diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index da0fcbe..1b1d84c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -152,12 +152,17 @@ The repository now contains small, dependency-free foundations beneath the - `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. +- `ravel.policy` validates frozen 0.6 policy inputs and derives a digest-bound + C policy header; `ravel.matched_compute` independently evaluates raw + development comparator counts. 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. +observation, rollback, matched-compute, and provider surface around the +existing adaptation primitive. `tools/ravel_0_6_decompose.py` losslessly emits +ten provenance-bound component include units and a deterministic unity wrapper; +the wrapper preserves static linkage and byte-identical behavior to the +monolithic generated source. This is a physical source decomposition, not a +claim that independently compiled C ABI contracts or evaluator authority exist. These modules are tested scaffolding and are not claims that RAVEL 0.6 has been evaluated, selected, independently evaluated, certified, promoted, or made diff --git a/docs/EVIDENCE_GUIDE.md b/docs/EVIDENCE_GUIDE.md index 282bbcc..a9b8ff1 100644 --- a/docs/EVIDENCE_GUIDE.md +++ b/docs/EVIDENCE_GUIDE.md @@ -92,6 +92,13 @@ Wall-clock timing is host-specific and non-normative. Deterministic expert, operation, or evaluation counts are the canonical work measures unless a separate protocol explicitly defines cross-host performance evidence. +For RAVEL 0.6 development, `ravel.policy` binds the machine-readable +preregistration digest (and the explicitly inherited frozen 0.5 objective +constant) into the generated C threshold identity. The matched-compute record +contains raw candidate/comparator work counts and an integer ratio; the +candidate does not emit a formal compute PASS. The Python parser independently +reconstructs the ratio, and an absent comparator remains `UNKNOWN`. + ## Distinct result questions RAVEL reports several different questions that must not be collapsed: diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 4bc059a..a79ff99 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -61,6 +61,13 @@ regenerating the frozen records. 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. +- `src/ravel/policy.py` is the fail-closed frozen 0.6 policy loader; generated + C constants carry its threshold identity. +- `src/ravel/matched_compute.py` validates raw development comparator counts and + ratios without producing formal authority. +- `tools/ravel_0_6_decompose.py` losslessly emits generated C component units + and a unity wrapper; `ravel_versions/0.6/ravel_0_6/README.md` documents the + current unity-build limitation. - [`MIGRATION.md`](MIGRATION.md) records the standalone extraction provenance. ## Build and verification entry points diff --git a/ravel_versions/0.6/Makefile b/ravel_versions/0.6/Makefile index 049ba7f..0aa6454 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 transaction-test component-test compiler-matrix sanitizers +.PHONY: build behavioral-test transaction-test component-test policy-test decomposition-test negative-test provider-test compiler-matrix sanitizers build: @set -e; out=$$(mktemp -d); python3 tools/ravel_0_6_build.py build --output-dir "$$out"; \ @@ -16,6 +16,18 @@ transaction-test: component-test: python3 -m unittest tests/test_components.py tests/test_lifecycle_experience.py +policy-test: + python3 -m unittest tests/test_policy.py tests/test_frozen_identities.py + +decomposition-test: + python3 -m unittest tests/test_ravel_0_6_decomposition.py + +negative-test: + python3 -m unittest tests/test_ravel_0_6_negative_matrix.py + +provider-test: + RAVEL06_PROVIDER=branching python3 -m unittest tests/test_ravel_0_6_decomposition.py + compiler-matrix: @set -eu; for compiler in gcc clang; do \ if command -v "$$compiler" >/dev/null 2>&1; then \ 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 ea93d42..925a5fa 100644 --- a/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md +++ b/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md @@ -9,6 +9,11 @@ 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. +- **Frozen policy source of truth:** `ravel.policy` validates the frozen 0.6 + preregistration and the explicitly inherited frozen 0.5 objective constant, + derives transaction limits, and binds a deterministic threshold identity. + The 0.6 record declares no numeric absolute compute cap or representation + number; those remain explicit relative/`UNKNOWN` contract fields. - **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, @@ -17,19 +22,28 @@ This is a development status record, not RAVEL 0.6 evaluation evidence. 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-02 policy and matched-compute integration:** the C transaction uses the + digest-bound two-pass, exact-256 replay, separate retention-floor/loss, and + frozen ratio constants. Development trials emit raw candidate and + fixed-topology work counts with a reconstructible ratio; Python derives the + ratio independently without trusting the C disposition. - **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 stable checksums. A C transaction mutation fixture also forces a retention gate failure and observes rollback. +- **R6-04 physical extraction and providers:** the generated source is + losslessly emitted as ten component units plus a deterministic unity wrapper. + The C mechanism has explicit branching and ring providers selected by a + recorded compile flag; provider identity and evidence change while core + source remains unchanged. - **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. + observation surface. The extraction is a reviewed unity-build boundary; + independently compiled C ABI contracts are not claimed. - **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, @@ -39,18 +53,21 @@ This is a development status record, not RAVEL 0.6 evaluation evidence. scoped execution outcomes into advisory episodic or negative memory records. It preserves `UNKNOWN`, rejection reasons, resource observations, and provenance rather than promoting remembered outcomes. +- **Execution-to-memory integration:** raw C development transaction records + convert into uniquely identified, scoped advisory memory. Accepted raw + executions remain `UNKNOWN` until governed disposition exists; rejected and + unavailable outcomes remain negative and deterministic retrieval includes + them. ## Not yet implemented or externally unavailable - 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. + JSON record and shared reason vocabulary, with accepted and negative-path + cross-checks. The parser/evaluator remains advisory and does not create + formal evidence status; missing external disposition remains `UNKNOWN`. +- Separately compiled C ABI contracts, full evaluator integration, and an + absolute compute budget remain incomplete or are not declared by the frozen + contract. - 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`. 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 6d141fb..4647c31 100644 --- a/ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md +++ b/ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md @@ -49,13 +49,40 @@ unfrozen, and unevaluated for preregistered selection/final purposes. Candidate-001 is now integrated as a reproducible temporary development build 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. +structured observations with stable rejection reasons. The frozen policy is +loaded once at generation time, and matched-compute work is emitted as a +separate raw development record. A compiled behavioral harness verifies both +corrections and fails against frozen 0.5 behavior and bounded reverted +mutations. Python component surfaces and the generated C unity extraction +provide two independent 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. + +### Queue disposition after the current iteration + +- **R6-01:** provenance/build foundation complete; generated component and + policy identities are now included in temporary build records. +- **R6-02:** integrated for the local development transaction and matched raw + comparator; the frozen contract has no numeric absolute compute cap, so that + separate gate remains undeclared rather than invented. +- **R6-03:** behavioral and C/Python negative fixtures complete for declared + constraints; source-constant mutations are intentionally distinguished from + Python frozen-policy mutations and remain manifest-detectable. +- **R6-04:** physical source extraction and C provider substitution are + implemented as a behavior-preserving unity build. Independently compiled C + ABI contracts remain the next architectural step. +- **R6-05:** ledger infrastructure is hardened and exercised only with + development fixtures. Candidate-001 remains unfrozen because further + implementation changes are still expected. +- **R6-06:** external custody and evaluation remain unavailable/`UNKNOWN`. + +The next logical implementation task is to promote one extracted C boundary at +a time to separately compiled units with explicit headers, retaining the unity +wrapper as a parity oracle. Do not freeze candidate-001 or consume selection +partitions until that work, policy parity, and the full development trial +matrix are stable. ## Codex next steps diff --git a/ravel_versions/0.6/ravel-0.6-matched-compute.schema.json b/ravel_versions/0.6/ravel-0.6-matched-compute.schema.json new file mode 100644 index 0000000..2d8ec49 --- /dev/null +++ b/ravel_versions/0.6/ravel-0.6-matched-compute.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ravel.dev/schema/ravel-0.6-matched-compute/0.1", + "title": "RAVEL 0.6 development matched-compute observation", + "type": "object", + "additionalProperties": false, + "required": ["candidate_training_evaluations", "matched_training_evaluations", "ratio_q20", "maximum_ratio_q20", "reference_available", "threshold_identity", "comparator_identity", "partition_identity"], + "properties": { + "candidate_training_evaluations": {"type": "integer", "minimum": 0}, + "matched_training_evaluations": {"type": "integer", "minimum": 0}, + "ratio_q20": {"type": "integer", "minimum": 0}, + "maximum_ratio_q20": {"type": "integer", "minimum": 0}, + "reference_available": {"type": "boolean"}, + "threshold_identity": {"type": "string", "minLength": 1}, + "comparator_identity": {"type": "string", "minLength": 1}, + "partition_identity": {"type": "string", "minLength": 1} + } +} diff --git a/ravel_versions/0.6/ravel-0.6-transaction.schema.json b/ravel_versions/0.6/ravel-0.6-transaction.schema.json index f2521d3..5041208 100644 --- a/ravel_versions/0.6/ravel-0.6-transaction.schema.json +++ b/ravel_versions/0.6/ravel-0.6-transaction.schema.json @@ -14,12 +14,15 @@ "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"], + "required": ["objective_before_q20", "objective_after_q20", "base_accuracy_before_q20", "base_accuracy_after_q20", "retention_accuracy_before_q20", "retention_accuracy_after_q20", "retention_accuracy_delta_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}, + "retention_accuracy_before_q20": {"type": "integer", "minimum": 0}, + "retention_accuracy_after_q20": {"type": "integer", "minimum": 0}, + "retention_accuracy_delta_q20": {"type": "integer"}, "representation_before_q20": {"type": "integer", "minimum": 0}, "representation_after_q20": {"type": "integer", "minimum": 0}, "prediction_rmse_before_q20": {"type": "integer", "minimum": 0}, diff --git a/ravel_versions/0.6/ravel_0_6/README.md b/ravel_versions/0.6/ravel_0_6/README.md new file mode 100644 index 0000000..6f26725 --- /dev/null +++ b/ravel_versions/0.6/ravel_0_6/README.md @@ -0,0 +1,22 @@ +# RAVEL 0.6 generated component boundary + +The 0.6 development build derives candidate-001 from the frozen 0.5 source, +then losslessly splits the generated text into bounded include units: + +- preamble and shared types; +- core mechanism state and learning; +- world/provider generation; +- transition compilation; +- planning; +- checkpoint codec; +- observations and adaptation transaction; +- reporting; +- trial driver. + +The current extraction compiles these pieces through a deterministic unity +wrapper. This preserves the generated candidate's static linkage and behavior +while making every component byte-addressable in the build manifest. It is a +physical source decomposition, not a claim that independently compiled C ABI +contracts or evaluator authority already exist. Promoting a component to a +separately compiled unit requires a separately reviewed header contract and a +new parity fixture. diff --git a/src/ravel/__init__.py b/src/ravel/__init__.py index c684a7a..9a64922 100644 --- a/src/ravel/__init__.py +++ b/src/ravel/__init__.py @@ -6,9 +6,11 @@ "c_observations", "experience", "lifecycle", + "matched_compute", "memory", "mechanism_state", "planning", + "policy", "providers", "resources", "transition", diff --git a/src/ravel/adaptation.py b/src/ravel/adaptation.py index b5077af..734443c 100644 --- a/src/ravel/adaptation.py +++ b/src/ravel/adaptation.py @@ -47,6 +47,8 @@ class RawObservation: update_passes: int compute_evaluations: int matched_compute_evaluations: int + retention_accuracy: float | None = None + retention_accuracy_delta_from_base: float | None = None def __post_init__(self) -> None: for name in ( @@ -56,6 +58,10 @@ def __post_init__(self) -> None: "original_prediction_degradation", ): _finite(getattr(self, name), name) + for name in ("retention_accuracy", "retention_accuracy_delta_from_base"): + value = getattr(self, name) + if value is not None: + _finite(value, name) for name in ( "transition_support_losses", "expert_count", @@ -86,8 +92,11 @@ class RetentionConstraintPolicy: maximum_retirements: int maximum_replay_records: int maximum_update_passes: int - maximum_compute_evaluations: int + maximum_compute_evaluations: int | None maximum_compute_ratio: float + retention_accuracy_floor: float | None = None + retention_loss_floor: float | None = None + exact_replay_records: int | None = None def __post_init__(self) -> None: for name in ( @@ -107,9 +116,19 @@ def __post_init__(self) -> None: "maximum_retirements", "maximum_replay_records", "maximum_update_passes", - "maximum_compute_evaluations", ): _nonnegative(getattr(self, name), name) + if self.maximum_compute_evaluations is not None: + _nonnegative(self.maximum_compute_evaluations, "maximum_compute_evaluations") + for name in ("retention_accuracy_floor", "retention_loss_floor", "exact_replay_records"): + value = getattr(self, name) + if value is not None: + if name == "exact_replay_records": + _nonnegative(value, name) + else: + _finite(value, name) + if name == "retention_accuracy_floor" and value < 0: + raise AdaptationInputError(f"{name} must be non-negative") @dataclass(frozen=True, slots=True) @@ -142,6 +161,18 @@ def evaluate_constraints( reasons.append("representation_floor") if proposed.original_prediction_degradation > policy.original_prediction_degradation_bound: reasons.append("original_prediction_degradation_bound") + if ( + policy.retention_accuracy_floor is not None + and proposed.retention_accuracy is not None + and proposed.retention_accuracy < policy.retention_accuracy_floor + ): + reasons.append("retention_accuracy_floor") + if ( + policy.retention_loss_floor is not None + and proposed.retention_accuracy_delta_from_base is not None + and proposed.retention_accuracy_delta_from_base < policy.retention_loss_floor + ): + reasons.append("retention_loss_floor") if proposed.transition_support_losses > policy.maximum_transition_support_losses: reasons.append("transition_support_preservation") if proposed.expert_count > policy.maximum_experts: @@ -150,11 +181,16 @@ def evaluate_constraints( reasons.append("birth_budget") if proposed.retirements > policy.maximum_retirements: reasons.append("retirement_budget") - if proposed.replay_records > policy.maximum_replay_records: + if policy.exact_replay_records is not None and proposed.replay_records != policy.exact_replay_records: + reasons.append("replay_budget") + elif proposed.replay_records > policy.maximum_replay_records: reasons.append("replay_budget") if proposed.update_passes > policy.maximum_update_passes: reasons.append("update_pass_budget") - if proposed.compute_evaluations > policy.maximum_compute_evaluations: + if ( + policy.maximum_compute_evaluations is not None + and proposed.compute_evaluations > policy.maximum_compute_evaluations + ): reasons.append("compute_budget") if proposed.matched_compute_evaluations == 0: reasons.append("matched_compute_reference_unavailable") diff --git a/src/ravel/c_observations.py b/src/ravel/c_observations.py index 893ebc1..4f0019d 100644 --- a/src/ravel/c_observations.py +++ b/src/ravel/c_observations.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Any, Mapping from .adaptation import ( @@ -11,6 +11,8 @@ RetentionConstraintPolicy, evaluate_constraints, ) +from .policy import load_frozen_policy +from .matched_compute import MatchedComputeObservation Q20 = 1_048_576.0 @@ -49,6 +51,11 @@ def count(name: str) -> int: after_representation = q20("representation_after_q20") previous_representation_score = 1.0 / (1.0 + before_representation) proposed_representation_score = 1.0 / (1.0 + after_representation) + retention_before = q20("retention_accuracy_before_q20") + retention_after = q20("retention_accuracy_after_q20") + retention_delta = raw.get("retention_accuracy_delta_q20") + if not isinstance(retention_delta, int): + raise ValueError("retention_accuracy_delta_q20 is malformed") previous = RawObservation( adaptation_objective=q20("objective_before_q20"), base_accuracy=q20("base_accuracy_before_q20"), @@ -62,6 +69,8 @@ def count(name: str) -> int: update_passes=0, compute_evaluations=0, matched_compute_evaluations=0, + retention_accuracy=retention_before, + retention_accuracy_delta_from_base=0.0, ) before_prediction = q20("prediction_rmse_before_q20") after_prediction = q20("prediction_rmse_after_q20") @@ -78,6 +87,8 @@ def count(name: str) -> int: update_passes=count("update_passes"), compute_evaluations=count("compute_evaluations"), matched_compute_evaluations=count("matched_compute_evaluations"), + retention_accuracy=retention_after, + retention_accuracy_delta_from_base=retention_delta / Q20, ) committed = value.get("committed") rollback = value.get("rollback_byte_identical") @@ -104,21 +115,36 @@ def count(name: str) -> int: rollback, ) - def evaluate(self) -> ConstraintReport: + def evaluate( + self, matched_compute: MatchedComputeObservation | None = None + ) -> ConstraintReport: """Apply the existing Python evaluator to the C observation pair.""" + frozen = load_frozen_policy() + if self.threshold_identity != frozen.threshold_identity: + return ConstraintReport(False, ("threshold_identity_mismatch",)) policy = RetentionConstraintPolicy( - adaptation_improvement_epsilon=105.0 / Q20, - base_accuracy_floor=891290.0 / Q20, + adaptation_improvement_epsilon=frozen.adaptation_epsilon_q20 / Q20, + base_accuracy_floor=frozen.base_accuracy_floor_q20 / 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, + original_prediction_degradation_bound=frozen.prediction_degradation_bound_q20 / Q20, + maximum_transition_support_losses=frozen.maximum_transition_support_losses, + maximum_experts=frozen.maximum_experts, + maximum_births=frozen.maximum_births, + maximum_retirements=frozen.maximum_retirements, + maximum_replay_records=frozen.replay_records, + maximum_update_passes=frozen.maximum_update_passes, + maximum_compute_evaluations=frozen.maximum_compute_evaluations, + maximum_compute_ratio=frozen.maximum_compute_ratio_q20 / Q20, + retention_accuracy_floor=frozen.retention_accuracy_floor_q20 / Q20, + retention_loss_floor=frozen.retention_loss_floor_q20 / Q20, + exact_replay_records=frozen.replay_records, ) - return evaluate_constraints(self.previous, self.proposed, policy) + proposed = self.proposed + if matched_compute is not None: + proposed = replace( + proposed, + compute_evaluations=matched_compute.candidate_training_evaluations, + matched_compute_evaluations=matched_compute.matched_training_evaluations, + ) + return evaluate_constraints(self.previous, proposed, policy) diff --git a/src/ravel/experience.py b/src/ravel/experience.py index 0bf6fc4..7825faa 100644 --- a/src/ravel/experience.py +++ b/src/ravel/experience.py @@ -3,6 +3,8 @@ from __future__ import annotations from dataclasses import dataclass, field +import hashlib +import json from typing import Any, Mapping from .memory import MemoryClass, MemoryRecord @@ -25,6 +27,7 @@ class ExperienceRecord: 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) + execution_identity: str | None = None def __post_init__(self) -> None: if not self.candidate_id or not self.context_identity or not self.task_environment: @@ -38,7 +41,47 @@ def negative(self) -> bool: @property def record_id(self) -> str: - return f"experience:{self.candidate_id}:{self.context_identity}" + suffix = f":{self.execution_identity}" if self.execution_identity else "" + return f"experience:{self.candidate_id}:{self.context_identity}{suffix}" + + @classmethod + def from_development_transaction( + cls, + *, + candidate_id: str, + context_identity: str, + task_environment: str, + provider_id: str, + transaction: Mapping[str, Any], + matched_compute: Mapping[str, Any] | None = None, + partition_identity: str = "ravel-0.6-development-adaptation-v1", + provenance: Mapping[str, str] | None = None, + ) -> "ExperienceRecord": + """Convert raw C development output into advisory, fail-closed memory.""" + + committed = transaction.get("committed") + if not isinstance(committed, bool): + raise ValueError("development transaction committed flag is malformed") + raw_result: dict[str, Any] = {"transaction": dict(transaction)} + if matched_compute is not None: + raw_result["matched_compute"] = dict(matched_compute) + material = json.dumps(raw_result, sort_keys=True, separators=(",", ":")).encode() + execution_identity = hashlib.sha256(material).hexdigest()[:24] + return cls( + candidate_id=candidate_id, + context_identity=context_identity, + task_environment=task_environment, + requested_strategy="retention-constrained-adaptation", + provider_id=provider_id, + verifier_id="development-raw-observation", + raw_result=raw_result, + formal_disposition="UNKNOWN", + adaptation_decision="accepted" if committed else "rejected", + rejection_reason=transaction.get("rejection_reason") if not committed else None, + provenance=dict(provenance or {}), + applicability_scope={"partition": partition_identity}, + execution_identity=execution_identity, + ) def to_memory_record(self, *, created_at: str) -> MemoryRecord: scope = dict(self.applicability_scope) diff --git a/src/ravel/lifecycle.py b/src/ravel/lifecycle.py index 2e41056..0c2a1af 100644 --- a/src/ravel/lifecycle.py +++ b/src/ravel/lifecycle.py @@ -113,6 +113,10 @@ def _current(self) -> dict[str, CandidateRecord]: 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)), ) + ordered = sorted(current.values(), key=lambda item: item.number) + for expected, record in enumerate(ordered, start=1): + if record.number != expected or record.candidate_id != candidate_id(expected): + raise LedgerError("candidate numbering has a gap or identity mutation") return current def records(self) -> tuple[CandidateRecord, ...]: diff --git a/src/ravel/matched_compute.py b/src/ravel/matched_compute.py new file mode 100644 index 0000000..34e674d --- /dev/null +++ b/src/ravel/matched_compute.py @@ -0,0 +1,79 @@ +"""Development-only matched-compute observations and fail-closed evaluation.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Mapping + +from .adaptation import ConstraintReport +from .policy import load_frozen_policy + + +Q20 = 1_048_576 + + +@dataclass(frozen=True, slots=True) +class MatchedComputeObservation: + candidate_training_evaluations: int + matched_training_evaluations: int + ratio_q20: int + maximum_ratio_q20: int + reference_available: bool + threshold_identity: str + comparator_identity: str + partition_identity: str + + @classmethod + def from_json(cls, value: Mapping[str, Any]) -> "MatchedComputeObservation": + def count(name: str) -> int: + result = value.get(name) + if not isinstance(result, int) or result < 0: + raise ValueError(f"matched-compute field {name} is malformed") + return result + + ratio = count("ratio_q20") + maximum = count("maximum_ratio_q20") + available = value.get("reference_available") + comparator = value.get("comparator_identity") + partition = value.get("partition_identity") + threshold = value.get("threshold_identity") + if not isinstance(available, bool): + raise ValueError("matched-compute availability is malformed") + if not isinstance(comparator, str) or not comparator: + raise ValueError("matched-compute comparator identity is malformed") + if not isinstance(partition, str) or not partition: + raise ValueError("matched-compute partition identity is malformed") + if not isinstance(threshold, str) or not threshold: + raise ValueError("matched-compute threshold identity is malformed") + result = cls( + count("candidate_training_evaluations"), + count("matched_training_evaluations"), + ratio, + maximum, + available, + threshold, + comparator, + partition, + ) + expected = ( + result.candidate_training_evaluations * Q20 + // result.matched_training_evaluations + if result.matched_training_evaluations + else 0 + ) + if result.ratio_q20 != expected: + raise ValueError("matched-compute ratio does not reconstruct from raw counts") + return result + + def evaluate(self) -> ConstraintReport: + policy = load_frozen_policy() + reasons: list[str] = [] + if self.threshold_identity != policy.threshold_identity: + reasons.append("threshold_identity_mismatch") + if not self.reference_available or self.matched_training_evaluations == 0: + reasons.append("matched_compute_reference_unavailable") + if self.maximum_ratio_q20 != policy.maximum_compute_ratio_q20: + reasons.append("threshold_identity_mismatch") + if self.ratio_q20 > policy.maximum_compute_ratio_q20: + reasons.append("matched_compute_ratio") + return ConstraintReport(not reasons, tuple(reasons)) diff --git a/src/ravel/policy.py b/src/ravel/policy.py new file mode 100644 index 0000000..81472e6 --- /dev/null +++ b/src/ravel/policy.py @@ -0,0 +1,230 @@ +"""Validated, immutable policy extraction for the RAVEL 0.6 development epoch. + +The machine-readable 0.6 preregistration is authoritative for the epoch. A +small amount of mechanism behavior is inherited from the frozen 0.5 surface: +the 0.6 record says that adaptation is added around that surface but does not +repeat its numeric objective epsilon. That inherited value is loaded from the +frozen 0.5 preregistration and both digests are bound into the policy identity. + +This module never loads a mutable runtime policy. Callers may inspect the +typed result or generate immutable build-time constants from it. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from decimal import Decimal, ROUND_HALF_UP +import hashlib +import json +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[2] +PREREGISTRATION = ROOT / "ravel_versions/0.6/ravel-0.6-preregistration.json" +FROZEN_0_5_PREREGISTRATION = ROOT / "ravel_versions/0.5/ravel-0.5-preregistration.json" +EXPECTED_PREREGISTRATION_SHA256 = ( + "26ae0b001355c978dbb2bda57fd7bcd74a3b3d4e46f45fa0b9658d88fcc885a3" +) +EXPECTED_0_5_PREREGISTRATION_SHA256 = ( + "f240c391b92823471132ffce1eeed154b3f03dc2af1e3e1f789690a99eb4cfaa" +) +Q20 = 1_048_576 + + +class PolicyError(ValueError): + """Raised when an epoch policy is absent, malformed, or mutated.""" + + +def _sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _q20(value: Decimal) -> int: + return int((value * Decimal(Q20)).to_integral_value(rounding=ROUND_HALF_UP)) + + +def _required(mapping: dict[str, Any], key: str) -> Any: + if key not in mapping: + raise PolicyError(f"frozen policy field is missing: {key}") + return mapping[key] + + +@dataclass(frozen=True, slots=True) +class FrozenRavel06Policy: + """The subset of frozen policy consumed by development transactions.""" + + preregistration_id: str + preregistration_sha256: str + inherited_05_preregistration_sha256: str + adaptation_epsilon_q20: int + base_accuracy_floor_q20: int + retention_accuracy_floor_q20: int + retention_loss_floor_q20: int + prediction_degradation_bound_q20: int + maximum_transition_support_losses: int + maximum_experts: int + maximum_births: int + maximum_retirements: int + replay_records: int + maximum_update_passes: int + maximum_compute_evaluations: int | None + maximum_compute_ratio_q20: int + maximum_candidate_identities: int + representation_floor_mode: str + + @property + def threshold_identity(self) -> str: + contract = { + "preregistration_id": self.preregistration_id, + "preregistration_sha256": self.preregistration_sha256, + "inherited_05_preregistration_sha256": self.inherited_05_preregistration_sha256, + "adaptation_epsilon_q20": self.adaptation_epsilon_q20, + "base_accuracy_floor_q20": self.base_accuracy_floor_q20, + "retention_accuracy_floor_q20": self.retention_accuracy_floor_q20, + "retention_loss_floor_q20": self.retention_loss_floor_q20, + "prediction_degradation_bound_q20": self.prediction_degradation_bound_q20, + "maximum_transition_support_losses": self.maximum_transition_support_losses, + "maximum_experts": self.maximum_experts, + "maximum_births": self.maximum_births, + "maximum_retirements": self.maximum_retirements, + "replay_records": self.replay_records, + "maximum_update_passes": self.maximum_update_passes, + "maximum_compute_evaluations": self.maximum_compute_evaluations, + "maximum_compute_ratio_q20": self.maximum_compute_ratio_q20, + "maximum_candidate_identities": self.maximum_candidate_identities, + "representation_floor_mode": self.representation_floor_mode, + } + digest = _sha256(json.dumps(contract, sort_keys=True, separators=(",", ":")).encode()) + return f"ravel-0.6-frozen-policy/{digest}" + + def to_dict(self) -> dict[str, Any]: + return { + "preregistration_id": self.preregistration_id, + "preregistration_sha256": self.preregistration_sha256, + "inherited_05_preregistration_sha256": self.inherited_05_preregistration_sha256, + "adaptation_epsilon_q20": self.adaptation_epsilon_q20, + "base_accuracy_floor_q20": self.base_accuracy_floor_q20, + "retention_accuracy_floor_q20": self.retention_accuracy_floor_q20, + "retention_loss_floor_q20": self.retention_loss_floor_q20, + "prediction_degradation_bound_q20": self.prediction_degradation_bound_q20, + "maximum_transition_support_losses": self.maximum_transition_support_losses, + "maximum_experts": self.maximum_experts, + "maximum_births": self.maximum_births, + "maximum_retirements": self.maximum_retirements, + "replay_records": self.replay_records, + "maximum_update_passes": self.maximum_update_passes, + "maximum_compute_evaluations": self.maximum_compute_evaluations, + "maximum_compute_ratio_q20": self.maximum_compute_ratio_q20, + "maximum_candidate_identities": self.maximum_candidate_identities, + "representation_floor_mode": self.representation_floor_mode, + "threshold_identity": self.threshold_identity, + } + + +def load_frozen_policy( + preregistration_path: Path = PREREGISTRATION, + inherited_05_path: Path = FROZEN_0_5_PREREGISTRATION, +) -> FrozenRavel06Policy: + """Load and validate the frozen policy, rejecting any identity mutation.""" + + preregistration_bytes = preregistration_path.read_bytes() + preregistration_sha = _sha256(preregistration_bytes) + if preregistration_sha != EXPECTED_PREREGISTRATION_SHA256: + raise PolicyError( + "RAVEL 0.6 preregistration identity mismatch: " + f"expected {EXPECTED_PREREGISTRATION_SHA256}, got {preregistration_sha}" + ) + inherited_bytes = inherited_05_path.read_bytes() + inherited_sha = _sha256(inherited_bytes) + if inherited_sha != EXPECTED_0_5_PREREGISTRATION_SHA256: + raise PolicyError( + "inherited RAVEL 0.5 policy identity mismatch: " + f"expected {EXPECTED_0_5_PREREGISTRATION_SHA256}, got {inherited_sha}" + ) + try: + prereg = json.loads(preregistration_bytes) + inherited = json.loads(inherited_bytes) + except json.JSONDecodeError as error: + raise PolicyError(f"frozen policy JSON is malformed: {error}") from error + if prereg.get("status") != "PREREGISTERED_BEFORE_IMPLEMENTATION": + raise PolicyError("RAVEL 0.6 preregistration status is not frozen") + if prereg.get("normative_for_epoch") is not True: + raise PolicyError("RAVEL 0.6 preregistration is not normative for its epoch") + + budget = _required(_required(prereg, "mechanism"), "budget") + common = _required(_required(prereg, "hard_gates"), "common") + gates = {gate["gate_id"]: gate for gate in common} + for gate_id in ( + "base_holdout_accuracy", + "base_holdout_retention", + "retention_loss_floor", + "old_prediction_retention", + "transition_unique_support", + "matched_compute_budget", + ): + if gate_id not in gates: + raise PolicyError(f"frozen common gate is missing: {gate_id}") + + inherited_constants = _required(inherited, "mechanism_constants") + inherited_epsilon = inherited_constants.get("topology_objective_min_q20") + if not isinstance(inherited_epsilon, int) or inherited_epsilon < 0: + raise PolicyError("frozen 0.5 inherited objective epsilon is malformed") + def numeric_gate(gate_id: str, operator: str) -> Decimal: + gate = gates[gate_id] + if gate.get("operator") != operator or not isinstance(gate.get("value"), (int, float)): + raise PolicyError(f"frozen gate {gate_id} has unexpected operator/value") + return Decimal(str(gate["value"])) + + return FrozenRavel06Policy( + preregistration_id=str(_required(prereg, "preregistration_id")), + preregistration_sha256=preregistration_sha, + inherited_05_preregistration_sha256=inherited_sha, + adaptation_epsilon_q20=inherited_epsilon, + base_accuracy_floor_q20=_q20(numeric_gate("base_holdout_accuracy", "ge")), + retention_accuracy_floor_q20=_q20(numeric_gate("base_holdout_retention", "ge")), + retention_loss_floor_q20=_q20(numeric_gate("retention_loss_floor", "ge")), + prediction_degradation_bound_q20=_q20(numeric_gate("old_prediction_retention", "le")), + maximum_transition_support_losses=int(numeric_gate("transition_unique_support", "eq")), + maximum_experts=int(budget["maximum_experts"]), + maximum_births=int(budget["maximum_births_per_trial"]), + maximum_retirements=int(budget["maximum_retirements_per_trial"]), + replay_records=int(budget["replay_records"]), + maximum_update_passes=int(budget["maximum_objective_tested_update_passes"]), + maximum_compute_evaluations=None, + maximum_compute_ratio_q20=_q20( + numeric_gate("matched_compute_budget", "le") + ), + maximum_candidate_identities=int(budget["maximum_candidate_identities"]), + representation_floor_mode="non_decreasing_from_previous_checkpoint", + ) + + +def policy_c_header(policy: FrozenRavel06Policy | None = None) -> str: + """Return deterministic C constants generated from the frozen policy.""" + + policy = load_frozen_policy() if policy is None else policy + maximum_compute = ( + "UINT64_MAX" + if policy.maximum_compute_evaluations is None + else f"UINT64_C({policy.maximum_compute_evaluations})" + ) + return "\n".join( + ( + "/* generated from frozen RAVEL 0.6 policy; do not edit */", + f'#define RAVEL06_THRESHOLD_IDENTITY "{policy.threshold_identity}"', + f"#define RAVEL06_OBJECTIVE_EPSILON_Q20 UINT64_C({policy.adaptation_epsilon_q20})", + f"#define RAVEL06_BASE_ACCURACY_FLOOR_Q20 UINT64_C({policy.base_accuracy_floor_q20})", + f"#define RAVEL06_RETENTION_ACCURACY_FLOOR_Q20 UINT64_C({policy.retention_accuracy_floor_q20})", + f"#define RAVEL06_RETENTION_LOSS_FLOOR_Q20 INT64_C({policy.retention_loss_floor_q20})", + f"#define RAVEL06_PREDICTION_DEGRADATION_BOUND_Q20 UINT64_C({policy.prediction_degradation_bound_q20})", + f"#define RAVEL06_MAX_TRANSITION_SUPPORT_LOSSES {policy.maximum_transition_support_losses}u", + f"#define RAVEL06_MAX_EXPERTS {policy.maximum_experts}u", + f"#define RAVEL06_MAX_BIRTHS {policy.maximum_births}u", + f"#define RAVEL06_MAX_RETIREMENTS {policy.maximum_retirements}u", + f"#define RAVEL06_REPLAY_RECORDS {policy.replay_records}u", + f"#define RAVEL06_MAX_UPDATE_PASSES {policy.maximum_update_passes}u", + f"#define RAVEL06_MAX_COMPUTE_EVALUATIONS {maximum_compute}", + f"#define RAVEL06_MAX_COMPUTE_RATIO_Q20 UINT64_C({policy.maximum_compute_ratio_q20})", + ) + ) diff --git a/tests/test_frozen_identities.py b/tests/test_frozen_identities.py new file mode 100644 index 0000000..307e29a --- /dev/null +++ b/tests/test_frozen_identities.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +FROZEN_IDENTITIES = { + "ravel_versions/0.4/ravel_0_4.c": "5243022245bce97b2e3be6dd46e397d33445c25469b8ce9a364c6a104e757cd4", + "ravel_versions/0.4/ravel-0.4-source-manifest.json": "c3b590a93313c929ef667f7c41100eb51a130c5015b2b2b7789bb6bf2033d768", + "ravel_versions/0.4/ravel-0.4-trial-evidence.json": "a5ffbfdbf2f46274413edf0644df2afa36df27da629c777bcf59b1f6e79066aa", + "ravel_versions/0.5/ravel_0_5.c": "1a8466ea1805811873c461fb891aaeaec18f6c9e7491b5ea7bd09bf698be102d", + "ravel_versions/0.5/ravel-0.5-source-and-execution-manifest.json": "18006006db509269ee374a39133bb25d8452edc0fe0103a43fa92c5660fd89d0", + "ravel_versions/0.5/ravel-0.5-trial-evidence.json": "9ffefe97e5331b65e5b998f9c2d3aac91cdf9cca246a377ca421a3bef0ba0e80", +} + + +class FrozenIdentityTests(unittest.TestCase): + def test_historical_source_and_evidence_identities_are_unchanged(self) -> None: + for relative, expected in FROZEN_IDENTITIES.items(): + with self.subTest(path=relative): + digest = hashlib.sha256((ROOT / relative).read_bytes()).hexdigest() + self.assertEqual(digest, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_lifecycle_experience.py b/tests/test_lifecycle_experience.py index a5009ea..298f614 100644 --- a/tests/test_lifecycle_experience.py +++ b/tests/test_lifecycle_experience.py @@ -5,6 +5,8 @@ from ravel.experience import ExperienceRecord from ravel.lifecycle import CandidateLedger, CandidateState, LedgerError +from ravel.memory import MemoryClass +from ravel.memory.store import SQLiteMemoryStore class LifecycleTests(unittest.TestCase): @@ -63,6 +65,41 @@ def test_ledger_mutation_and_candidate_limit_fail_closed(self) -> None: with self.assertRaises(LedgerError): ledger.get(candidate.candidate_id) + def test_candidate_number_gap_is_detected(self) -> None: + with tempfile.TemporaryDirectory() as directory: + ledger = CandidateLedger(f"{directory}/candidates.jsonl", maximum_candidates=8) + first = ledger.create(development_partition="dev", created_at="t0") + ledger._append( + "ravel-0.6-candidate-003", + CandidateState.CREATED, + {"number": 3, "development_partition": "dev", "created_at": "t2"}, + ) + with self.assertRaises(LedgerError): + ledger.get(first.candidate_id) + + def test_contamination_flag_and_rejection_are_retained(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) + result = ledger.record_selection( + candidate.candidate_id, + selected=False, + result_ref="selection-result", + rejection_reasons=("UNKNOWN",), + contamination_flag=True, + ) + self.assertTrue(result.contamination_flag) + self.assertEqual(result.rejection_reasons, ("UNKNOWN",)) + class ExperienceTests(unittest.TestCase): def test_unknown_and_rejected_experience_becomes_negative_memory(self) -> None: @@ -84,6 +121,39 @@ def test_unknown_and_rejected_experience_becomes_negative_memory(self) -> None: self.assertEqual(record.experience_identity, experience.record_id) self.assertIn("UNKNOWN", record.statement) + def test_real_development_outcomes_remain_scoped_negative_memory(self) -> None: + accepted = ExperienceRecord.from_development_transaction( + candidate_id="ravel-0.6-candidate-001", + context_identity="transaction-accepted", + task_environment="ravel-toy-branching-c/1", + provider_id="ravel-candidate", + transaction={"committed": True, "rejection_reason": "none"}, + matched_compute={"reference_available": True}, + ) + rejected = ExperienceRecord.from_development_transaction( + candidate_id="ravel-0.6-candidate-001", + context_identity="transaction-rejected", + task_environment="ravel-toy-branching-c/1", + provider_id="ravel-candidate", + transaction={"committed": False, "rejection_reason": "retention_loss_floor"}, + ) + with tempfile.TemporaryDirectory() as directory: + with SQLiteMemoryStore(f"{directory}/memory.sqlite") as store: + store.insert_records_atomic( + ( + accepted.to_memory_record(created_at="2026-08-08T00:00:00Z"), + rejected.to_memory_record(created_at="2026-08-08T00:00:01Z"), + ) + ) + first = store.search_records("retention constrained adaptation") + second = store.search_records("retention constrained adaptation") + self.assertEqual(first, second) + self.assertEqual(len(first), 2) + self.assertTrue(all(record.memory_class is MemoryClass.NEGATIVE for record, _ in first)) + self.assertEqual( + store.search_records("retention", include_negative=False), () + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_policy.py b/tests/test_policy.py new file mode 100644 index 0000000..6109359 --- /dev/null +++ b/tests/test_policy.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import json +from pathlib import Path +import tempfile +import unittest + +from ravel.policy import PolicyError, load_frozen_policy, policy_c_header + + +ROOT = Path(__file__).resolve().parents[1] + + +class FrozenPolicyTests(unittest.TestCase): + def test_policy_is_derived_from_frozen_records(self) -> None: + policy = load_frozen_policy() + self.assertEqual(policy.base_accuracy_floor_q20, 891290) + self.assertEqual(policy.retention_accuracy_floor_q20, 943718) + self.assertEqual(policy.retention_loss_floor_q20, -104858) + self.assertEqual(policy.maximum_update_passes, 2) + self.assertEqual(policy.replay_records, 256) + self.assertEqual(policy.maximum_compute_ratio_q20, 1153434) + self.assertIsNone(policy.maximum_compute_evaluations) + self.assertIn(policy.threshold_identity, policy_c_header(policy)) + + def test_mutating_either_policy_record_is_fail_closed(self) -> None: + source = json.loads( + (ROOT / "ravel_versions/0.6/ravel-0.6-preregistration.json").read_text() + ) + source["mechanism"]["budget"]["maximum_experts"] = 79 + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "mutated.json" + path.write_text(json.dumps(source), encoding="utf-8") + with self.assertRaises(PolicyError): + load_frozen_policy(path) + + def test_policy_identity_is_stable(self) -> None: + first = load_frozen_policy() + second = load_frozen_policy() + self.assertEqual(first.to_dict(), second.to_dict()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ravel_0_6_decomposition.py b/tests/test_ravel_0_6_decomposition.py new file mode 100644 index 0000000..3968071 --- /dev/null +++ b/tests/test_ravel_0_6_decomposition.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import json +import hashlib +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +from tools.ravel_0_6_build import build +from tools.ravel_0_6_decompose import split_candidate_source +from tools.ravel_0_6_seed_candidate import FROZEN_SOURCE, build_candidate_source + + +ROOT = Path(__file__).resolve().parents[1] + + +def compile_and_run( + source: Path, binary: Path, extra_flags: tuple[str, ...] = () +) -> dict[str, object]: + built = subprocess.run( + ["cc", "-std=c11", "-O0", "-Wall", "-Wextra", "-Werror", "-pedantic", *extra_flags, str(source), "-lm", "-o", str(binary)], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if built.returncode != 0: + raise AssertionError(built.stderr) + result = subprocess.run( + [str(binary), "--trial", "decomposition", "--regime", "separated_state", "--seed", "0x1234"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if result.returncode != 0: + raise AssertionError(result.stderr) + return json.loads(result.stdout) + + +class DecompositionTests(unittest.TestCase): + def test_split_is_lossless_and_unity_wrapper_preserves_behavior(self) -> None: + source = build_candidate_source(FROZEN_SOURCE.read_bytes()) + pieces = split_candidate_source(source) + self.assertGreaterEqual(len(pieces), 8) + self.assertEqual("".join(piece for _, piece in pieces), source) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + monolithic = root / "monolithic.c" + direct_binary = root / "direct" + monolithic.write_text(source, encoding="utf-8") + direct = compile_and_run(monolithic, direct_binary) + record = build(root / "split") + split_binary = root / "split" / "ravel_0_6_candidate_001" + split = compile_and_run( + root / "split" / "ravel_0_6_candidate_001.c", split_binary + ) + self.assertEqual(direct, split) + self.assertEqual( + record["generated_source"]["monolithic_sha256"], + hashlib.sha256(source.encode()).hexdigest(), + ) + self.assertEqual(len(record["generated_components"]), len(pieces)) + + def test_real_c_provider_substitution_is_explicit_and_observable(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + branching_dir = root / "branching" + ring_dir = root / "ring" + old_provider = os.environ.get("RAVEL06_PROVIDER") + try: + os.environ["RAVEL06_PROVIDER"] = "branching" + branching_record = build(branching_dir) + os.environ["RAVEL06_PROVIDER"] = "ring" + ring_record = build(ring_dir) + finally: + if old_provider is None: + os.environ.pop("RAVEL06_PROVIDER", None) + else: + os.environ["RAVEL06_PROVIDER"] = old_provider + self.assertNotEqual( + branching_record["environment_provider"]["provider_id"], + ring_record["environment_provider"]["provider_id"], + ) + branching = compile_and_run( + branching_dir / "ravel_0_6_candidate_001.c", + branching_dir / "run", + ) + ring = compile_and_run( + ring_dir / "ravel_0_6_candidate_001.c", ring_dir / "run", + ("-DRAVEL06_PROVIDER_RING",), + ) + self.assertNotEqual(branching["environment_provider_id"], ring["environment_provider_id"]) + self.assertNotEqual(branching["candidate"]["model_identity"], ring["candidate"]["model_identity"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ravel_0_6_negative_matrix.py b/tests/test_ravel_0_6_negative_matrix.py new file mode 100644 index 0000000..30da561 --- /dev/null +++ b/tests/test_ravel_0_6_negative_matrix.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import json +from pathlib import Path +import subprocess +import tempfile +import unittest + +from ravel.c_observations import CTransactionObservation +from ravel.matched_compute import MatchedComputeObservation +from ravel.policy import load_frozen_policy +from tools.ravel_0_6_seed_candidate import FROZEN_SOURCE, build_candidate_source + + +ROOT = Path(__file__).resolve().parents[1] + + +def run_mutated(replacement: tuple[str, str] | None) -> dict[str, object]: + source = build_candidate_source(FROZEN_SOURCE.read_bytes()) + if replacement is not None: + old, new = replacement + if source.count(old) != 1: + raise AssertionError(f"mutation target is not unique: {old}") + source = source.replace(old, new, 1) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source_path = root / "candidate.c" + binary = root / "candidate" + source_path.write_text(source, encoding="utf-8") + built = subprocess.run( + ["cc", "-std=c11", "-O0", "-Wall", "-Wextra", "-Werror", "-pedantic", str(source_path), "-lm", "-o", str(binary)], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if built.returncode != 0: + raise AssertionError(built.stderr) + result = subprocess.run( + [str(binary), "--trial", "negative-matrix", "--regime", "separated_state", "--seed", "0x1234"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if result.returncode != 0: + raise AssertionError(result.stderr) + return json.loads(result.stdout) + + +class NegativeMatrixTests(unittest.TestCase): + def test_python_raw_observation_matrix_has_each_reason(self) -> None: + payload = run_mutated(None) + transaction = payload["candidate"]["adaptation_transaction"] + matched = MatchedComputeObservation.from_json(payload["matched_compute"]) + cases = ( + ("adaptation_improvement_below_epsilon", {"objective_after_q20": transaction["raw"]["objective_before_q20"]}), + ("base_accuracy_floor", {"base_accuracy_after_q20": 0}), + ("retention_accuracy_floor", {"retention_accuracy_after_q20": 0}), + ("retention_loss_floor", {"retention_accuracy_delta_q20": -200000}), + ("expert_capacity_budget", {"expert_count": 81}), + ("birth_budget", {"births": 17}), + ("replay_budget", {"replay_records": 255}), + ("update_pass_budget", {"update_passes": 3}), + ("transition_support_preservation", {"transition_support_losses": 1}), + ) + for reason, changes in cases: + with self.subTest(reason=reason): + mutated = json.loads(json.dumps(transaction)) + mutated["raw"].update(changes) + parsed = CTransactionObservation.from_json(mutated) + report = parsed.evaluate(matched) + self.assertIn(reason, report.rejection_reasons) + ratio_mutation = dict(payload["matched_compute"]) + ratio_mutation.update( + { + "candidate_training_evaluations": 1200000, + "matched_training_evaluations": 1000000, + "ratio_q20": 1200000 * 1048576 // 1000000, + } + ) + over_budget = MatchedComputeObservation.from_json(ratio_mutation) + self.assertEqual(over_budget.evaluate().rejection_reasons, ("matched_compute_ratio",)) + + def test_policy_gate_mutations_have_stable_c_and_python_reasons(self) -> None: + cases = ( + ( + "objective", + "#define RAVEL06_OBJECTIVE_EPSILON_Q20 UINT64_C(105)", + "#define RAVEL06_OBJECTIVE_EPSILON_Q20 UINT64_C(1000000)", + "adaptation_improvement_below_epsilon", + ), + ( + "base", + "#define RAVEL06_BASE_ACCURACY_FLOOR_Q20 UINT64_C(891290)", + "#define RAVEL06_BASE_ACCURACY_FLOOR_Q20 UINT64_MAX", + "base_accuracy_floor", + ), + ( + "retention", + "#define RAVEL06_RETENTION_ACCURACY_FLOOR_Q20 UINT64_C(943718)", + "#define RAVEL06_RETENTION_ACCURACY_FLOOR_Q20 UINT64_MAX", + "retention_accuracy_floor", + ), + ( + "retention loss", + "#define RAVEL06_RETENTION_LOSS_FLOOR_Q20 INT64_C(-104858)", + "#define RAVEL06_RETENTION_LOSS_FLOOR_Q20 INT64_C(200000)", + "retention_loss_floor", + ), + ( + "expert", + "#define RAVEL06_MAX_EXPERTS 80u", + "#define RAVEL06_MAX_EXPERTS 0u", + "expert_capacity_budget", + ), + ( + "birth", + "#define RAVEL06_MAX_BIRTHS 16u", + "#define RAVEL06_MAX_BIRTHS 0u", + "birth_budget", + ), + ( + "replay", + "#define RAVEL06_REPLAY_RECORDS 256u", + "#define RAVEL06_REPLAY_RECORDS 255u", + "replay_budget", + ), + ( + "update", + "#define RAVEL06_MAX_UPDATE_PASSES 2u", + "#define RAVEL06_MAX_UPDATE_PASSES 1u", + "update_pass_budget", + ), + ) + for name, old, new, reason in cases: + with self.subTest(name=name): + payload = run_mutated((old, new)) + transaction = payload["candidate"]["adaptation_transaction"] + self.assertFalse(transaction["committed"]) + self.assertEqual(transaction["rejection_reason"], reason) + parsed = CTransactionObservation.from_json(transaction) + matched = MatchedComputeObservation.from_json(payload["matched_compute"]) + # The source mutation is deliberately outside the frozen policy + # loader. C must reject it, while Python must remain bound to + # the unmutated policy; the build manifest is what detects this + # implementation-policy drift. + if transaction["committed"]: + self.fail(f"C accepted the mutated {name} constraint") + if name == "objective": + self.assertTrue(parsed.evaluate(matched).passed) + else: + self.assertNotIn(reason, parsed.evaluate(matched).rejection_reasons) + self.assertTrue(transaction["rollback_byte_identical"]) + + def test_malformed_threshold_and_matched_compute_are_not_pass(self) -> None: + policy = load_frozen_policy() + payload = run_mutated( + ( + f'#define RAVEL06_THRESHOLD_IDENTITY "{policy.threshold_identity}"', + '#define RAVEL06_THRESHOLD_IDENTITY "mutated-policy"', + ) + ) + transaction = CTransactionObservation.from_json( + payload["candidate"]["adaptation_transaction"] + ) + self.assertEqual(transaction.evaluate().rejection_reasons, ("threshold_identity_mismatch",)) + matched = MatchedComputeObservation.from_json(payload["matched_compute"]) + mutated = dict(payload["matched_compute"]) + mutated["ratio_q20"] = mutated["maximum_ratio_q20"] + 1 + with self.assertRaises(ValueError): + MatchedComputeObservation.from_json(mutated) + self.assertEqual(matched.evaluate().rejection_reasons, ("threshold_identity_mismatch",)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ravel_0_6_provenance.py b/tests/test_ravel_0_6_provenance.py index 1768596..22ce1fc 100644 --- a/tests/test_ravel_0_6_provenance.py +++ b/tests/test_ravel_0_6_provenance.py @@ -41,6 +41,10 @@ def test_build_record_binds_generator_compiler_and_raw_status(self) -> None: 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["generator"]["policy_source"]["sha256"]) + self.assertEqual(record["policy"]["preregistration_sha256"], "26ae0b001355c978dbb2bda57fd7bcd74a3b3d4e46f45fa0b9658d88fcc885a3") + self.assertEqual(record["environment_provider"]["provider_id"], "ravel-toy-branching-c/1") + self.assertEqual(len(record["generated_components"]), 10) self.assertTrue(record["mechanism_components"]) self.assertTrue(record["compiler"]["argv"]) self.assertEqual(record["execution"]["status"], "NOT_RUN") diff --git a/tests/test_ravel_0_6_transaction.py b/tests/test_ravel_0_6_transaction.py index 820abd9..50d679e 100644 --- a/tests/test_ravel_0_6_transaction.py +++ b/tests/test_ravel_0_6_transaction.py @@ -7,6 +7,8 @@ import unittest from ravel.c_observations import CTransactionObservation +from ravel.matched_compute import MatchedComputeObservation +from ravel.policy import load_frozen_policy from tools.ravel_0_6_seed_candidate import FROZEN_SOURCE, build_candidate_source @@ -47,7 +49,7 @@ def test_trial_uses_all_constraint_transaction_and_emits_raw_observations(self) 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["threshold_identity"], load_frozen_policy().threshold_identity) self.assertEqual(transaction["failed_constraint_mask"], 0) self.assertEqual(transaction["rejection_reason"], "none") self.assertEqual(transaction["raw"]["transition_support_losses"], 0) @@ -61,6 +63,9 @@ def test_trial_uses_all_constraint_transaction_and_emits_raw_observations(self) parsed.evaluate().rejection_reasons, ("matched_compute_reference_unavailable",), ) + matched = MatchedComputeObservation.from_json(payload["matched_compute"]) + self.assertTrue(matched.evaluate().passed) + self.assertTrue(parsed.evaluate(matched).passed) try: import jsonschema except ImportError: @@ -69,6 +74,10 @@ def test_trial_uses_all_constraint_transaction_and_emits_raw_observations(self) (ROOT / "ravel_versions/0.6/ravel-0.6-transaction.schema.json").read_text() ) jsonschema.validate(transaction, schema) + matched_schema = json.loads( + (ROOT / "ravel_versions/0.6/ravel-0.6-matched-compute.schema.json").read_text() + ) + jsonschema.validate(payload["matched_compute"], matched_schema) def test_mutated_hard_gate_rejects_and_rolls_back(self) -> None: source = build_candidate_source(FROZEN_SOURCE.read_bytes()) diff --git a/tools/ravel_0_6_build.py b/tools/ravel_0_6_build.py index 4c18421..489ed92 100644 --- a/tools/ravel_0_6_build.py +++ b/tools/ravel_0_6_build.py @@ -26,16 +26,21 @@ FROZEN_SOURCE_SHA256, build_candidate_source, ) + from .ravel_0_6_decompose import write_decomposed_candidate except ImportError: # direct script execution from the tools directory from ravel_0_6_seed_candidate import ( # type: ignore[no-redef] FROZEN_SOURCE, FROZEN_SOURCE_SHA256, build_candidate_source, ) + from ravel_0_6_decompose import write_decomposed_candidate # type: ignore[no-redef] ROOT = Path(__file__).resolve().parents[1] GENERATOR = Path(__file__).resolve() TRANSACTION_SURFACE = ROOT / "tools/ravel_0_6_transaction_surface.py" +POLICY_FILE = ROOT / "src/ravel/policy.py" +FROZEN_PREREGISTRATION = ROOT / "ravel_versions/0.6/ravel-0.6-preregistration.json" +INHERITED_05_PREREGISTRATION = ROOT / "ravel_versions/0.5/ravel-0.5-preregistration.json" COMPONENT_FILES = ( "src/ravel/mechanism_state.py", "src/ravel/world.py", @@ -46,7 +51,15 @@ "src/ravel/experience.py", ) CANDIDATE_ID = "ravel-0.6-candidate-001" -ENVIRONMENT_KEYS = ("CC", "CFLAGS", "CPPFLAGS", "LDFLAGS", "LC_ALL", "LANG") +ENVIRONMENT_KEYS = ( + "CC", + "CFLAGS", + "CPPFLAGS", + "LDFLAGS", + "LC_ALL", + "LANG", + "RAVEL06_PROVIDER", +) CANONICAL_FLAGS = ("-std=c11", "-O3", "-Wall", "-Wextra", "-Werror", "-pedantic") @@ -88,6 +101,15 @@ def compiler_command() -> list[str]: return [executable] +def provider_configuration() -> tuple[str, list[str]]: + provider = os.environ.get("RAVEL06_PROVIDER", "branching") + if provider == "branching": + return "ravel-toy-branching-c/1", [] + if provider == "ring": + return "ravel-toy-ring-c/1", ["-DRAVEL06_PROVIDER_RING"] + raise BuildError("RAVEL06_PROVIDER must be branching or ring") + + def run_capture(argv: list[str]) -> subprocess.CompletedProcess[str]: return subprocess.run(argv, cwd=ROOT, text=True, capture_output=True, check=False) @@ -103,10 +125,15 @@ def build(output_dir: Path, *, require_clean_worktree: bool = False) -> dict[str output_dir = output_dir.resolve() output_dir.mkdir(parents=True, exist_ok=True) source_path = output_dir / "ravel_0_6_candidate_001.c" + monolithic_source_path = output_dir / "ravel_0_6_candidate_001.generated.c" binary_path = output_dir / "ravel_0_6_candidate_001" record_path = output_dir / "ravel-0.6-candidate-001-build.json" - existing = [path for path in (source_path, binary_path, record_path) if path.exists()] + existing = [ + path + for path in (source_path, monolithic_source_path, binary_path, record_path) + if path.exists() + ] if existing: raise BuildError( "stale generated output exists; use a new empty output directory: " @@ -118,10 +145,19 @@ def build(output_dir: Path, *, require_clean_worktree: bool = False) -> dict[str raise BuildError("clean-worktree check failed: " + " | ".join(status)) source = build_candidate_source(FROZEN_SOURCE.read_bytes()).encode("utf-8") - source_path.write_bytes(source) + provider_id, provider_flags = provider_configuration() + monolithic_source_path.write_bytes(source) + source_path, component_paths = write_decomposed_candidate( + source.decode("utf-8"), output_dir + ) compiler = compiler_command() version = run_capture(compiler + ["--version"]) - argv = compiler + list(CANONICAL_FLAGS) + [str(source_path), "-lm", "-o", str(binary_path)] + argv = compiler + list(CANONICAL_FLAGS) + provider_flags + [ + str(source_path), + "-lm", + "-o", + str(binary_path), + ] result = run_capture(argv) record: dict[str, Any] = { "schema": "ravel-0.6-development-build/0.1", @@ -141,17 +177,38 @@ def build(output_dir: Path, *, require_clean_worktree: bool = False) -> dict[str "path": str(TRANSACTION_SURFACE.relative_to(ROOT)), "sha256": sha256_file(TRANSACTION_SURFACE), }, + "policy_source": { + "path": str(POLICY_FILE.relative_to(ROOT)), + "sha256": sha256_file(POLICY_FILE), + }, + }, + "policy": { + "preregistration_path": str(FROZEN_PREREGISTRATION.relative_to(ROOT)), + "preregistration_sha256": sha256_file(FROZEN_PREREGISTRATION), + "inherited_05_preregistration_path": str(INHERITED_05_PREREGISTRATION.relative_to(ROOT)), + "inherited_05_preregistration_sha256": sha256_file(INHERITED_05_PREREGISTRATION), + }, + "environment_provider": { + "provider_id": provider_id, + "compile_flags": provider_flags, }, "mechanism_components": [ {"path": path, "sha256": sha256_file(ROOT / path)} for path in COMPONENT_FILES ], "generated_source": { - "path": str(source_path), - "sha256": sha256_bytes(source), - "bytes": len(source), + "path": source_path.name, + "monolithic_path": monolithic_source_path.name, + "sha256": sha256_file(source_path), + "bytes": source_path.stat().st_size, + "monolithic_sha256": sha256_bytes(source), + "monolithic_bytes": len(source), "development_only": True, }, + "generated_components": [ + {"path": path.name, "sha256": sha256_file(path), "bytes": path.stat().st_size} + for path in component_paths + ], "compiler": { "executable": compiler[0], "version_argv": compiler + ["--version"], diff --git a/tools/ravel_0_6_decompose.py b/tools/ravel_0_6_decompose.py new file mode 100644 index 0000000..988f744 --- /dev/null +++ b/tools/ravel_0_6_decompose.py @@ -0,0 +1,75 @@ +"""Deterministically split generated candidate source into bounded C units. + +The pieces are included in one unity translation unit for this extraction +iteration. This preserves the frozen-source-derived static linkage and exact +behavior while making each maintained mechanism surface independently +addressable and provenance-bound. A future iteration may promote selected +pieces to separately compiled units after a header contract is frozen. +""" + +from __future__ import annotations + +from pathlib import Path + + +COMPONENT_BOUNDARIES = ( + ("ravel_0_6_preamble.inc", "static uint64_t rng_state"), + ("ravel_0_6_core.inc", "static void make_world"), + ("ravel_0_6_world.inc", "static double dist_x"), + ("ravel_0_6_mechanism.inc", "static uint16_t nearest_vector"), + ("ravel_0_6_transition.inc", "static Eval evaluate"), + ("ravel_0_6_planning.inc", "static void bb_bytes"), + ("ravel_0_6_checkpoint.inc", "static int eval_equal"), + ("ravel_0_6_observations.inc", "static void digest_hex"), + ("ravel_0_6_reporting.inc", "static int run_trial"), + ("ravel_0_6_driver.inc", None), +) + + +class DecompositionError(ValueError): + """Raised when a generated source does not match the extraction contract.""" + + +def split_candidate_source(source: str) -> tuple[tuple[str, str], ...]: + starts: list[int] = [] + for _, marker in COMPONENT_BOUNDARIES[:-1]: + if marker is None: + raise DecompositionError("non-final component has no boundary marker") + matches = [position for position in range(len(source)) if source.startswith(marker, position)] + if len(matches) != 1: + raise DecompositionError(f"expected one component marker {marker!r}, found {len(matches)}") + starts.append(matches[0]) + if starts != sorted(starts): + raise DecompositionError("component markers are not ordered") + pieces: list[tuple[str, str]] = [] + first_name = COMPONENT_BOUNDARIES[0][0] + pieces.append((first_name, source[: starts[0]])) + for index, start in enumerate(starts): + end = starts[index + 1] if index + 1 < len(starts) else len(source) + pieces.append((COMPONENT_BOUNDARIES[index + 1][0], source[start:end])) + if "".join(piece for _, piece in pieces) != source: + raise DecompositionError("component extraction is not lossless") + return tuple(pieces) + + +def write_decomposed_candidate(source: str, output_dir: Path) -> tuple[Path, tuple[Path, ...]]: + output_dir.mkdir(parents=True, exist_ok=True) + pieces = split_candidate_source(source) + paths: list[Path] = [] + for name, content in pieces: + path = output_dir / name + if path.exists(): + raise DecompositionError(f"stale generated component exists: {path}") + path.write_text(content, encoding="utf-8", newline="\n") + paths.append(path) + wrapper = output_dir / "ravel_0_6_candidate_001.c" + if wrapper.exists(): + raise DecompositionError(f"stale generated wrapper exists: {wrapper}") + wrapper.write_text( + "/* generated unity wrapper; component bytes are provenance-bound */\n" + + "\n".join(f'#include "{path.name}"' for path in paths) + + "\n", + encoding="utf-8", + newline="\n", + ) + return wrapper, tuple(paths) diff --git a/tools/ravel_0_6_seed_candidate.py b/tools/ravel_0_6_seed_candidate.py index 4bfaf11..27ca42e 100644 --- a/tools/ravel_0_6_seed_candidate.py +++ b/tools/ravel_0_6_seed_candidate.py @@ -104,6 +104,40 @@ for (uint32_t k = 0; k < TRANSITION_TOP_K; ++k) { """ +PROVIDER_SURFACE = """\ +#ifndef RAVEL06_PROVIDER_RING +#define RAVEL06_PROVIDER_ID "ravel-toy-branching-c/1" +#else +#define RAVEL06_PROVIDER_ID "ravel-toy-ring-c/1" +#endif + +void make_ring_world(World *w, const TrialSpec *spec) { + memset(w, 0, sizeof *w); + for (uint32_t s = 0; s < STATES; ++s) { + for (uint32_t d = 0; d < D; ++d) { + int sign = ((s + 3u * d + (s >> 2u)) & 1u) ? 1 : -1; + w->center[s][d] = (int16_t)(sign * (spec->amplitude - (int)(d % 3u))); + } + w->label[s] = (uint8_t)((s * 7u + (s >> 3u)) & 7u); + for (uint32_t a = 0; a < ACTIONS; ++a) { + w->base_next[s][a] = (uint8_t)((s + a + 1u) & 63u); + w->drift_next[s][a] = w->base_next[s][a]; + } + if (spec->transition_drift && s < 24u) { + w->drift_next[s][1] = (uint8_t)((s + 5u) & 63u); + } + } +} + +static void make_world(World *w, const TrialSpec *spec) { +#ifdef RAVEL06_PROVIDER_RING + make_ring_world(w, spec); +#else + make_branching_world(w, spec); +#endif +} +""" + SOURCE_MARKER = " * It emits observations and integrity facts, never development verdicts.\n" CANDIDATE_MARKER = ( SOURCE_MARKER @@ -143,6 +177,56 @@ def build_candidate_source(source_bytes: bytes) -> str: raise SeedError(f"{name}: expected one source match, found {count}") source = source.replace(old, new, 1) + old_world_start = "static void make_world(World *w, const TrialSpec *spec) {" + if source.count(old_world_start) != 1: + raise SeedError("provider surface: expected one synthetic world provider") + source = source.replace( + old_world_start, + "void make_branching_world(World *w, const TrialSpec *spec) {", + 1, + ) + provider_boundary = "\n}\n\nstatic void make_observation" + if source.count(provider_boundary) != 1: + raise SeedError("provider surface: expected world provider boundary") + source = source.replace( + provider_boundary, + "\n}\n\n" + PROVIDER_SURFACE + "\nstatic void make_observation", + 1, + ) + + old_trial_identity = r''' " \"trial_id\":\"%s\",\"regime\":\"%s\","''' + new_trial_identity = r''' " \"environment_provider_id\":\"%s\",\n" + " \"trial_id\":\"%s\",\"regime\":\"%s\","''' + if source.count(old_trial_identity) != 1: + raise SeedError("provider surface: expected trial identity output") + source = source.replace(old_trial_identity, new_trial_identity, 1) + old_trial_args = " spec->trial_id, spec->regime, spec->seed," + new_trial_args = " RAVEL06_PROVIDER_ID, spec->trial_id, spec->regime, spec->seed," + if source.count(old_trial_args) != 1: + raise SeedError("provider surface: expected trial identity arguments") + source = source.replace(old_trial_args, new_trial_args, 1) + + old_observe_signature = ( + " VariantObservation *out, const Model *base, const Event *base_train,\n" + " const Event *adapt_train, const Event *drift_hold, const Event *retention," + ) + new_observe_signature = ( + " VariantObservation *out, const Model *base, const Event *base_train,\n" + " const Event *base_hold, const Event *adapt_train,\n" + " const Event *drift_hold, const Event *retention," + ) + if source.count(old_observe_signature) != 1: + raise SeedError("transaction surface: expected one variant observation signature") + source = source.replace(old_observe_signature, new_observe_signature, 1) + old_observe_call = "&candidate, &base, base_train, adapt_train," + if source.count(old_observe_call) != 1: + raise SeedError("transaction surface: expected candidate observation call") + source = source.replace(old_observe_call, "&candidate, &base, base_train, base_hold, adapt_train,", 1) + old_variant_call = "&observation, &base, base_train, adapt_train," + if source.count(old_variant_call) != 2: + raise SeedError("transaction surface: expected two comparator observation calls") + source = source.replace(old_variant_call, "&observation, &base, base_train, base_hold, adapt_train,", 2) + observation_marker = "typedef struct {\n Model model;\n" if source.count(observation_marker) != 1: raise SeedError("transaction surface: expected one observation boundary") @@ -163,6 +247,7 @@ def build_candidate_source(source_bytes: bytes) -> str: 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, + base_hold, BASE_HOLD_N, retention, RETENTION_N, config, &out->adaptation_metric, &out->replay_metric, &out->topology, &out->transaction);""" @@ -184,6 +269,53 @@ def build_candidate_source(source_bytes: bytes) -> str: raise SeedError("transaction surface: expected one candidate JSON boundary") source = source.replace(old_candidate_output, new_candidate_output, 1) + old_comparison_boundary = ' printf(" \\\"comparisons\\\":{\\n");' + if source.count(old_comparison_boundary) != 1: + raise SeedError("matched compute: expected one comparison boundary") + source = source.replace( + old_comparison_boundary, + old_comparison_boundary + "\n MatchedComputeObservation matched_compute = {0};", + 1, + ) + old_matched_metrics = ( + " observation.planning =\n" + " evaluate_planning(&observation.model, &world, &spec,\n" + " planning_seed, 1);\n" + " BEGIN_VARIANT();" + ) + new_matched_metrics = ( + " observation.planning =\n" + " evaluate_planning(&observation.model, &world, &spec,\n" + " planning_seed, 1);\n" + " matched_compute.candidate_training_evaluations =\n" + " base_metric.expert_evaluations + candidate.adaptation_metric.expert_evaluations;\n" + " matched_compute.matched_training_evaluations =\n" + " base_metric.expert_evaluations + observation.adaptation_metric.expert_evaluations;\n" + " matched_compute.reference_available =\n" + " matched_compute.matched_training_evaluations > 0u;\n" + " matched_compute.ratio_q20 = matched_compute.reference_available\n" + " ? (matched_compute.candidate_training_evaluations * UINT64_C(1048576)) /\n" + " matched_compute.matched_training_evaluations\n" + " : 0u;\n" + " matched_compute.maximum_ratio_q20 = RAVEL06_MAX_COMPUTE_RATIO_Q20;\n" + " matched_compute.threshold_identity = RAVEL06_THRESHOLD_IDENTITY;\n" + " matched_compute.comparator_identity =\n" + " \"fixed-topology-64-expert-routed/matched-development-work-v1\";\n" + " matched_compute.partition_identity = \"ravel-0.6-development-adaptation-v1\";\n" + " BEGIN_VARIANT();" + ) + if source.count(old_matched_metrics) != 1: + raise SeedError("matched compute: expected one comparator observation boundary") + source = source.replace(old_matched_metrics, new_matched_metrics, 1) + old_trial_close = ' printf("\\n }\\n}\\n");' + new_trial_close = ( + ' printf("\\n },\\n \\"matched_compute\\":");\n' + ' print_matched_compute_json(&matched_compute);\n' + ' printf("\\n}\\n");' + ) + if source.count(old_trial_close) != 1: + raise SeedError("matched compute: expected one trial JSON close") + source = source.replace(old_trial_close, new_trial_close, 1) return source diff --git a/tools/ravel_0_6_transaction_surface.py b/tools/ravel_0_6_transaction_surface.py index c6a08bd..3004bc9 100644 --- a/tools/ravel_0_6_transaction_surface.py +++ b/tools/ravel_0_6_transaction_surface.py @@ -6,13 +6,28 @@ all-hard-gates commit boundary. """ -TRANSACTION_SURFACE = r''' +try: + from ravel.policy import policy_c_header +except ImportError: # direct execution from a source checkout before install + import sys + from pathlib import Path + + sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + from ravel.policy import policy_c_header # type: ignore[no-redef] + + +TRANSACTION_SURFACE = ( + policy_c_header() + + 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 retention_accuracy_before_q20; + uint64_t retention_accuracy_after_q20; + int64_t retention_accuracy_delta_q20; uint64_t representation_before_q20; uint64_t representation_after_q20; uint64_t prediction_rmse_before_q20; @@ -33,6 +48,17 @@ const char *rejection_reason; } AdaptationTransaction; +typedef struct { + uint64_t candidate_training_evaluations; + uint64_t matched_training_evaluations; + uint64_t ratio_q20; + uint64_t maximum_ratio_q20; + uint8_t reference_available; + const char *threshold_identity; + const char *comparator_identity; + const char *partition_identity; +} MatchedComputeObservation; + enum { RAVEL06_FAIL_MECHANISM = 1u << 0, RAVEL06_FAIL_OBJECTIVE = 1u << 1, @@ -45,14 +71,11 @@ RAVEL06_FAIL_RETIREMENT_BUDGET = 1u << 8, RAVEL06_FAIL_REPLAY_BUDGET = 1u << 9, RAVEL06_FAIL_UPDATE_BUDGET = 1u << 10, - RAVEL06_FAIL_COMPUTE_BUDGET = 1u << 11 + RAVEL06_FAIL_COMPUTE_BUDGET = 1u << 11, + RAVEL06_FAIL_RETENTION_ACCURACY = 1u << 12, + RAVEL06_FAIL_RETENTION_LOSS = 1u << 13 }; -#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; @@ -109,6 +132,8 @@ 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_RETENTION_ACCURACY) return "retention_accuracy_floor"; + if (mask & RAVEL06_FAIL_RETENTION_LOSS) return "retention_loss_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"; @@ -123,13 +148,14 @@ static int adapt_model_transaction( Model *model, const Event *base_train, const Event *adapt_train, + const Event *base_holdout, uint32_t base_holdout_count, 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"; + transaction->threshold_identity = RAVEL06_THRESHOLD_IDENTITY; int mechanism_ok = adapt_model(&proposed, base_train, adapt_train, config, metric, replay, topology); canonicalize_model(&proposed); @@ -141,17 +167,24 @@ transaction->births = metric->births; transaction->retirements = metric->retired; transaction->replay_records = replay->selected; - transaction->update_passes = config->matched_work ? 4u : 2u; + transaction->update_passes = 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); + Eval previous_base_eval = evaluate(&previous, base_holdout, base_holdout_count, config->routed); + Eval proposed_base_eval = evaluate(&proposed, base_holdout, base_holdout_count, config->routed); + const Event *retention_data = retention != NULL ? retention : base_train; + uint32_t retention_count_for_gate = retention != NULL ? retention_count : BASE_TRAIN_N; + Eval previous_eval = evaluate(&previous, retention_data, retention_count_for_gate, config->routed); + Eval proposed_eval = evaluate(&proposed, retention_data, retention_count_for_gate, config->routed); + transaction->base_accuracy_before_q20 = ravel06_accuracy_q20(&previous_base_eval); + transaction->base_accuracy_after_q20 = ravel06_accuracy_q20(&proposed_base_eval); + transaction->retention_accuracy_before_q20 = ravel06_accuracy_q20(&previous_eval); + transaction->retention_accuracy_after_q20 = ravel06_accuracy_q20(&proposed_eval); + transaction->retention_accuracy_delta_q20 = + (int64_t)transaction->retention_accuracy_after_q20 - + (int64_t)transaction->retention_accuracy_before_q20; transaction->representation_before_q20 = ravel06_representation_q20(&previous_eval); transaction->representation_after_q20 = @@ -163,13 +196,20 @@ uint32_t failed = 0u; if (!mechanism_ok) failed |= RAVEL06_FAIL_MECHANISM; - if (transaction->objective_after_q20 < - transaction->objective_before_q20 + TOPOLOGY_OBJECTIVE_MIN_Q20) { + if (transaction->objective_after_q20 <= transaction->objective_before_q20 || + transaction->objective_after_q20 - transaction->objective_before_q20 < + RAVEL06_OBJECTIVE_EPSILON_Q20) { failed |= RAVEL06_FAIL_OBJECTIVE; } if (transaction->base_accuracy_after_q20 < RAVEL06_BASE_ACCURACY_FLOOR_Q20) { failed |= RAVEL06_FAIL_BASE_ACCURACY; } + if (transaction->retention_accuracy_after_q20 < RAVEL06_RETENTION_ACCURACY_FLOOR_Q20) { + failed |= RAVEL06_FAIL_RETENTION_ACCURACY; + } + if (transaction->retention_accuracy_delta_q20 < RAVEL06_RETENTION_LOSS_FLOOR_Q20) { + failed |= RAVEL06_FAIL_RETENTION_LOSS; + } if (transaction->representation_after_q20 > transaction->representation_before_q20) { failed |= RAVEL06_FAIL_REPRESENTATION; @@ -181,19 +221,21 @@ RAVEL06_PREDICTION_DEGRADATION_BOUND_Q20) { failed |= RAVEL06_FAIL_PREDICTION; } - if (transaction->transition_support_losses != 0u) { + if (transaction->transition_support_losses > + RAVEL06_MAX_TRANSITION_SUPPORT_LOSSES) { 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) { + if (transaction->expert_count > RAVEL06_MAX_EXPERTS) failed |= RAVEL06_FAIL_EXPERT_BUDGET; + if (transaction->births > RAVEL06_MAX_BIRTHS) failed |= RAVEL06_FAIL_BIRTH_BUDGET; + if (transaction->retirements > RAVEL06_MAX_RETIREMENTS) { failed |= RAVEL06_FAIL_RETIREMENT_BUDGET; } - if (transaction->replay_records > REPLAY_N) failed |= RAVEL06_FAIL_REPLAY_BUDGET; + if (transaction->replay_records != RAVEL06_REPLAY_RECORDS) 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) { + if (RAVEL06_MAX_COMPUTE_EVALUATIONS != UINT64_MAX && + transaction->compute_evaluations > RAVEL06_MAX_COMPUTE_EVALUATIONS) { failed |= RAVEL06_FAIL_COMPUTE_BUDGET; } transaction->failed_constraint_mask = failed; @@ -222,6 +264,9 @@ "\"raw\":{\"objective_before_q20\":%u," "\"objective_after_q20\":%u,\"base_accuracy_before_q20\":%" PRIu64 ",\"base_accuracy_after_q20\":%" PRIu64 + ",\"retention_accuracy_before_q20\":%" PRIu64 + ",\"retention_accuracy_after_q20\":%" PRIu64 + ",\"retention_accuracy_delta_q20\":%" PRId64 ",\"representation_before_q20\":%" PRIu64 ",\"representation_after_q20\":%" PRIu64 ",\"prediction_rmse_before_q20\":%" PRIu64 @@ -239,6 +284,9 @@ transaction->objective_before_q20, transaction->objective_after_q20, transaction->base_accuracy_before_q20, transaction->base_accuracy_after_q20, + transaction->retention_accuracy_before_q20, + transaction->retention_accuracy_after_q20, + transaction->retention_accuracy_delta_q20, transaction->representation_before_q20, transaction->representation_after_q20, transaction->prediction_rmse_before_q20, @@ -250,4 +298,24 @@ transaction->matched_compute_evaluations, transaction->matched_compute_reference_available ? "true" : "false"); } + +static void print_matched_compute_json( + const MatchedComputeObservation *observation) { + printf("{\"candidate_training_evaluations\":%" PRIu64 + ",\"matched_training_evaluations\":%" PRIu64 + ",\"ratio_q20\":%" PRIu64 + ",\"maximum_ratio_q20\":%" PRIu64 + ",\"reference_available\":%s" + ",\"threshold_identity\":\"%s\"" + ",\"comparator_identity\":\"%s\"" + ",\"partition_identity\":\"%s\"}", + observation->candidate_training_evaluations, + observation->matched_training_evaluations, + observation->ratio_q20, observation->maximum_ratio_q20, + observation->reference_available ? "true" : "false", + observation->threshold_identity, + observation->comparator_identity, + observation->partition_identity); +} ''' +)