diff --git a/CHANGELOG.md b/CHANGELOG.md index 532b505d..9dfdb71f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ ### Added +- Local verification now maps `warp-core` optic artifact and causal fact source + changes to the exact integration test targets they exercise, avoiding broad + Cargo name-filter runs while preserving targeted smoke coverage. - `warp-core` now publishes in-memory causal graph facts from optic artifact registration. Successful registration emits `GraphFact::ArtifactRegistered`, computes a deterministic `FactDigest`, and links that digest from an @@ -18,16 +21,14 @@ publication boundary: facts are world statements, receipts explain publication/refusal boundaries, and registration facts are the first in-memory proof that Echo can describe its own runtime decisions. -- `echo-wesley-gen` now imports real `wesley-core` 0.0.3 runtime optic +- `echo-wesley-gen` now imports real `wesley-core` 0.0.4 runtime optic artifacts into `warp-core` registration structs, preserving Wesley artifact hashes, schema ids, operation ids, requirements digests, and registration descriptors while keeping `warp-core` free of a Wesley dependency. Echo still - owns opaque runtime-local `OpticArtifactHandle` issuance, and the imported - requirements bytes are staged through an explicit adapter-local v0 - canonicalization helper for registration proof only, not enforcement. - `warp-core` stores those requirements as opaque bytes and must not interpret - them for admission until Wesley exposes canonical requirements bytes and a - durable codec. + owns opaque runtime-local `OpticArtifactHandle` issuance. Imported admission + requirements now preserve Wesley-owned canonical requirement bytes, codec id, + and digest directly from `OpticAdmissionRequirementsArtifact`; downstream + runtimes must not serialize Wesley structs to create admission truth. - `docs/procedures/DIRECT-MAIN-EXCEPTION-LOG.md` records the 2026-05-14 docs-only direct-main exception for the Echo graph model checkpoint, including authorization context, exact commits, validation, changed files, and the diff --git a/Cargo.lock b/Cargo.lock index 665d53a2..1cfcb12c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2162,9 +2162,9 @@ dependencies = [ [[package]] name = "wesley-core" -version = "0.0.3" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185e806cc7e9d4263023562a00832d777ab146b902f5ee1531940dc39e6d6515" +checksum = "96faa36a92a4b0b92d27c467b34a67e006943795f2b9a0bc1e1237729afd17c0" dependencies = [ "apollo-parser", "async-trait", diff --git a/crates/echo-wesley-gen/Cargo.toml b/crates/echo-wesley-gen/Cargo.toml index d8365458..70684c1f 100644 --- a/crates/echo-wesley-gen/Cargo.toml +++ b/crates/echo-wesley-gen/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" syn = { version = "2.0", features = ["full", "extra-traits"] } prettyplease = "0.2" warp-core = { workspace = true } -wesley-core = "0.0.3" +wesley-core = "0.0.4" [lints] diff --git a/crates/echo-wesley-gen/README.md b/crates/echo-wesley-gen/README.md index 825f8dac..7a42944a 100644 --- a/crates/echo-wesley-gen/README.md +++ b/crates/echo-wesley-gen/README.md @@ -45,3 +45,7 @@ cat ir.json | cargo run -p echo-wesley-gen -- --out generated.rs retained reading identity. - Optional fields become `Option`; lists become `Vec` (wrapped in Option when not required). - Unknown scalar names are emitted as identifiers as-is (so ensure upstream IR types are valid Rust idents). +- Runtime optic artifact imports preserve Wesley-owned canonical admission + requirement bytes, codec id, and digest directly from + `OpticAdmissionRequirementsArtifact`; Echo stores them as opaque registry + payload and does not reserialize Wesley requirement structs. diff --git a/crates/echo-wesley-gen/src/lib.rs b/crates/echo-wesley-gen/src/lib.rs index 3c9620d0..af9d45a1 100644 --- a/crates/echo-wesley-gen/src/lib.rs +++ b/crates/echo-wesley-gen/src/lib.rs @@ -7,18 +7,10 @@ //! runtime registration and opaque handles. This crate is the dependency seam //! that may see both sides. //! -//! The v0 adapter stores Wesley admission requirements as deterministic -//! `serde_json` bytes in `warp-core`. Those bytes are registry payload only; -//! enforcement, grant validation, admission tickets, witnesses, and execution -//! are intentionally out of scope for this adapter. - -/// Adapter-local staging codec for imported Wesley admission requirements. -/// -/// This is not runtime admission truth. It is a v0 canonical staging format so -/// Echo can store opaque requirements bytes while Wesley grows a durable -/// canonical requirements byte/codec surface. -pub const WESLEY_REQUIREMENTS_STAGING_CODEC_V0: &str = - "echo-wesley-gen/wesley-requirements-canonical-json-staging/v0"; +//! The adapter imports Wesley-owned canonical admission requirement bytes, +//! codec ids, and digests into `warp-core` without reserializing Wesley +//! requirement structs. Enforcement, grant validation, admission tickets, +//! witnesses, and execution are intentionally out of scope for this adapter. /// Imported Wesley runtime optic artifact ready for Echo registration. #[derive(Debug, Clone, PartialEq, Eq)] @@ -37,40 +29,31 @@ pub struct ImportedRuntimeOpticArtifact { pub fn import_runtime_optic_artifact( artifact: &wesley_core::OpticArtifact, ) -> anyhow::Result { - let requirements_bytes = canonicalize_wesley_requirements_v0(&artifact.requirements)?; + if artifact.requirements_digest != artifact.requirements_artifact.digest { + anyhow::bail!( + "Wesley artifact requirements digest does not match requirements artifact digest" + ); + } Ok(ImportedRuntimeOpticArtifact { artifact: warp_core::OpticArtifact { artifact_id: artifact.artifact_id.clone(), artifact_hash: artifact.artifact_hash.clone(), schema_id: artifact.schema_id.clone(), - requirements_digest: artifact.requirements_digest.clone(), + requirements_digest: artifact.requirements_artifact.digest.clone(), operation: warp_core::OpticArtifactOperation { operation_id: artifact.operation.operation_id.clone(), }, requirements: warp_core::OpticAdmissionRequirements { - bytes: requirements_bytes, + codec: artifact.requirements_artifact.codec.clone(), + digest: artifact.requirements_artifact.digest.clone(), + bytes: artifact.requirements_artifact.bytes.clone(), }, }, descriptor: import_registration_descriptor(&artifact.registration), }) } -/// Canonicalizes Wesley admission requirements for adapter-local v0 staging. -/// -/// The helper deliberately names the seam: these bytes are not permanent -/// artifact truth and `warp-core` must not interpret them for admission. -/// Durable admission should eventually consume Wesley-owned canonical -/// requirements bytes plus an explicit codec id. -pub fn canonicalize_wesley_requirements_v0( - requirements: &wesley_core::OpticAdmissionRequirements, -) -> anyhow::Result> { - let value = serde_json::to_value(requirements)?; - let mut bytes = Vec::new(); - write_canonical_json_value(&value, &mut bytes)?; - Ok(bytes) -} - /// Imports a Wesley registration descriptor into Echo's registration shape. pub fn import_registration_descriptor( descriptor: &wesley_core::OpticRegistrationDescriptor, @@ -83,77 +66,3 @@ pub fn import_registration_descriptor( requirements_digest: descriptor.requirements_digest.clone(), } } - -fn write_canonical_json_value( - value: &serde_json::Value, - bytes: &mut Vec, -) -> anyhow::Result<()> { - match value { - serde_json::Value::Null => bytes.extend_from_slice(b"null"), - serde_json::Value::Bool(true) => bytes.extend_from_slice(b"true"), - serde_json::Value::Bool(false) => bytes.extend_from_slice(b"false"), - serde_json::Value::Number(number) => { - bytes.extend_from_slice(serde_json::to_string(number)?.as_bytes()); - } - serde_json::Value::String(text) => { - bytes.extend_from_slice(serde_json::to_string(text)?.as_bytes()); - } - serde_json::Value::Array(values) => { - bytes.push(b'['); - for (index, item) in values.iter().enumerate() { - if index > 0 { - bytes.push(b','); - } - write_canonical_json_value(item, bytes)?; - } - bytes.push(b']'); - } - serde_json::Value::Object(object) => { - let mut entries: Vec<_> = object.iter().collect(); - entries.sort_by(|(left_key, _), (right_key, _)| left_key.cmp(right_key)); - - bytes.push(b'{'); - for (index, (key, item)) in entries.into_iter().enumerate() { - if index > 0 { - bytes.push(b','); - } - bytes.extend_from_slice(serde_json::to_string(key)?.as_bytes()); - bytes.push(b':'); - write_canonical_json_value(item, bytes)?; - } - bytes.push(b'}'); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::write_canonical_json_value; - - #[test] - fn canonical_json_writer_sorts_object_keys_recursively() -> anyhow::Result<()> { - let left = serde_json::json!({ - "z": [{"b": 2, "a": 1}], - "a": {"d": false, "c": true} - }); - let right = serde_json::json!({ - "a": {"c": true, "d": false}, - "z": [{"a": 1, "b": 2}] - }); - let mut left_bytes = Vec::new(); - let mut right_bytes = Vec::new(); - - write_canonical_json_value(&left, &mut left_bytes)?; - write_canonical_json_value(&right, &mut right_bytes)?; - - assert_eq!(left_bytes, right_bytes); - assert_eq!( - left_bytes, - br#"{"a":{"c":true,"d":false},"z":[{"a":1,"b":2}]}"# - ); - - Ok(()) - } -} diff --git a/crates/echo-wesley-gen/src/main.rs b/crates/echo-wesley-gen/src/main.rs index cfbc74b4..18ba2f3b 100644 --- a/crates/echo-wesley-gen/src/main.rs +++ b/crates/echo-wesley-gen/src/main.rs @@ -23,7 +23,7 @@ use ir::{OpKind, TypeKind, WesleyIR}; const ECHO_IR_VERSION: &str = "echo-ir/v1"; const DEFAULT_CODEC_ID: &str = "cbor-canon-v1"; const DEFAULT_REGISTRY_VERSION: u32 = 1; -const WESLEY_CORE_VERSION: &str = "0.0.3"; +const WESLEY_CORE_VERSION: &str = "0.0.4"; #[derive(Parser)] #[command( diff --git a/crates/echo-wesley-gen/tests/runtime_optic_import.rs b/crates/echo-wesley-gen/tests/runtime_optic_import.rs index 3412db54..d76e8e28 100644 --- a/crates/echo-wesley-gen/tests/runtime_optic_import.rs +++ b/crates/echo-wesley-gen/tests/runtime_optic_import.rs @@ -3,10 +3,7 @@ #![allow(clippy::expect_used)] //! Proof that `echo-wesley-gen` adapts real Wesley runtime optic artifacts into Echo. -use echo_wesley_gen::{ - canonicalize_wesley_requirements_v0, import_runtime_optic_artifact, - WESLEY_REQUIREMENTS_STAGING_CODEC_V0, -}; +use echo_wesley_gen::import_runtime_optic_artifact; use warp_core::{ OpticArtifactRegistrationError, OpticArtifactRegistry, OPTIC_ARTIFACT_HANDLE_KIND, }; @@ -139,6 +136,14 @@ fn imports_real_wesley_runtime_optic_artifact() { !imported.artifact.requirements.bytes.is_empty(), "Wesley requirements must be imported into Echo registry bytes" ); + assert_eq!( + imported.artifact.requirements.codec, + wesley_artifact.requirements_artifact.codec + ); + assert_eq!( + imported.artifact.requirements.digest, + wesley_artifact.requirements_artifact.digest + ); } #[test] @@ -171,18 +176,21 @@ fn registers_imported_wesley_artifact_and_returns_opaque_handle() { #[test] fn imported_requirements_bytes_are_deterministic() { - let wesley_artifact = compile_fixture_artifact(); - let first = canonicalize_wesley_requirements_v0(&wesley_artifact.requirements) - .expect("requirements canonicalization should succeed"); - let second = canonicalize_wesley_requirements_v0(&wesley_artifact.requirements) - .expect("requirements canonicalization should repeat"); + let first_artifact = compile_fixture_artifact(); + let second_artifact = compile_fixture_artifact(); let first_import = - import_runtime_optic_artifact(&wesley_artifact).expect("artifact import should succeed"); + import_runtime_optic_artifact(&first_artifact).expect("artifact import should succeed"); let second_import = - import_runtime_optic_artifact(&wesley_artifact).expect("artifact import should repeat"); + import_runtime_optic_artifact(&second_artifact).expect("artifact import should repeat"); - assert_eq!(first, second); - assert_eq!(first_import.artifact.requirements.bytes, first); + assert_eq!( + first_artifact.requirements_artifact, + second_artifact.requirements_artifact + ); + assert_eq!( + first_import.artifact.requirements.bytes, + first_artifact.requirements_artifact.bytes + ); assert_eq!( first_import.artifact.requirements.bytes, second_import.artifact.requirements.bytes @@ -192,23 +200,64 @@ fn imported_requirements_bytes_are_deterministic() { #[test] fn imported_requirements_bytes_are_not_empty() { let wesley_artifact = compile_fixture_artifact(); - let bytes = canonicalize_wesley_requirements_v0(&wesley_artifact.requirements) - .expect("requirements canonicalization should succeed"); - assert!(!bytes.is_empty()); + assert!(!wesley_artifact.requirements_artifact.bytes.is_empty()); +} + +#[test] +fn imports_wesley_owned_requirements_artifact_without_reserializing_requirements() { + let wesley_artifact = compile_fixture_artifact(); + let imported = + import_runtime_optic_artifact(&wesley_artifact).expect("artifact import should succeed"); + + assert_eq!( + imported.artifact.requirements_digest, + wesley_artifact.requirements_artifact.digest + ); + assert_eq!( + imported.artifact.requirements.digest, + wesley_artifact.requirements_artifact.digest + ); + assert_eq!( + imported.artifact.requirements.codec, + wesley_artifact.requirements_artifact.codec + ); + assert_eq!( + imported.artifact.requirements.bytes, + wesley_artifact.requirements_artifact.bytes + ); } #[test] -fn imported_requirements_bytes_are_adapter_canonical_staging() { +fn imported_requirements_artifact_is_wesley_owned() { let wesley_artifact = compile_fixture_artifact(); let imported = import_runtime_optic_artifact(&wesley_artifact).expect("artifact import should succeed"); - let staged_bytes = canonicalize_wesley_requirements_v0(&wesley_artifact.requirements) - .expect("requirements canonicalization should succeed"); - assert_eq!(imported.artifact.requirements.bytes, staged_bytes); - assert!(WESLEY_REQUIREMENTS_STAGING_CODEC_V0.contains("staging")); - assert!(WESLEY_REQUIREMENTS_STAGING_CODEC_V0.ends_with("/v0")); + assert_eq!( + imported.artifact.requirements.bytes, + wesley_artifact.requirements_artifact.bytes + ); + assert_eq!( + imported.artifact.requirements.codec, + wesley_artifact.requirements_artifact.codec + ); + assert_eq!( + imported.artifact.requirements.digest, + wesley_artifact.requirements_artifact.digest + ); + assert_eq!( + imported.artifact.requirements.digest, + imported.artifact.requirements_digest + ); +} + +#[test] +fn rejects_wesley_artifact_with_mismatched_requirements_artifact_digest() { + let mut wesley_artifact = compile_fixture_artifact(); + wesley_artifact.requirements_digest = "tampered-requirements-digest".to_owned(); + + assert!(import_runtime_optic_artifact(&wesley_artifact).is_err()); } #[test] @@ -302,6 +351,6 @@ fn warp_core_does_not_depend_on_serde_json_for_wesley_requirements() { assert!( active_serde_json_lines.is_empty(), - "warp-core must not depend on serde_json for Wesley requirements; JSON staging belongs in echo-wesley-gen" + "warp-core must not depend on serde_json for Wesley requirements; canonical requirements bytes belong to Wesley" ); } diff --git a/crates/warp-core/src/optic_artifact.rs b/crates/warp-core/src/optic_artifact.rs index 01225df8..2d5bc27b 100644 --- a/crates/warp-core/src/optic_artifact.rs +++ b/crates/warp-core/src/optic_artifact.rs @@ -56,8 +56,11 @@ pub struct OpticArtifactOperation { /// must not provide replacement requirements or footprint law. #[derive(Clone, Debug, PartialEq, Eq)] pub struct OpticAdmissionRequirements { - /// Opaque requirement bytes. Later slices can replace this fixture-shaped - /// payload with shared Wesley/Continuum types without changing ownership. + /// Explicit codec id for the opaque requirement bytes. + pub codec: String, + /// Wesley-computed digest of the opaque requirement bytes. + pub digest: String, + /// Opaque requirement bytes emitted by Wesley. pub bytes: Vec, } diff --git a/crates/warp-core/tests/causal_fact_publication_tests.rs b/crates/warp-core/tests/causal_fact_publication_tests.rs index f5284d55..5718ad83 100644 --- a/crates/warp-core/tests/causal_fact_publication_tests.rs +++ b/crates/warp-core/tests/causal_fact_publication_tests.rs @@ -18,6 +18,8 @@ fn fixture_artifact() -> OpticArtifact { operation_id: "operation:textWindow:v0".to_owned(), }, requirements: OpticAdmissionRequirements { + codec: "wesley.requirements.canonical-json.v0".to_owned(), + digest: "requirements-digest:stack-witness-0001".to_owned(), bytes: b"fixture admission requirements".to_vec(), }, } diff --git a/crates/warp-core/tests/optic_artifact_registry_tests.rs b/crates/warp-core/tests/optic_artifact_registry_tests.rs index 27a8cb77..945e6465 100644 --- a/crates/warp-core/tests/optic_artifact_registry_tests.rs +++ b/crates/warp-core/tests/optic_artifact_registry_tests.rs @@ -17,6 +17,8 @@ fn fixture_artifact() -> OpticArtifact { operation_id: "operation:textWindow:v0".to_owned(), }, requirements: OpticAdmissionRequirements { + codec: "wesley.requirements.canonical-json.v0".to_owned(), + digest: "requirements-digest:stack-witness-0001".to_owned(), bytes: b"fixture admission requirements".to_vec(), }, } diff --git a/crates/warp-core/tests/optic_invocation_admission_tests.rs b/crates/warp-core/tests/optic_invocation_admission_tests.rs index 07985859..fb864bb6 100644 --- a/crates/warp-core/tests/optic_invocation_admission_tests.rs +++ b/crates/warp-core/tests/optic_invocation_admission_tests.rs @@ -19,6 +19,8 @@ fn fixture_artifact() -> OpticArtifact { operation_id: "operation:textWindow:v0".to_owned(), }, requirements: OpticAdmissionRequirements { + codec: "wesley.requirements.canonical-json.v0".to_owned(), + digest: "requirements-digest:stack-witness-0001".to_owned(), bytes: b"fixture admission requirements".to_vec(), }, } diff --git a/scripts/hooks/README.md b/scripts/hooks/README.md index 78c2abaf..84cab9aa 100644 --- a/scripts/hooks/README.md +++ b/scripts/hooks/README.md @@ -54,7 +54,9 @@ proof to CI. That local smoke path is also file-family aware for `warp-core`: ordinary source edits stay on the library test lane, while runtime/inbox, playback, and PRNG touches pull the specific extra smoke checks they need instead of one fixed -bundle every time. +bundle every time. Optic artifact and causal fact source edits also pull their +focused registry, invocation admission, publication, and grant-intent smoke +targets by exact Cargo integration target rather than by test-name filters. The same principle now applies to the WASM boundary crates: `warp-wasm` distinguishes plain lib work from `warp_kernel` engine work, `echo-wasm-abi` diff --git a/scripts/verify-local.sh b/scripts/verify-local.sh index ec787afa..5e61cc65 100755 --- a/scripts/verify-local.sh +++ b/scripts/verify-local.sh @@ -1065,6 +1065,17 @@ prepare_warp_core_scope() { crates/warp-core/tests/*.rs) append_unique "$(basename "$file" .rs)" FULL_SCOPE_WARP_CORE_EXTRA_TESTS ;; + crates/warp-core/src/optic_artifact.rs) + append_unique "optic_artifact_registry_tests" FULL_SCOPE_WARP_CORE_EXTRA_TESTS + append_unique "optic_invocation_admission_tests" FULL_SCOPE_WARP_CORE_EXTRA_TESTS + append_unique "causal_fact_publication_tests" FULL_SCOPE_WARP_CORE_EXTRA_TESTS + append_unique "capability_grant_intent_tests" FULL_SCOPE_WARP_CORE_EXTRA_TESTS + ;; + crates/warp-core/src/causal_facts.rs) + append_unique "causal_fact_publication_tests" FULL_SCOPE_WARP_CORE_EXTRA_TESTS + append_unique "optic_artifact_registry_tests" FULL_SCOPE_WARP_CORE_EXTRA_TESTS + append_unique "optic_invocation_admission_tests" FULL_SCOPE_WARP_CORE_EXTRA_TESTS + ;; crates/warp-core/src/coordinator.rs|\ crates/warp-core/src/engine_impl.rs|\ crates/warp-core/src/head.rs|\ diff --git a/tests/hooks/test_verify_local.sh b/tests/hooks/test_verify_local.sh index 40d4bcff..1ad52a95 100755 --- a/tests/hooks/test_verify_local.sh +++ b/tests/hooks/test_verify_local.sh @@ -1187,6 +1187,52 @@ else pass "warp-core default smoke avoids inbox when the file family does not need it" fi +fake_warp_core_optic_artifact_output="$(run_fake_verify full crates/warp-core/src/optic_artifact.rs)" +if printf '%s\n' "$fake_warp_core_optic_artifact_output" | grep -q -- '--test optic_artifact_registry_tests'; then + pass "optic artifact changes pull the registry smoke test" +else + fail "optic artifact changes should pull the registry smoke test" + printf '%s\n' "$fake_warp_core_optic_artifact_output" +fi +if printf '%s\n' "$fake_warp_core_optic_artifact_output" | grep -q -- '--test optic_invocation_admission_tests'; then + pass "optic artifact changes pull the invocation admission smoke test" +else + fail "optic artifact changes should pull the invocation admission smoke test" + printf '%s\n' "$fake_warp_core_optic_artifact_output" +fi +if printf '%s\n' "$fake_warp_core_optic_artifact_output" | grep -q -- '--test causal_fact_publication_tests'; then + pass "optic artifact changes pull the causal fact publication smoke test" +else + fail "optic artifact changes should pull the causal fact publication smoke test" + printf '%s\n' "$fake_warp_core_optic_artifact_output" +fi +if printf '%s\n' "$fake_warp_core_optic_artifact_output" | grep -q -- '--test capability_grant_intent_tests'; then + pass "optic artifact changes pull the capability grant intent smoke test" +else + fail "optic artifact changes should pull the capability grant intent smoke test" + printf '%s\n' "$fake_warp_core_optic_artifact_output" +fi + +fake_warp_core_causal_facts_output="$(run_fake_verify full crates/warp-core/src/causal_facts.rs)" +if printf '%s\n' "$fake_warp_core_causal_facts_output" | grep -q -- '--test causal_fact_publication_tests'; then + pass "causal fact source changes pull the causal fact publication smoke test" +else + fail "causal fact source changes should pull the causal fact publication smoke test" + printf '%s\n' "$fake_warp_core_causal_facts_output" +fi +if printf '%s\n' "$fake_warp_core_causal_facts_output" | grep -q -- '--test optic_artifact_registry_tests'; then + pass "causal fact source changes pull the artifact registry smoke test" +else + fail "causal fact source changes should pull the artifact registry smoke test" + printf '%s\n' "$fake_warp_core_causal_facts_output" +fi +if printf '%s\n' "$fake_warp_core_causal_facts_output" | grep -q -- '--test optic_invocation_admission_tests'; then + pass "causal fact source changes pull the invocation admission smoke test" +else + fail "causal fact source changes should pull the invocation admission smoke test" + printf '%s\n' "$fake_warp_core_causal_facts_output" +fi + fake_warp_core_runtime_output="$(run_fake_verify full crates/warp-core/src/coordinator.rs)" if printf '%s\n' "$fake_warp_core_runtime_output" | grep -q -- '--test inbox'; then pass "runtime-facing warp-core changes pull the inbox smoke test"