From 99d39a1ce184c814a3ae6b15fe52612f6e708d92 Mon Sep 17 00:00:00 2001 From: epi13 Date: Sun, 9 Aug 2026 09:58:27 -0800 Subject: [PATCH] Integrate RAVEL development workloads with MNCS Fabric --- .github/workflows/tests.yml | 2 +- README.md | 6 +- config/ravel-fabric.example.toml | 26 + docs/ARCHITECTURE.md | 26 + docs/EVIDENCE_GUIDE.md | 17 + docs/FABRIC_INTEGRATION.md | 82 ++ docs/PROJECT_MAP.md | 10 + docs/PROVIDER_RUNTIME.md | 28 +- mncs-forge.toml | 38 +- pyproject.toml | 2 + ravel_versions/0.6/Makefile | 5 +- .../0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md | 15 +- ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md | 15 +- .../ravel-0.6-fabric-observation.schema.json | 33 + .../ravel-0.6-family-compatibility-lock.json | 31 + .../ravel-0.6-forge-development-policy.json | 6 +- ravel_versions/0.6/ravel-0.6-limitations.md | 12 + src/ravel/__init__.py | 1 + src/ravel/experience.py | 72 ++ src/ravel/fabric.py | 795 ++++++++++++++++++ tests/test_ravel_0_6_transaction.py | 24 + tests/test_ravel_fabric.py | 131 +++ tools/ravel_fabric_reference.py | 70 ++ tools/ravel_forge_check.py | 75 ++ 24 files changed, 1509 insertions(+), 13 deletions(-) create mode 100644 config/ravel-fabric.example.toml create mode 100644 docs/FABRIC_INTEGRATION.md create mode 100644 ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json create mode 100644 ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json create mode 100644 src/ravel/fabric.py create mode 100644 tests/test_ravel_fabric.py create mode 100644 tools/ravel_fabric_reference.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea78fa8..d605018 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,4 +40,4 @@ jobs: - 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 provider-test evaluator-test forge-test receipt-test abi-test compiler-matrix sanitizers + - run: make -f ravel_versions/0.6/Makefile build behavioral-test transaction-test component-test decomposition-test negative-test provider-test evaluator-test forge-test receipt-test abi-test fabric-test compiler-matrix sanitizers diff --git a/README.md b/README.md index dd46e47..9b5bb66 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 digest-bound policy/evaluator surfaces, separately compiled checkpoint and world/provider contracts, branching/ring unity parity, a Forge-governed development configuration, official MNCS bundle/receipt adapters, 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`. +> **Project status:** RAVEL is research software. Historical RAVEL 0.4 and 0.5 results remain development `FAIL`; RAVEL 0.6 candidate-001 now has digest-bound policy/evaluator surfaces, separately compiled checkpoint and world/provider contracts, branching/ring unity parity, a Forge-governed development configuration, official MNCS bundle/receipt adapters, a bounded local Fabric development path, 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 @@ -113,6 +113,10 @@ algorithmic superiority. The bounded component surfaces in `src/ravel/world.py`, `src/ravel/transition.py`, `src/ravel/planning.py`, `src/ravel/checkpoint.py`, and `src/ravel/mechanism_state.py` provide deterministic provider substitution and checkpoint fixtures; they do not replace the historical 0.5 source. +`src/ravel/fabric.py` adds the optional public Fabric local-controller path; +`tools/ravel_fabric_reference.py` runs branching/ring parity, replication, +reconciliation, bundle, and replay/negative checks without dispatching selection +or final material. ## Non-goals diff --git a/config/ravel-fabric.example.toml b/config/ravel-fabric.example.toml new file mode 100644 index 0000000..b9ae8b7 --- /dev/null +++ b/config/ravel-fabric.example.toml @@ -0,0 +1,26 @@ +# Operator-only example. This file contains placeholders, not credentials. +# Fabric network execution is optional and requires pre-staged bundle material. + +controller_id = "ravel-development-controller" +state_path = "build/fabric-network/controller.jsonl" +pre_staged_bundle_identity = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + +[[workers]] +worker_id = "ravel-development-worker-a" +host = "worker-a.example.invalid" +port = 4433 +capabilities = ["os:linux", "python", "compiler:gcc"] +ca_file = "operator-secrets/ca.pem" +client_cert = "operator-secrets/controller.pem" +client_key = "operator-secrets/controller-key.pem" +trust_store = "operator-secrets/trust.json" + +[[workers]] +worker_id = "ravel-development-worker-b" +host = "worker-b.example.invalid" +port = 4433 +capabilities = ["os:linux", "python", "compiler:clang"] +ca_file = "operator-secrets/ca.pem" +client_cert = "operator-secrets/controller.pem" +client_key = "operator-secrets/controller-key.pem" +trust_store = "operator-secrets/trust.json" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 44db1f6..80920f1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -178,6 +178,32 @@ These modules are tested scaffolding and are not claims that RAVEL 0.6 has been evaluated, selected, independently evaluated, certified, promoted, or made production-safe. +## Forge and Fabric development path + +RAVEL now has a bounded, optional MNCS Fabric integration for development +evidence. RAVEL creates a versioned semantic `FabricWorkload`; it is not a +Fabric `JobPlan` and it cannot expose selection or final material. Forge remains +the declared workflow/control boundary, and the RAVEL adapter translates the +workload into Fabric's public `LocalController`/`LocalWorker` APIs. Fabric then +owns manifest admission, bounded execution, raw process observations, receipts, +challenge/replay handling, worker identity, and reconciliation. + +The local reference path executes the branching and ring provider parity matrix +on two logical workers in one process. Its reconciliation result is retained as +`PASS` when the immutable records agree, but the report explicitly records +`scope=local-in-process-replication` and `independence=UNKNOWN`. Fabric status is +not an RAVEL evaluator result. RAVEL memory retains references to workload, +worker, record, receipt, bundle, and replay identities and stores the resulting +experience as advisory `UNKNOWN` until a RAVEL evaluator answers its own +question. + +The optional network adapter is TLS-only and requires operator-provided trust +material, worker capabilities, and an exact pre-staged Fabric manifest identity. +It does not add SSH, plaintext fallback, bundle transfer, sandbox claims, or +protected custody. Native Fabric bundle transfer remains unclaimed, so the +reference report distinguishes `bundle verified`, `pre_staged`, and +`executed=UNKNOWN`. + ## Current modular evidence boundary The generated C candidate has two separately compiled development contracts: diff --git a/docs/EVIDENCE_GUIDE.md b/docs/EVIDENCE_GUIDE.md index 5c75428..ccde7e4 100644 --- a/docs/EVIDENCE_GUIDE.md +++ b/docs/EVIDENCE_GUIDE.md @@ -152,6 +152,23 @@ separate evaluator programs cannot by themselves establish: Those claims require external facts and actors, not stronger wording around local files. +## Fabric-backed development evidence + +Fabric records raw execution observations; they are immutable evidence by +reference, not RAVEL verdicts. A RAVEL workload binds the development candidate, +question, provider, bundle, Fabric manifest, capability requirements, resource +budget, Forge workflow, and development partition. The resulting experience +retains workload, worker, request, record, receipt, bundle, challenge, replay, +result, and resource identities without copying the raw Fabric record. + +The local reference report distinguishes `bundle verified`, `pre_staged`, and +`executed=UNKNOWN` because current native Fabric bundle transfer is not claimed. +Two logical workers in one local process are `local-in-process-replication`, not +independent evaluation, protected custody, or R6-06 evidence. Fabric +reconciliation `PASS` answers only the Fabric reconciliation question; the +RAVEL development evaluator remains separate and normally records the imported +observation as `UNKNOWN`. + ## Review checklist Before accepting a RAVEL result or modifying the directory, verify: diff --git a/docs/FABRIC_INTEGRATION.md b/docs/FABRIC_INTEGRATION.md new file mode 100644 index 0000000..5c339e3 --- /dev/null +++ b/docs/FABRIC_INTEGRATION.md @@ -0,0 +1,82 @@ +# RAVEL and MNCS Fabric + +RAVEL uses MNCS Forge and Fabric as sibling layers: + +```text +RAVEL semantic question + -> Forge declared development workflow + -> Fabric bounded JobPlan/execution record + -> MNCS receipt and resource facts + -> RAVEL scoped advisory experience +``` + +## Versioned RAVEL contracts + +`ravel-fabric-workload/0.1` is RAVEL's semantic request. It binds the candidate, +experiment, question kind, logical and Fabric manifest bundle identities, +capabilities, resource budget, replication count, provider, development +partition, and Forge workflow. It is not a Fabric `JobPlan`, evaluator result, +promotion request, or selection/final input. + +`ravel-fabric-observation/0.1` is a reference to immutable Fabric evidence. It +retains workload, request, worker, record, receipt, bundle, challenge/replay, +result, provider, and resource identities plus Fabric outcome/reason codes. It +does not duplicate the raw execution record and is explicitly +`development observation; not evaluator authority`. + +The report schema is +[`ravel-0.6-fabric-observation.schema.json`](../ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json). +The compatibility snapshot is +[`ravel-0.6-family-compatibility-lock.json`](../ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json); +it is evidence about inspected public contracts, not an installation lockfile. + +## Local reference backend + +`FabricLocalBackend` uses Fabric's public `FabricService`, `LocalController`, +`LocalWorker`, manifest, receipt, challenge/replay, and reconciliation APIs. It +builds a bounded development-only artifact and runs the branching and ring +provider parity task on two logical workers. These workers share a process and +host, so the report labels the scope `local-in-process-replication` and keeps +independence `UNKNOWN`. + +The local command is: + +```bash +python3 tools/ravel_fabric_reference.py --workspace build/fabric-reference --json +``` + +The project-local Forge workflow `fabric-reference` invokes the same command. +The `fabric-negative` workflow tests capability mismatch (`UNKNOWN`), wrong +manifest and corrupt record (`FAIL`), idempotent duplicate request, and +conflicting replay. A first valid challenge consumption is `PASS`; consuming +the same challenge again is a Fabric `FAIL` and is retained rather than hidden. + +## Network boundary + +`FabricNetworkBackend` is optional and TLS-only. `FabricNetworkConfig` requires +operator-supplied CA, client certificate/key, trust store, worker endpoint, +capabilities, and an exact pre-staged Fabric manifest identity. The checked-in +[`ravel-fabric.example.toml`](../config/ravel-fabric.example.toml) contains only +placeholders. RAVEL does not use SSH as a dispatch protocol, does not add a +plaintext fallback, and does not claim native bundle transfer until Fabric +exposes and verifies it. + +The report therefore keeps these facts separate: + +```text +bundle verified PASS +bundle pre-staged PASS (local artifact root) +archive executed UNKNOWN +receipt/archive probe FAIL (Fabric receipt currently binds its artifact manifest) +Fabric reconciliation PASS (Fabric question only) +RAVEL evaluator separate; normally UNKNOWN +``` + +The receipt/archive probe is retained as a negative compatibility observation; +it is not rewritten as an official execution binding. Native Fabric bundle +transfer and a receipt adapter that binds the MNCS archive remain a sibling +capability boundary. + +Fabric execution is not a sandbox, independent evaluation, protected custody, +MNCS/MNCDS conformance, or promotion authority. Selection and future-final +material are rejected by the workload contract and are not dispatched. diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 48fcebd..7980a69 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -47,6 +47,8 @@ regenerating the frozen records. recorded outcomes. - [Evidence guide](EVIDENCE_GUIDE.md) explains the evidence layers and claim boundaries. +- [Fabric integration](FABRIC_INTEGRATION.md) documents the bounded RAVEL / + Forge / Fabric development path and its status boundaries. - [Architecture gaps](ARCHITECTURE_GAPS.md) records the early design gaps. - [`../tools/README.md`](../tools/README.md) documents evaluators, digest tools, mutation checks, runtime capture, and 0.6 candidate derivation. @@ -61,6 +63,10 @@ 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/fabric.py` defines the development-only Fabric workload and + reference-observation contracts, the public local-controller backend, and an + optional TLS-only network backend; `tools/ravel_fabric_reference.py` runs the + bounded branching/ring matrix. - `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 @@ -74,6 +80,10 @@ regenerating the frozen records. - `mncs-forge.toml` declares the Forge-governed development workflows; `src/ravel/mncs_bundles.py` delegates immutable execution-bundle operations to MNCS when that optional sibling is installed. +- `config/ravel-fabric.example.toml` is a placeholder-only operator template; + `ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json` and the family + compatibility lock version the public evidence boundary without making sibling + checkouts required for ordinary tests. - `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 ABI boundary. `ravel_0_6_checkpoint.[ch]` and diff --git a/docs/PROVIDER_RUNTIME.md b/docs/PROVIDER_RUNTIME.md index 6973947..e3d4443 100644 --- a/docs/PROVIDER_RUNTIME.md +++ b/docs/PROVIDER_RUNTIME.md @@ -36,11 +36,29 @@ normal test suite fail. The local Forge `0.1.0a2` checkout was inspected and exercised for this iteration. Its current CLI exposes typed project, provider, verifier, candidate, workflow, bundle, and lifecycle operations; RAVEL's project-local configuration -declares 13 development workflows in a fresh local Forge ledger; twelve -bounded workflows passed and live family compatibility remained -`UNKNOWN` for unavailable sibling producer checkouts. The project-scoped Forge readiness policy is separate from the frozen -RAVEL preregistration and does not consume selection data. The precedence is -`FAIL > UNKNOWN > PASS`. +declares 17 development workflows. The new Fabric capability and family-lock +workflow passed; the family-lock workflow currently remains `UNKNOWN` because +the inspected Fabric checkout has uncommitted sibling changes. The Fabric +reference workflow passed through Forge after the local reference matrix was +repaired. The project-scoped Forge readiness +policy is separate from the frozen RAVEL preregistration and does not consume +selection data. The precedence is `FAIL > UNKNOWN > PASS`. `ForgeCliProvider` invokes that JSON interface when explicitly configured and preserves lifecycle rejection as raw `UNKNOWN`. Forge remains optional for the core package and is not reimplemented by RAVEL. + +## Fabric execution substrate + +`ravel.fabric.FabricLocalBackend` is the development reference implementation. +It uses only the public MNCS Fabric service/controller/worker boundary and +delegates bundle construction to the official MNCS execution-bundle tooling. +The reference matrix executes both providers with two logical local workers. +Fabric reconciliation passes for matching immutable records; RAVEL records the +replication scope and does not call it independence. + +`FabricNetworkBackend` is optional and unavailable without explicit operator +TLS configuration. It requires a matching pre-staged Fabric manifest and never +falls back to SSH or unauthenticated transport. Missing capabilities, missing +trust material, conflicting replays, and incomplete bundle execution facts stay +`UNKNOWN` or `FAIL` according to the governing contract; they are not converted +to RAVEL success. diff --git a/mncs-forge.toml b/mncs-forge.toml index 303f7aa..ef0fcdf 100644 --- a/mncs-forge.toml +++ b/mncs-forge.toml @@ -9,7 +9,7 @@ root = "." [paths] candidates = [] generated = ["build"] -contracts = ["ravel_versions/0.4", "ravel_versions/0.5", "ravel_versions/0.6/ravel-0.6-preregistration.json", "ravel_versions/0.6/ravel-0.6-preregistration.schema.json"] +contracts = ["ravel_versions/0.4", "ravel_versions/0.5", "ravel_versions/0.6/ravel-0.6-preregistration.json", "ravel_versions/0.6/ravel-0.6-preregistration.schema.json", "ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json", "ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json"] references = ["docs", "README.md"] evaluators = ["src/ravel/development_evaluator.py"] acceptance_policies = ["ravel_versions/0.6/ravel-0.6-forge-development-policy.json", "ravel_versions/0.6/ravel-0.6-preregistration.json"] @@ -152,3 +152,39 @@ command = ["python3", "tools/ravel_bundle_check.py"] provider_protocol = false subject = "project" disclosure = "compact" + +[[workflows]] +name = "fabric-capabilities" +category = "inspection" +mode = "development" +command = ["python3", "tools/ravel_forge_check.py", "fabric-capabilities"] +provider_protocol = false +subject = "project" +disclosure = "compact" + +[[workflows]] +name = "fabric-reference" +category = "differential_behavior" +mode = "development" +command = ["python3", "tools/ravel_forge_check.py", "fabric-reference"] +provider_protocol = false +subject = "project" +disclosure = "compact" + +[[workflows]] +name = "fabric-negative" +category = "mutation" +mode = "development" +command = ["python3", "tools/ravel_forge_check.py", "fabric-negative"] +provider_protocol = false +subject = "project" +disclosure = "compact" + +[[workflows]] +name = "family-compatibility-lock" +category = "inspection" +mode = "development" +command = ["python3", "tools/ravel_forge_check.py", "family-compatibility-lock"] +provider_protocol = false +subject = "project" +disclosure = "compact" diff --git a/pyproject.toml b/pyproject.toml index c46cb22..14a1969 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,8 @@ where = ["src"] "ravel_versions/0.6/ravel-0.6-preregistration.schema.json", "ravel_versions/0.6/ravel-0.6-transaction.schema.json", "ravel_versions/0.6/ravel-0.6-matched-compute.schema.json", + "ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json", + "ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json", ] [tool.pytest.ini_options] diff --git a/ravel_versions/0.6/Makefile b/ravel_versions/0.6/Makefile index dba4a2d..6e94b55 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 policy-test decomposition-test negative-test provider-test evaluator-test forge-test receipt-test abi-test compiler-matrix sanitizers +.PHONY: build behavioral-test transaction-test component-test policy-test decomposition-test negative-test provider-test evaluator-test forge-test receipt-test abi-test fabric-test compiler-matrix sanitizers build: @set -e; out=$$(mktemp -d); python3 tools/ravel_0_6_build.py build --output-dir "$$out"; \ @@ -40,6 +40,9 @@ receipt-test: abi-test: python3 -m unittest tests/test_ravel_0_6_decomposition.py tests/test_mncs_bundles.py +fabric-test: + python3 -m unittest tests/test_ravel_fabric.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 abd5037..7d34294 100644 --- a/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md +++ b/ravel_versions/0.6/RAVEL_0_6_IMPLEMENTATION_STATUS.md @@ -82,6 +82,15 @@ This is a development status record, not RAVEL 0.6 evaluation evidence. executions remain `UNKNOWN` until governed disposition exists; rejected and unavailable outcomes remain negative and deterministic retrieval includes them. +- **Fabric development substrate:** `ravel.fabric` now defines the + `ravel-fabric-workload/0.1` and `ravel-fabric-observation/0.1` boundaries, + executes a bounded branching/ring provider-parity matrix through Fabric's + public local controller/worker service, retains Fabric record/receipt/bundle + identities, exercises challenge/replay and conflicting-request handling, and + imports observations into advisory negative/`UNKNOWN` memory. Reconciliation + is explicitly local in-process replication; it is not independence or final + evaluation. The TLS-only network adapter is implemented but unavailable + without operator trust material and pre-staged bundles. ## Not yet implemented or externally unavailable @@ -92,7 +101,11 @@ This is a development status record, not RAVEL 0.6 evaluation evidence. - Additional separately compiled C ABI contracts, full cross-project evaluator lifecycle integration, and an absolute compute budget remain incomplete or are not declared by the frozen contract. Forge/RAVEL lifecycle mapping is - reference-only and does not collapse the two state machines. + reference-only and does not collapse the two state machines. Observation / + reporting remains the next safe C extraction candidate after dependency review. +- The project-local Forge configuration now declares Fabric capability, + reference, negative-matrix, and family-compatibility-lock workflows. The + local Fabric path is optional for package import and ordinary CI. - 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 ed515ee..df0db07 100644 --- a/ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md +++ b/ravel_versions/0.6/RAVEL_0_6_NEXT_STEPS.md @@ -64,6 +64,14 @@ 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. +The project now also exposes `ravel-fabric-workload/0.1` and +`ravel-fabric-observation/0.1`. Forge invokes a bounded local Fabric reference +matrix for branching and ring provider parity, two logical-worker +reconciliation, challenge/replay negatives, capability mismatch, and receipt / +bundle binding probes. Fabric records and MNCS identities are retained by +reference; local replication is not independent evaluation, and archive +execution remains `UNKNOWN` while native Fabric bundle transfer is unavailable. + ### Queue disposition after the current iteration - **R6-01:** provenance/build foundation complete; generated component and @@ -77,14 +85,17 @@ exact boundary. - **R6-04:** checkpoint and world/provider boundaries are separately compiled and parity-tested against unity for both branching and ring. Transition, planning, adaptation, and driver surfaces remain unity units. The next safe - ABI candidate is observation/reporting, after dependency review. + ABI candidate is observation/reporting, after dependency review. Fabric is + now a real optional development execution substrate behind Forge, with a + TLS-only network boundary that is unavailable without operator staging. - **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 review and, if independently -falsifiable, promote the observation/reporting boundary. Do not freeze +falsifiable, promote the observation/reporting boundary while aligning its raw +record identity with the Fabric observation contract. Do not freeze candidate-001 or consume selection partitions until further implementation, policy parity, and the full development trial matrix are stable. diff --git a/ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json b/ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json new file mode 100644 index 0000000..ea93b69 --- /dev/null +++ b/ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "ravel-fabric-observation/0.1", + "title": "RAVEL development Fabric reference report", + "type": "object", + "required": ["schema", "status", "authority", "semantics", "providers"], + "properties": { + "schema": {"const": "ravel-fabric-reference-run/0.1"}, + "status": {"enum": ["PASS", "FAIL", "UNKNOWN"]}, + "authority": {"const": "development-only"}, + "semantics": {"type": "string"}, + "selection_material": {"const": "not-dispatched"}, + "final_material": {"const": "not-dispatched"}, + "providers": { + "type": "array", + "items": { + "type": "object", + "required": ["workload", "observations", "reconciliation", "bundle", "fabric_status"], + "properties": { + "workload": {"type": "object"}, + "observations": {"type": "array", "items": {"type": "object"}}, + "reconciliation": {"type": "object"}, + "bundle": {"type": "object"}, + "replay": {"type": "object"}, + "negative_cases": {"type": "object"}, + "fabric_status": {"enum": ["PASS", "FAIL", "UNKNOWN"]} + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json b/ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json new file mode 100644 index 0000000..a04e179 --- /dev/null +++ b/ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json @@ -0,0 +1,31 @@ +{ + "schema": "ravel-family-compatibility-lock/1", + "authority": "compatibility evidence only", + "checked_at": "2026-08-09", + "contracts": { + "mncs-fabric": { + "commit": "b7eb8d8e544f0f53328e0332309f7445937a631e", + "public_surfaces": ["FabricService", "LocalController", "LocalWorker", "NetworkController", "TLSNetworkTransport", "execution records", "receipts", "reconciliation"] + }, + "mncs-forge-mcp": { + "commit": "7710ea606bd592e0be95957c96132e8732fbb955", + "public_surfaces": ["project configuration", "declared development workflows", "lifecycle records"] + }, + "machine-native-complexity-standard": { + "commit": "80f08d312dce963265c7f69ac5b4bae8245bd692", + "public_surfaces": ["execution bundle", "execution receipt"] + }, + "Machine-Native-Experimental-Learning": { + "commit": "57b07b2d25a8ea9dad93ea396ae5cc0dff7f9f5b", + "public_surfaces": ["DistributedWorkload", "LocalFabricBackend", "NetworkFabricBackend", "Fabric execution observation"] + }, + "MNCS-Commons": { + "commit": "b1eb5a1081bbb63ee3a6284e8046035bd72a47bc", + "public_surfaces": ["CompatibilityApplication", "live compatibility report"] + }, + "mncs-language": { + "commit": "f234cc8079faa5895a38b7abce0c96031f7d2565", + "public_surfaces": ["semantic identities", "HIR", "verifier request/result identities"] + } + } +} diff --git a/ravel_versions/0.6/ravel-0.6-forge-development-policy.json b/ravel_versions/0.6/ravel-0.6-forge-development-policy.json index 3202104..ed5f5f6 100644 --- a/ravel_versions/0.6/ravel-0.6-forge-development-policy.json +++ b/ravel_versions/0.6/ravel-0.6-forge-development-policy.json @@ -16,7 +16,11 @@ "package", "live-family-compat", "lifecycle", - "bundle" + "bundle", + "fabric-capabilities", + "fabric-reference", + "fabric-negative", + "family-compatibility-lock" ], "selection_data": "not-used", "promotion_authority": "external-and-unknown" diff --git a/ravel_versions/0.6/ravel-0.6-limitations.md b/ravel_versions/0.6/ravel-0.6-limitations.md index 77f980a..750b14c 100644 --- a/ravel_versions/0.6/ravel-0.6-limitations.md +++ b/ravel_versions/0.6/ravel-0.6-limitations.md @@ -32,3 +32,15 @@ `UNKNOWN`. A structurally valid receipt is not assurance or conformance. A verifier disposition does not establish runner process facts, and the local Forge runner is not a sandbox or automatic execution-bundle consumer. +- Fabric local reference execution is bounded process execution, not a hostile + code sandbox. Logical worker replication shares one controller process and + host, so independence and protected custody remain `UNKNOWN`. Network Fabric + execution is optional, TLS-only, operator-configured, and currently requires + pre-staged matching manifest material because native bundle transfer is not + claimed. Fabric `PASS` is not RAVEL `PASS`, and no selection/final material is + dispatched by the RAVEL workload contract. +- The local adapter records a receipt/archive binding probe as `FAIL` because + the current Fabric receipt binds its artifact manifest while the official + MNCS archive has a distinct bundle manifest identity. The actual + archive-executed binding remains `UNKNOWN`; this is retained as an integration + boundary, not hidden or promoted to `PASS`. diff --git a/src/ravel/__init__.py b/src/ravel/__init__.py index cf06b59..1b900e1 100644 --- a/src/ravel/__init__.py +++ b/src/ravel/__init__.py @@ -6,6 +6,7 @@ "c_observations", "development_evaluator", "experience", + "fabric", "lifecycle", "matched_compute", "mncs_receipts", diff --git a/src/ravel/experience.py b/src/ravel/experience.py index 7825faa..7ac66da 100644 --- a/src/ravel/experience.py +++ b/src/ravel/experience.py @@ -83,6 +83,78 @@ def from_development_transaction( execution_identity=execution_identity, ) + @classmethod + def from_fabric_observation( + cls, + observation: Mapping[str, Any], + *, + partition_identity: str = "ravel-0.6-development-adaptation-v1", + ) -> "ExperienceRecord": + """Retain a Fabric observation by reference, never as evaluator authority. + + Fabric owns the immutable execution record and receipt. RAVEL stores only + their identities and a scoped diagnostic interpretation; a Fabric PASS is + deliberately represented as ``UNKNOWN`` here until a RAVEL evaluator + answers the question it owns. + """ + + required = ("candidate_identity", "workload_identity", "fabric_outcome") + if any(not isinstance(observation.get(key), str) for key in required): + raise ValueError("Fabric observation identity or outcome is malformed") + candidate_id = str(observation["candidate_identity"]) + workload_identity = str(observation["workload_identity"]) + provider_id = str(observation.get("provider_identity") or "unknown-provider") + references = { + key: value + for key in ( + "workload_identity", + "candidate_binding_identity", + "request_identity", + "worker_identity", + "fabric_record_identity", + "receipt_identity", + "bundle_identity", + "bundle_archive_identity", + "fabric_manifest_identity", + "challenge_identity", + "replay_identity", + ) + if isinstance(value := observation.get(key), str) + } + raw_result = { + "fabric_reference": references, + "fabric_outcome": observation["fabric_outcome"], + "reason_codes": list(observation.get("reason_codes", ())), + "semantics": "development observation; not evaluator authority", + } + return cls( + candidate_id=candidate_id, + context_identity=workload_identity, + task_environment="mncs-fabric", + requested_strategy="fabric-development-execution", + provider_id=provider_id, + verifier_id="fabric-execution-observation", + raw_result=raw_result, + formal_disposition="UNKNOWN", + resource_observations=dict(observation.get("resource_observations", {})), + provenance={ + "fabric_record_identity": references.get("fabric_record_identity", ""), + "receipt_identity": references.get("receipt_identity", ""), + "bundle_identity": references.get("bundle_identity", ""), + }, + applicability_scope={ + "partition": partition_identity, + "visibility": "development-visible", + "authority": "development-only", + "worker": references.get("worker_identity", "unknown"), + }, + execution_identity=( + references.get("receipt_identity") + or references.get("fabric_record_identity") + or workload_identity + ), + ) + def to_memory_record(self, *, created_at: str) -> MemoryRecord: scope = dict(self.applicability_scope) scope.update({"candidate": self.candidate_id, "context": self.context_identity}) diff --git a/src/ravel/fabric.py b/src/ravel/fabric.py new file mode 100644 index 0000000..f167704 --- /dev/null +++ b/src/ravel/fabric.py @@ -0,0 +1,795 @@ +"""Optional RAVEL integration with the public MNCS Fabric boundary. + +RAVEL owns the semantic question and its development-only workload identity. +Fabric owns artifact admission, bounded execution, worker placement, raw +execution records, receipts, challenge/replay evidence, and reconciliation. +This module retains Fabric records by identity and never turns Fabric status +into an RAVEL evaluator or promotion decision. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum +import hashlib +import json +import math +import os +from pathlib import Path +import shutil +import stat +import tempfile +from typing import Any, Mapping, Protocol + +from .mncs_bundles import BundleResult, build_execution_bundle + + +WORKLOAD_SCHEMA = "ravel-fabric-workload/0.1" +OBSERVATION_SCHEMA = "ravel-fabric-observation/0.1" +REFERENCE_SCHEMA = "ravel-fabric-reference-report/0.1" +DEVELOPMENT_PARTITION = "ravel-0.6-development-adaptation-v1" +DEVELOPMENT_VISIBILITY = "development-visible" +DEVELOPMENT_AUTHORITY = "development-only" +MAX_OUTPUT_BYTES = 256 * 1024 +MAX_REPLICAS = 8 + + +class FabricError(RuntimeError): + """A bounded RAVEL/Fabric integration error.""" + + +class FabricUnavailableError(FabricError): + """The optional public Fabric package or requested capability is absent.""" + + +class FabricQuestion(StrEnum): + BEHAVIORAL_FIXTURE = "behavioral-fixture" + PROVIDER_PARITY = "provider-parity" + CHECKPOINT_INTEGRITY = "checkpoint-integrity" + NEGATIVE_MUTATION = "negative-mutation" + COMPILE_PORTABILITY = "compile-portability" + COMPONENT_PARITY = "component-parity" + MATCHED_COMPUTE = "matched-compute-observation" + REPLICATED_EXECUTION = "replicated-execution" + + +def _canonical(value: object) -> bytes: + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def _identity(value: object) -> str: + return "sha256:" + hashlib.sha256(_canonical(value)).hexdigest() + + +def _is_identity(value: object) -> bool: + return ( + isinstance(value, str) + and value.startswith("sha256:") + and len(value) == 71 + and all(char in "0123456789abcdef" for char in value[7:]) + ) + + +def _is_external_identity(value: object) -> bool: + return _is_identity(value) or ( + isinstance(value, str) + and len(value) == 64 + and all(char in "0123456789abcdef" for char in value) + ) + + +def _bounded_text(value: object, label: str, maximum: int = 256) -> str: + if not isinstance(value, str) or not value or len(value) > maximum or "\x00" in value: + raise FabricError(f"{label} must be bounded text") + return value + + +def _aggregate(statuses: list[str]) -> str: + if "FAIL" in statuses: + return "FAIL" + if "UNKNOWN" in statuses: + return "UNKNOWN" + return "PASS" + + +@dataclass(frozen=True, slots=True) +class FabricWorkload: + """RAVEL's semantic request, distinct from Fabric's JobPlan.""" + + candidate_identity: str + experiment_identity: str + question_kind: FabricQuestion | str + bundle_identity: str + fabric_manifest_identity: str + required_capabilities: tuple[str, ...] = ("python",) + resource_budget: Mapping[str, int | float] = field( + default_factory=lambda: {"wall_seconds": 60, "output_bytes": MAX_OUTPUT_BYTES} + ) + replication_count: int = 1 + provider_identity: str | None = None + expected_output_kind: str = "diagnostic-observation" + partition_identity: str = DEVELOPMENT_PARTITION + forge_workflow_identity: str = "ravel-forge-fabric-reference/1" + visibility: str = DEVELOPMENT_VISIBILITY + authority: str = DEVELOPMENT_AUTHORITY + + def __post_init__(self) -> None: + _bounded_text(self.candidate_identity, "candidate_identity") + if not _is_identity(self.experiment_identity): + raise FabricError("experiment_identity must be a sha256 identity") + _bounded_text(str(self.question_kind), "question_kind", 96) + if not _is_external_identity(self.bundle_identity): + raise FabricError("bundle_identity must be a supported external identity") + if not _is_identity(self.fabric_manifest_identity): + raise FabricError("fabric_manifest_identity must be a sha256 identity") + if not self.required_capabilities or len(set(self.required_capabilities)) != len( + self.required_capabilities + ): + raise FabricError("required capabilities must be non-empty and unique") + for capability in self.required_capabilities: + _bounded_text(capability, "required capability", 128) + if self.replication_count < 1 or self.replication_count > MAX_REPLICAS: + raise FabricError("replication_count is outside the bounded range") + if self.visibility != DEVELOPMENT_VISIBILITY: + raise FabricError("Fabric workloads may expose development-visible material only") + if self.partition_identity != DEVELOPMENT_PARTITION: + raise FabricError("Fabric workloads must use the frozen development partition") + if self.authority != DEVELOPMENT_AUTHORITY: + raise FabricError("Fabric workloads are development-only") + _bounded_text(self.expected_output_kind, "expected_output_kind") + _bounded_text(self.forge_workflow_identity, "forge_workflow_identity") + for key, value in self.resource_budget.items(): + _bounded_text(key, "resource budget key", 96) + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise FabricError("resource budget values must be finite numbers") + if not math.isfinite(float(value)) or value < 0: + raise FabricError("resource budget values must be finite and non-negative") + if self.provider_identity is not None: + _bounded_text(self.provider_identity, "provider_identity") + + @property + def candidate_binding_identity(self) -> str: + return _identity({"ravel_candidate_identity": self.candidate_identity}) + + def to_dict(self, *, include_identity: bool = True) -> dict[str, Any]: + value: dict[str, Any] = { + "schema": WORKLOAD_SCHEMA, + "candidate_identity": self.candidate_identity, + "candidate_binding_identity": self.candidate_binding_identity, + "experiment_identity": self.experiment_identity, + "question_kind": str(self.question_kind), + "bundle_identity": self.bundle_identity, + "fabric_manifest_identity": self.fabric_manifest_identity, + "required_capabilities": list(self.required_capabilities), + "resource_budget": dict(self.resource_budget), + "replication_count": self.replication_count, + "provider_identity": self.provider_identity, + "expected_output_kind": self.expected_output_kind, + "partition_identity": self.partition_identity, + "forge_workflow_identity": self.forge_workflow_identity, + "visibility": self.visibility, + "authority": self.authority, + "semantics": "RAVEL semantic development request; not a JobPlan, verdict, or promotion input", + } + if include_identity: + value["workload_identity"] = _identity(value) + return value + + @property + def workload_identity(self) -> str: + return self.to_dict(include_identity=False).get("workload_identity") or _identity( + self.to_dict(include_identity=False) + ) + + +@dataclass(frozen=True, slots=True) +class FabricExecutionObservation: + """A RAVEL reference to immutable Fabric evidence.""" + + workload_identity: str + candidate_identity: str + candidate_binding_identity: str | None + worker_identity: str | None + request_identity: str | None + fabric_record_identity: str | None + fabric_manifest_identity: str | None + bundle_identity: str | None + bundle_archive_identity: str | None + receipt_identity: str | None + challenge_identity: str | None + replay_identity: str | None + provider_identity: str | None + result_identities: tuple[str, ...] + fabric_outcome: str + reason_codes: tuple[str, ...] = () + resource_observations: Mapping[str, Any] = field(default_factory=dict) + semantics: str = "development observation; not evaluator authority" + + def __post_init__(self) -> None: + if not _is_identity(self.workload_identity): + raise FabricError("workload_identity is invalid") + _bounded_text(self.candidate_identity, "candidate_identity") + if self.candidate_binding_identity is not None and not _is_identity( + self.candidate_binding_identity + ): + raise FabricError("candidate_binding_identity is invalid") + if self.fabric_outcome not in {"PASS", "FAIL", "UNKNOWN"}: + raise FabricError("fabric_outcome must be PASS, FAIL, or UNKNOWN") + if self.semantics != "development observation; not evaluator authority": + raise FabricError("Fabric observations cannot claim evaluator authority") + for label, value in ( + ("fabric_record_identity", self.fabric_record_identity), + ("fabric_manifest_identity", self.fabric_manifest_identity), + ("receipt_identity", self.receipt_identity), + ("challenge_identity", self.challenge_identity), + ("replay_identity", self.replay_identity), + ): + if value is not None and not _is_external_identity(value): + raise FabricError(f"{label} is invalid") + + def to_dict(self) -> dict[str, Any]: + return { + "schema": OBSERVATION_SCHEMA, + "workload_identity": self.workload_identity, + "candidate_identity": self.candidate_identity, + "candidate_binding_identity": self.candidate_binding_identity, + "worker_identity": self.worker_identity, + "request_identity": self.request_identity, + "fabric_record_identity": self.fabric_record_identity, + "fabric_manifest_identity": self.fabric_manifest_identity, + "bundle_identity": self.bundle_identity, + "bundle_archive_identity": self.bundle_archive_identity, + "receipt_identity": self.receipt_identity, + "challenge_identity": self.challenge_identity, + "replay_identity": self.replay_identity, + "provider_identity": self.provider_identity, + "result_identities": list(self.result_identities), + "fabric_outcome": self.fabric_outcome, + "reason_codes": list(self.reason_codes), + "resource_observations": dict(self.resource_observations), + "semantics": self.semantics, + } + + +@dataclass(frozen=True, slots=True) +class FabricReferenceResult: + workload: FabricWorkload + observations: tuple[FabricExecutionObservation, ...] + reconciliation: Mapping[str, Any] + bundle: Mapping[str, Any] + replay: Mapping[str, Any] + negative_cases: Mapping[str, Any] + fabric_status: str + limitations: tuple[str, ...] + + def to_dict(self) -> dict[str, Any]: + return { + "schema": REFERENCE_SCHEMA, + "workload": self.workload.to_dict(), + "observations": [item.to_dict() for item in self.observations], + "reconciliation": dict(self.reconciliation), + "bundle": dict(self.bundle), + "replay": dict(self.replay), + "negative_cases": dict(self.negative_cases), + "fabric_status": self.fabric_status, + "limitations": list(self.limitations), + "authority": DEVELOPMENT_AUTHORITY, + "semantics": "Fabric development reference report; not evaluator authority", + } + + +class ExecutionBackend(Protocol): + def capabilities(self, worker_label: str) -> Mapping[str, Any]: ... + + def execute_provider_parity( + self, + provider: str, + *, + candidate_identity: str = "ravel-0.6-candidate-001", + replication_count: int = 2, + ) -> FabricReferenceResult: ... + + def reconcile(self, records: list[Mapping[str, Any]]) -> Mapping[str, Any]: ... + + +def _write_bundle_source_manifest(source_root: Path, destination: Path) -> None: + entries = [] + for path in sorted(source_root.rglob("*")): + if not path.is_file() or path.is_symlink(): + continue + relative = path.relative_to(source_root).as_posix() + entries.append({"path": relative, "source": relative, "role": "test", "mode": "0644"}) + value = { + "schema_version": "0.1-experimental", + "record_type": "mncs-execution-bundle-source", + "bundle_id": "ravel-fabric-reference", + "entries": entries, + "entrypoints": [{"name": "ravel-fabric-task", "path": "fabric_task.py"}], + "runtime_requirements": [], + "policy_references": [], + "limits": { + "max_file_count": max(8, len(entries) + 4), + "max_file_bytes": 8 * 1024 * 1024, + "max_total_bytes": 64 * 1024 * 1024, + "max_path_bytes": 512, + "max_expansion_ratio": 100, + }, + "extensions": {"ravel:purpose": "development-only Fabric reference"}, + } + destination.write_text(json.dumps(value, sort_keys=True), encoding="utf-8") + + +def _task_source(provider: str) -> str: + return f'''import json +from pathlib import Path +import subprocess + +provider = {provider!r} +outputs = {{}} +for label, executable in (("separate", "candidate-separate"), ("unity", "candidate-unity")): + path = Path(executable) + path.chmod(path.stat().st_mode | 0o111) + result = subprocess.run( + [str(path.resolve()), "--trial", "decomposition", "--regime", "separated_state", "--seed", "0x1234"], + capture_output=True, + check=False, + ) + raw = result.stdout.decode("utf-8", errors="replace") + outputs[label] = {{"returncode": result.returncode, "stdout": raw}} + if result.returncode != 0: + raise SystemExit(result.returncode or 1) +parsed = {{label: json.loads(value["stdout"]) for label, value in outputs.items()}} +parity = outputs["separate"]["stdout"] == outputs["unity"]["stdout"] +result = {{ + "schema": "ravel-fabric-c-trial/0.1", + "provider_identity": provider, + "candidate_identity": parsed["separate"].get("candidate_id"), + "environment_provider_id": parsed["separate"].get("environment_provider_id"), + "parity": parity, + "separate": parsed["separate"], + "unity": parsed["unity"], +}} +Path("fabric-result.json").write_text(json.dumps(result, sort_keys=True), encoding="utf-8") +raise SystemExit(0 if parity else 1) +''' + + +class FabricLocalBackend: + """Execute a bounded RAVEL C parity workload via LocalController/Worker.""" + + backend_identity = "ravel-fabric-local-public-controller/0.1" + + def __init__(self, workspace: str | Path) -> None: + self.workspace = Path(workspace) + self.workspace.mkdir(parents=True, exist_ok=True) + try: + from mncs_fabric.artifacts import build_manifest + from mncs_fabric.challenges import ChallengeReplayStore, challenge_for_receipt + from mncs_fabric.controller import LocalController + from mncs_fabric.receipts import build_execution_receipt + from mncs_fabric.service import FabricService + from mncs_fabric.worker import LocalWorker + except ImportError as error: + self.available = False + self.unavailable_reason = f"mncs-fabric unavailable: {type(error).__name__}" + return + self.available = True + self.unavailable_reason = None + self._build_manifest = build_manifest + self._ChallengeReplayStore = ChallengeReplayStore + self._challenge_for_receipt = challenge_for_receipt + self._LocalController = LocalController + self._LocalWorker = LocalWorker + self._build_receipt = build_execution_receipt + self._service = FabricService() + + def _require(self) -> None: + if not self.available: + raise FabricUnavailableError(self.unavailable_reason or "mncs-fabric is unavailable") + + def capabilities(self, worker_label: str) -> Mapping[str, Any]: + self._require() + return self._service.capabilities(worker_label) + + def reconcile(self, records: list[Mapping[str, Any]]) -> Mapping[str, Any]: + self._require() + return self._service.reconcile(list(records), require_distinct_nodes=True) + + def _build_provider(self, provider: str, output: Path) -> dict[str, Any]: + from tools.ravel_0_6_build import build + + prior = os.environ.get("RAVEL06_PROVIDER") + try: + os.environ["RAVEL06_PROVIDER"] = provider + return build(output) + finally: + if prior is None: + os.environ.pop("RAVEL06_PROVIDER", None) + else: + os.environ["RAVEL06_PROVIDER"] = prior + + def _make_artifact(self, provider: str, root: Path) -> tuple[Path, dict[str, Any], BundleResult]: + artifact = root / "artifact" + artifact.mkdir(parents=True, exist_ok=True) + build_record = self._build_provider(provider, root / "build") + build_root = root / "build" + for source, target in ( + (build_root / "ravel_0_6_candidate_001", artifact / "candidate-separate"), + (build_root / "ravel_0_6_candidate_001.unity", artifact / "candidate-unity"), + ): + shutil.copyfile(source, target) + target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + (artifact / "build-record.json").write_text( + json.dumps(build_record, sort_keys=True), encoding="utf-8" + ) + (artifact / "fabric_task.py").write_text(_task_source(provider), encoding="utf-8") + manifest = self._build_manifest(artifact) + source_manifest = root / "mncs-source-manifest.json" + _write_bundle_source_manifest(artifact, source_manifest) + bundle = build_execution_bundle(source_manifest, artifact, root / "ravel-execution-bundle.zip") + return artifact, manifest, bundle + + def _plan(self, workload: FabricWorkload) -> dict[str, Any]: + return { + "schema_version": "mncs-fabric.job-plan.v0.1", + "job_id": "ravel-fabric-" + workload.workload_identity[7:31], + "candidate_identity": workload.candidate_binding_identity, + "artifact_manifest_identity": workload.fabric_manifest_identity, + "argv": ["@python", "fabric_task.py"], + "working_directory": ".", + "timeout_seconds": float(workload.resource_budget.get("wall_seconds", 60)), + "output_limit_bytes": int(workload.resource_budget.get("output_bytes", MAX_OUTPUT_BYTES)), + "environment": {"PYTHONHASHSEED": "0"}, + "required_capabilities": list(workload.required_capabilities), + "result_paths": ["fabric-result.json"], + "network_policy": "DECLARED_OFFLINE", + } + + def _receipt_with_challenge(self, record: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None]: + base = self._build_receipt( + record, + subject_family="RAVEL", + subject_kind="development-fabric-workload", + runner_identity="ravel-fabric-local-worker:0.1", + ) + challenge_report = self._challenge_for_receipt( + base, issuer_identity="ravel-development-challenge" + ) + if not challenge_report.valid or challenge_report.challenge is None: + return base, None + return ( + self._build_receipt( + record, + subject_family="RAVEL", + subject_kind="development-fabric-workload", + runner_identity="ravel-fabric-local-worker:0.1", + challenge=challenge_report.challenge, + ), + challenge_report.challenge, + ) + + def execute_provider_parity( + self, + provider: str, + *, + candidate_identity: str = "ravel-0.6-candidate-001", + replication_count: int = 2, + ) -> FabricReferenceResult: + self._require() + if provider not in {"branching", "ring"}: + raise FabricError("provider must be branching or ring") + if replication_count < 1 or replication_count > MAX_REPLICAS: + raise FabricError("replication_count is outside the bounded range") + provider_root = self.workspace / provider + provider_root.mkdir(parents=True, exist_ok=True) + artifact, manifest, bundle = self._make_artifact(provider, provider_root) + bundle_identity = bundle.logical_identity or manifest["manifest_identity"] + experiment_identity = _identity( + {"candidate_identity": candidate_identity, "provider": provider, "question": FabricQuestion.PROVIDER_PARITY} + ) + workload = FabricWorkload( + candidate_identity=candidate_identity, + experiment_identity=experiment_identity, + question_kind=FabricQuestion.PROVIDER_PARITY, + bundle_identity=bundle_identity, + fabric_manifest_identity=manifest["manifest_identity"], + required_capabilities=("python",), + replication_count=replication_count, + provider_identity=f"ravel-toy-{provider}-c/1", + ) + plan = self._plan(workload) + controller = self._LocalController( + f"ravel-fabric-controller-{provider}", provider_root / "controller.jsonl" + ) + workers = [] + for suffix in ("a", "b")[:replication_count]: + worker = self._LocalWorker( + f"ravel-{provider}-worker-{suffix}", + artifact, + provider_root / f"worker-{suffix}.jsonl", + ) + controller.register(worker) + workers.append(worker) + responses = controller.dispatch(plan, manifest, replicas=replication_count) + raw_records = [ + response.get("payload", {}).get("record") + for response in responses + if response.get("message_type") == "execution.result" + and isinstance(response.get("payload", {}).get("record"), dict) + ] + receipts: list[dict[str, Any]] = [] + challenges: list[dict[str, Any]] = [] + receipt_bindings: list[dict[str, Any]] = [] + observations: list[FabricExecutionObservation] = [] + for response, record in zip(responses, raw_records): + self._service.verify_record(record) + receipt, challenge = self._receipt_with_challenge(record) + receipts.append(receipt) + try: + binding = self._service.bind_receipt_to_execution_bundle( + receipt, provider_root / "ravel-execution-bundle.zip" + ) + receipt_bindings.append( + { + "status": "PASS" if binding.get("valid") else "FAIL", + "valid": bool(binding.get("valid")), + "issues": list(binding.get("issues", [])), + } + ) + except Exception as error: + receipt_bindings.append( + {"status": "UNKNOWN", "issues": [type(error).__name__]} + ) + if challenge is not None: + challenges.append(challenge) + result_values = record.get("results", []) + result_ids = tuple( + value["sha256"] + for value in result_values + if isinstance(value, dict) and isinstance(value.get("sha256"), str) + ) + node = record.get("node") if isinstance(record.get("node"), dict) else {} + observations.append( + FabricExecutionObservation( + workload_identity=workload.workload_identity, + candidate_identity=candidate_identity, + candidate_binding_identity=workload.candidate_binding_identity, + worker_identity=response.get("worker_id") or node.get("machine_label"), + request_identity=response.get("request_id"), + fabric_record_identity=record.get("record_id"), + fabric_manifest_identity=record.get("artifact_manifest_identity"), + bundle_identity=bundle.logical_identity, + bundle_archive_identity=bundle.archive_identity, + receipt_identity=receipt.get("receipt_identity"), + challenge_identity=challenge.get("challenge_identity") if challenge else None, + replay_identity=None, + provider_identity=provider, + result_identities=result_ids, + fabric_outcome=record.get("outcome", "UNKNOWN"), + reason_codes=(record.get("termination_reason", "UNKNOWN"),), + resource_observations={ + "duration_ms": record.get("duration_ms"), + "node_fingerprint": node.get("node_fingerprint"), + "capabilities": sorted(self._service.capabilities(str(node.get("machine_label"))).get("capabilities", [])), + }, + ) + ) + reconciliation = self._service.reconcile(raw_records, require_distinct_nodes=True) if raw_records else {"outcome": "UNKNOWN", "reasons": ["worker_result_unavailable"]} + replay_store = self._ChallengeReplayStore(provider_root / "challenge-replay.jsonl") + replay_results: list[dict[str, Any]] = [] + for challenge, receipt in zip(challenges, receipts): + first = replay_store.consume(challenge, receipt) + duplicate = replay_store.consume(challenge, receipt) + replay_results.append( + { + "challenge_identity": challenge.get("challenge_identity"), + "first": first.category, + "replay_identity": first.replay_receipt.get("replay_identity") if first.replay_receipt else None, + "duplicate": duplicate.category, + "duplicate_reasons": list(duplicate.issues), + } + ) + duplicate_response = None + conflict_response = None + if workers and raw_records: + worker = workers[0] + first_response = responses[0] + request_id = first_response.get("request_id") + if isinstance(request_id, str): + from mncs_fabric.transport import InProcessTransport + + duplicate_response = controller.dispatch_via( + InProcessTransport(worker), plan, manifest, worker_id=worker.worker_id, request_id=request_id + ) + changed = dict(plan) + changed["candidate_identity"] = _identity({"ravel_candidate_identity": "ravel-0.6-candidate-002"}) + conflict_response = controller.dispatch_via( + InProcessTransport(worker), changed, manifest, worker_id=worker.worker_id, request_id=request_id + ) + capability_plan = dict(plan) + capability_plan["job_id"] = capability_plan["job_id"] + "-capability" + capability_plan["required_capabilities"] = ["capability:ravel-does-not-provide"] + capability_record = self._service.execute_local( + capability_plan, artifact, manifest, f"ravel-{provider}-capability", work_root=provider_root + ) + wrong_manifest = dict(plan) + wrong_manifest["job_id"] = wrong_manifest["job_id"] + "-manifest" + wrong_manifest["artifact_manifest_identity"] = "sha256:" + "f" * 64 + wrong_manifest_record = self._service.execute_local( + wrong_manifest, artifact, manifest, f"ravel-{provider}-wrong-manifest", work_root=provider_root + ) + malformed_record = dict(raw_records[0]) if raw_records else {} + if malformed_record: + malformed_record["record_id"] = "sha256:" + "0" * 64 + negative_cases = { + "capability_mismatch": { + "outcome": capability_record.get("outcome"), + "reason": capability_record.get("termination_reason"), + }, + "wrong_manifest": { + "outcome": wrong_manifest_record.get("outcome"), + "reason": wrong_manifest_record.get("termination_reason"), + }, + "duplicate_request": duplicate_response.get("payload", {}).get("disposition") if isinstance(duplicate_response, dict) else "UNKNOWN", + "conflicting_replay": conflict_response.get("payload", {}).get("disposition") if isinstance(conflict_response, dict) else "UNKNOWN", + "corrupt_record_identity": self._service.verify_record(malformed_record) if malformed_record else {"outcome": "UNKNOWN"}, + } + fabric_status = _aggregate( + [str(item.get("outcome", "UNKNOWN")) for item in [reconciliation] if isinstance(item, Mapping)] + + [item.fabric_outcome for item in observations] + ) + observation_replay = {} + for item, replay in zip(observations, replay_results): + object.__setattr__(item, "replay_identity", replay.get("replay_identity")) + observation_replay[item.worker_identity or "unknown"] = replay + return FabricReferenceResult( + workload=workload, + observations=tuple(observations), + reconciliation={**reconciliation, "scope": "local-in-process-replication", "independence": "UNKNOWN"}, + bundle={ + "mncs_status": bundle.status, + "logical_identity": bundle.logical_identity, + "archive_identity": bundle.archive_identity, + "fabric_manifest_identity": manifest["manifest_identity"], + "verified": "PASS" if bundle.status == "PASS" else bundle.status, + "pre_staged": "PASS", + "executed": "UNKNOWN", + "official_receipt_binding": "UNKNOWN", + "receipt_binding_probe": { + "status": _aggregate( + [str(item.get("status", "UNKNOWN")) for item in receipt_bindings] + ) if receipt_bindings else "UNKNOWN", + "results": receipt_bindings, + "semantics": "probe only; Fabric did not execute the MNCS archive", + }, + "issues": list(bundle.issues), + }, + replay={"results": replay_results, "store": "workspace-local-development-ledger"}, + negative_cases=negative_cases, + fabric_status=fabric_status, + limitations=( + "Local logical workers share one controller process and host.", + "Replication is not independent evaluation or protected custody.", + "MNCS archive execution is UNKNOWN because Fabric native bundle transfer is not claimed.", + "Fabric local execution is bounded but not a hostile-code security sandbox.", + ), + ) + + +@dataclass(frozen=True, slots=True) +class FabricWorkerEndpoint: + worker_id: str + host: str + port: int + capabilities: tuple[str, ...] + ca_file: Path + client_cert: Path + client_key: Path + trust_store: Path + + +@dataclass(frozen=True, slots=True) +class FabricNetworkConfig: + controller_id: str + state_path: Path + workers: tuple[FabricWorkerEndpoint, ...] + pre_staged_bundle_identity: str | None = None + + @classmethod + def load(cls, path: str | Path) -> "FabricNetworkConfig": + import tomllib + + source = Path(path).resolve(strict=True) + try: + raw = tomllib.loads(source.read_text(encoding="utf-8")) + except (OSError, tomllib.TOMLDecodeError) as error: + raise FabricError(f"invalid RAVEL Fabric network configuration: {error}") from error + base = source.parent + workers = tuple( + FabricWorkerEndpoint( + worker_id=str(item["worker_id"]), + host=str(item["host"]), + port=int(item["port"]), + capabilities=tuple(str(value) for value in item["capabilities"]), + ca_file=(base / str(item["ca_file"])).resolve(), + client_cert=(base / str(item["client_cert"])).resolve(), + client_key=(base / str(item["client_key"])).resolve(), + trust_store=(base / str(item["trust_store"])).resolve(), + ) + for item in raw.get("workers", []) + ) + if not raw.get("controller_id") or not workers: + raise FabricError("network configuration requires controller_id and workers") + if len({item.worker_id for item in workers}) != len(workers): + raise FabricError("network workers must have unique identities") + for worker in workers: + if not worker.capabilities or not 1 <= worker.port <= 65535: + raise FabricError("network worker endpoint is incomplete") + for path_value in ( + worker.ca_file, + worker.client_cert, + worker.client_key, + worker.trust_store, + ): + if not path_value.is_file(): + raise FabricUnavailableError(f"network trust material unavailable: {path_value}") + bundle = raw.get("pre_staged_bundle_identity") + if bundle is not None and not _is_external_identity(bundle): + raise FabricError("pre_staged_bundle_identity is invalid") + return cls(str(raw["controller_id"]), (base / str(raw.get("state_path", "fabric-controller.jsonl"))).resolve(), workers, bundle) + + +class FabricNetworkBackend: + """Optional TLS-only adapter; bundle transfer remains operator-owned.""" + + backend_identity = "ravel-fabric-network-public-controller/0.1" + + def __init__(self, config: FabricNetworkConfig) -> None: + try: + from mncs_fabric.controller import NetworkController + from mncs_fabric.enrollment import TrustStore + from mncs_fabric.transport import TLSNetworkTransport + except ImportError as error: + raise FabricUnavailableError("mncs-fabric network API is unavailable") from error + self.config = config + self.controller = NetworkController(config.controller_id, config.state_path) + for worker in config.workers: + transport = TLSNetworkTransport( + worker.host, + worker.port, + ca_file=worker.ca_file, + client_cert=worker.client_cert, + client_key=worker.client_key, + expected_worker_id=worker.worker_id, + trust_store=TrustStore(worker.trust_store), + ) + self.controller.register_remote( + worker.worker_id, frozenset(worker.capabilities), transport + ) + + def dispatch(self, plan: Mapping[str, Any], manifest: Mapping[str, Any], *, replicas: int = 1) -> list[dict[str, Any]]: + expected = self.config.pre_staged_bundle_identity + if expected is None or expected != manifest.get("manifest_identity"): + raise FabricUnavailableError( + "network Fabric requires an explicitly pre-staged matching Fabric manifest" + ) + return self.controller.dispatch_remote(dict(plan), dict(manifest), replicas=replicas) + + +__all__ = [ + "DEVELOPMENT_PARTITION", + "DEVELOPMENT_VISIBILITY", + "ExecutionBackend", + "FabricError", + "FabricExecutionObservation", + "FabricLocalBackend", + "FabricNetworkBackend", + "FabricNetworkConfig", + "FabricQuestion", + "FabricReferenceResult", + "FabricUnavailableError", + "FabricWorkerEndpoint", + "FabricWorkload", +] diff --git a/tests/test_ravel_0_6_transaction.py b/tests/test_ravel_0_6_transaction.py index 6e551df..c2a2490 100644 --- a/tests/test_ravel_0_6_transaction.py +++ b/tests/test_ravel_0_6_transaction.py @@ -140,6 +140,30 @@ def test_evaluator_marks_malformed_required_evidence_unknown(self) -> None: self.assertEqual(evaluation.status, "UNKNOWN") self.assertIn("malformed_required_observation", evaluation.reason_codes) + def test_mechanism_failure_with_valid_compute_remains_fail(self) -> None: + with tempfile.TemporaryDirectory() as directory: + payload = build_and_trial( + build_candidate_source(FROZEN_SOURCE.read_bytes()), Path(directory) + ) + payload["candidate"]["adaptation_transaction"]["raw"]["base_accuracy_after_q20"] = 0 + payload["candidate"]["adaptation_transaction"]["committed"] = False + payload["candidate"]["adaptation_transaction"]["failed_constraint_mask"] = 4 + evaluation = evaluate_trial(payload, expected_provider_id="ravel-toy-branching-c/1") + self.assertEqual(evaluation.mechanism_status, "FAIL") + self.assertEqual(evaluation.matched_compute_status, "PASS") + self.assertEqual(evaluation.status, "FAIL") + + def test_execution_integrity_failure_is_unknown_not_mechanism_failure(self) -> None: + with tempfile.TemporaryDirectory() as directory: + payload = build_and_trial( + build_candidate_source(FROZEN_SOURCE.read_bytes()), Path(directory) + ) + payload["execution_integrity_status"] = "FAIL" + evaluation = evaluate_trial(payload, expected_provider_id="ravel-toy-branching-c/1") + self.assertEqual(evaluation.mechanism_status, "PASS") + self.assertEqual(evaluation.execution_integrity_status, "UNKNOWN") + self.assertEqual(evaluation.status, "UNKNOWN") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_ravel_fabric.py b/tests/test_ravel_fabric.py new file mode 100644 index 0000000..da17877 --- /dev/null +++ b/tests/test_ravel_fabric.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import tempfile +import unittest +import json +from pathlib import Path + +from ravel.experience import ExperienceRecord +from ravel.fabric import ( + FabricError, + FabricLocalBackend, + FabricQuestion, + FabricUnavailableError, + FabricWorkload, + FabricNetworkConfig, +) +from ravel.memory import MemoryClass +from ravel.memory.store import SQLiteMemoryStore + + +class FabricContractTests(unittest.TestCase): + def test_workload_is_development_only_and_binds_candidate(self) -> None: + workload = FabricWorkload( + candidate_identity="ravel-0.6-candidate-001", + experiment_identity="sha256:" + "1" * 64, + question_kind=FabricQuestion.PROVIDER_PARITY, + bundle_identity="sha256:" + "2" * 64, + fabric_manifest_identity="sha256:" + "3" * 64, + ) + self.assertEqual(workload.to_dict()["visibility"], "development-visible") + self.assertEqual(workload.to_dict()["authority"], "development-only") + self.assertNotEqual(workload.workload_identity, workload.candidate_binding_identity) + with self.assertRaises(FabricError): + FabricWorkload( + candidate_identity="ravel-0.6-candidate-001", + experiment_identity="sha256:" + "1" * 64, + question_kind=FabricQuestion.PROVIDER_PARITY, + bundle_identity="sha256:" + "2" * 64, + fabric_manifest_identity="sha256:" + "3" * 64, + visibility="selection-visible", + ) + + def test_missing_required_fabric_identity_is_rejected(self) -> None: + with self.assertRaises(FabricError): + FabricWorkload( + candidate_identity="ravel-0.6-candidate-001", + experiment_identity="not-an-identity", + question_kind=FabricQuestion.PROVIDER_PARITY, + bundle_identity="sha256:" + "2" * 64, + fabric_manifest_identity="sha256:" + "3" * 64, + ) + + def test_network_template_never_falls_back_without_operator_trust(self) -> None: + with self.assertRaises(FabricUnavailableError): + FabricNetworkConfig.load("config/ravel-fabric.example.toml") + + +class FabricLocalReferenceTests(unittest.TestCase): + def setUp(self) -> None: + self.directory = tempfile.TemporaryDirectory(prefix="ravel-test-fabric-") + self.backend = FabricLocalBackend(Path(self.directory.name)) + if not self.backend.available: + self.skipTest(self.backend.unavailable_reason or "mncs-fabric unavailable") + + def tearDown(self) -> None: + self.directory.cleanup() + + def test_branching_local_replication_replay_and_negative_matrix(self) -> None: + report = self.backend.execute_provider_parity("branching") + self.assertEqual(report.fabric_status, "PASS") + self.assertEqual(report.bundle["mncs_status"], "PASS") + self.assertEqual(report.bundle["verified"], "PASS") + self.assertEqual(report.bundle["pre_staged"], "PASS") + self.assertEqual(report.bundle["executed"], "UNKNOWN") + self.assertEqual(report.bundle["official_receipt_binding"], "UNKNOWN") + self.assertEqual(report.bundle["receipt_binding_probe"]["status"], "FAIL") + self.assertEqual(len(report.bundle["receipt_binding_probe"]["results"]), 2) + self.assertEqual(report.reconciliation["outcome"], "PASS") + self.assertEqual(report.reconciliation["scope"], "local-in-process-replication") + self.assertEqual(report.reconciliation["independence"], "UNKNOWN") + self.assertEqual(len(report.observations), 2) + self.assertTrue(all(item.fabric_outcome == "PASS" for item in report.observations)) + self.assertTrue(all(item.challenge_identity for item in report.observations)) + self.assertTrue(all(item.replay_identity for item in report.observations)) + self.assertTrue(all(item.bundle_identity == report.bundle["logical_identity"] for item in report.observations)) + self.assertTrue(all(item.semantics.endswith("not evaluator authority") for item in report.observations)) + self.assertEqual(report.replay["results"][0]["first"], "PASS") + self.assertEqual(report.replay["results"][0]["duplicate"], "FAIL") + self.assertEqual(report.negative_cases["duplicate_request"], "DUPLICATE_IDEMPOTENT") + self.assertEqual(report.negative_cases["conflicting_replay"], "CONFLICTING_REPLAY") + self.assertEqual(report.negative_cases["capability_mismatch"]["outcome"], "UNKNOWN") + self.assertEqual(report.negative_cases["wrong_manifest"]["outcome"], "FAIL") + self.assertEqual(report.negative_cases["corrupt_record_identity"]["outcome"], "FAIL") + try: + import jsonschema + except ImportError: + jsonschema = None + if jsonschema is not None: + schema = json.loads( + Path("ravel_versions/0.6/ravel-0.6-fabric-observation.schema.json").read_text() + ) + jsonschema.validate( + {"schema": "ravel-fabric-reference-run/0.1", "status": report.fabric_status, + "authority": "development-only", "semantics": "reference", "providers": [report.to_dict()]}, + schema, + ) + + def test_ring_local_replication_has_same_contract(self) -> None: + report = self.backend.execute_provider_parity("ring") + self.assertEqual(report.fabric_status, "PASS") + self.assertEqual(report.reconciliation["outcome"], "PASS") + self.assertTrue(all(item.provider_identity == "ring" for item in report.observations)) + self.assertTrue(all(item.candidate_binding_identity == report.workload.candidate_binding_identity for item in report.observations)) + + def test_fabric_observation_enters_memory_as_scoped_unknown(self) -> None: + report = self.backend.execute_provider_parity("branching", replication_count=1) + experience = ExperienceRecord.from_fabric_observation(report.observations[0].to_dict()) + self.assertEqual(experience.formal_disposition, "UNKNOWN") + self.assertEqual(experience.task_environment, "mncs-fabric") + self.assertEqual(experience.raw_result["fabric_reference"]["bundle_identity"], report.bundle["logical_identity"]) + self.assertNotIn("record", experience.raw_result) + with tempfile.TemporaryDirectory(prefix="ravel-test-memory-") as directory: + with SQLiteMemoryStore(f"{directory}/memory.sqlite") as store: + store.insert_records_atomic((experience.to_memory_record(created_at="2026-08-09T00:00:00Z"),)) + records = store.search_records("fabric development execution") + self.assertEqual(len(records), 1) + self.assertIs(records[0][0].memory_class, MemoryClass.NEGATIVE) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/ravel_fabric_reference.py b/tools/ravel_fabric_reference.py new file mode 100644 index 0000000..daee585 --- /dev/null +++ b/tools/ravel_fabric_reference.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Run the bounded local Fabric reference matrix for RAVEL development.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import shutil +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from ravel.fabric import FabricError, FabricLocalBackend, _aggregate + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--workspace", + type=Path, + default=Path("build/fabric-reference"), + help="temporary workspace for generated artifacts and Fabric records", + ) + parser.add_argument("--json", action="store_true", help="emit the report as JSON") + args = parser.parse_args() + backend = FabricLocalBackend(args.workspace) + if not backend.available: + report = { + "schema": "ravel-fabric-reference-run/0.1", + "status": "UNKNOWN", + "authority": "development-only", + "reason": "Fabric capability unavailable", + "detail": backend.unavailable_reason, + } + print(json.dumps(report, sort_keys=True)) + return 0 + + reports = [] + run_workspace = Path(tempfile.mkdtemp(prefix="run-", dir=args.workspace)) + try: + backend = FabricLocalBackend(run_workspace) + for provider in ("branching", "ring"): + try: + reports.append(backend.execute_provider_parity(provider).to_dict()) + except FabricError as error: + reports.append({"provider": provider, "status": "UNKNOWN", "reason": str(error)}) + finally: + shutil.rmtree(run_workspace, ignore_errors=True) + status = _aggregate( + [str(report.get("fabric_status", report.get("status", "UNKNOWN"))) for report in reports] + ) + report = { + "schema": "ravel-fabric-reference-run/0.1", + "backend_identity": backend.backend_identity, + "status": status, + "authority": "development-only", + "semantics": "Fabric observations are diagnostic evidence, not evaluator authority", + "providers": reports, + "selection_material": "not-dispatched", + "final_material": "not-dispatched", + } + print(json.dumps(report, sort_keys=True, indent=2 if args.json else None)) + return 0 if status in {"PASS", "UNKNOWN"} else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/ravel_forge_check.py b/tools/ravel_forge_check.py index fc22a3f..14b13ec 100644 --- a/tools/ravel_forge_check.py +++ b/tools/ravel_forge_check.py @@ -11,6 +11,7 @@ import unittest ROOT = Path(__file__).resolve().parents[1] +FABRIC_LOCK = ROOT / "ravel_versions/0.6/ravel-0.6-family-compatibility-lock.json" def _run(module: str, *tests: str) -> tuple[str, str]: @@ -81,6 +82,80 @@ def check(name: str) -> tuple[str, str]: if separate != unity or record["component_contracts"]["world"]["abi_version"] != "ravel-0.6-world-abi/1": return "FAIL", f"{provider} unity/separate facts differ" return "PASS", "branching and ring unity/separate raw trials matched" + if name == "fabric-capabilities": + try: + from ravel.fabric import FabricLocalBackend + + with tempfile.TemporaryDirectory(prefix="ravel-forge-fabric-capabilities-") as directory: + backend = FabricLocalBackend(Path(directory)) + if not backend.available: + return "UNKNOWN", backend.unavailable_reason or "Fabric unavailable" + capabilities = backend.capabilities("local-reference-worker") + return "PASS", json.dumps({"backend": backend.backend_identity, "capabilities": capabilities}, sort_keys=True) + except ImportError as error: + return "UNKNOWN", f"Fabric capability unavailable: {type(error).__name__}" + if name == "fabric-reference": + result = subprocess.run( + ["python3", "tools/ravel_fabric_reference.py", "--workspace", "build/forge-fabric-reference"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + detail = result.stdout[-12000:] or result.stderr[-2000:] + if result.returncode not in {0}: + return "FAIL", detail + try: + report = json.loads(result.stdout) + except json.JSONDecodeError: + return "FAIL", "Fabric reference did not emit JSON" + return str(report.get("status", "UNKNOWN")), detail + if name == "fabric-negative": + return _run(name, "tests/test_ravel_fabric.py") + if name == "family-compatibility-lock": + if not FABRIC_LOCK.is_file(): + return "FAIL", "RAVEL family compatibility lock is missing" + lock = json.loads(FABRIC_LOCK.read_text(encoding="utf-8")) + missing: list[str] = [] + drifted: dict[str, dict[str, str]] = {} + unresolved: dict[str, str] = {} + for name_, entry in lock.get("contracts", {}).items(): + candidates = { + "mncs-fabric": ROOT.parent / "mncs-fabric", + "mncs-forge-mcp": ROOT.parent / "mncs-forge-mcp", + "machine-native-complexity-standard": ROOT.parent / "machine-native-complexity-standard", + "Machine-Native-Experimental-Learning": ROOT.parent / "Machine-Native-Experimental-Learning", + "MNCS-Commons": ROOT.parent / "MNCS-Commons", + "mncs-language": ROOT.parent / "mncs-language", + } + path = candidates[name_] + if not path.is_dir(): + missing.append(name_) + continue + current = subprocess.run( + ["git", "-C", str(path), "rev-parse", "HEAD"], + text=True, + capture_output=True, + check=False, + ) + actual = current.stdout.strip() + expected = str(entry.get("commit")) + if current.returncode != 0 or actual != expected: + drifted[name_] = {"expected": expected, "actual": actual or "UNKNOWN"} + continue + dirty = subprocess.run( + ["git", "-C", str(path), "status", "--porcelain"], + text=True, + capture_output=True, + check=False, + ).stdout.strip() + if dirty: + unresolved[name_] = "checkout has uncommitted changes" + if drifted: + return "FAIL", json.dumps({"status": "DRIFTED", "contracts": drifted}, sort_keys=True) + if missing or unresolved: + return "UNKNOWN", json.dumps({"status": "UNKNOWN", "missing": missing, "unresolved": unresolved}, sort_keys=True) + return "PASS", json.dumps({"status": "COMPATIBLE", "contracts": sorted(lock["contracts"])}, sort_keys=True) if name == "package": return _run(name, "tests/test_frozen_identities.py", "tests/test_lifecycle_experience.py") if name == "live-family-compat":