diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21f3ad8..8678b51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: - run: python -m mnel ledger verify build/demo/evidence.jsonl - run: python -m mnel provider-study-reference --workspace build/provider-study-reference - run: python -m mnel ledger verify build/provider-study-reference/provider-portfolio-evidence.jsonl + - run: python -m mnel family-integration-reference --workspace build/family-integration-reference + - run: python -m mnel ledger verify build/family-integration-reference/family-integration-evidence.jsonl - run: git diff --check rust-provider-runtime: diff --git a/CHANGELOG.md b/CHANGELOG.md index 96dad75..d2661d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## 0.4.0a0 — unreleased +- Add a strict MNCS Forge Provider Protocol 0.1 adapter and project-scoped Forge config; + capabilities and analysis responses are bounded, one-line, diagnostic-only records. +- Add pinned MNCS-family compatibility snapshots and a `family-integration-reference` + path using the public Fabric service, typed execution receipts, MNEL normalization, + inert Commons interchange, and RAVEL 0.6 proposal-context fixtures. +- Add a bounded Rust parser/reference inference surface for the existing transition-frequency + artifact, with a checked-in fixture loaded by both Python and Rust; ABI v1 export remains + explicitly open because it lacks portable artifact initialization. + - Complete bounded deterministic skeptic discovery for verifier gaps, disagreements, abstentions, missing counterfactuals, learned-provider disagreement, and verifier health holes. Candidates remain proposal-only and carry visible evidence lineage. diff --git a/Cargo.lock b/Cargo.lock index 5396460..9a9a5f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "libc" version = "0.2.189" @@ -72,6 +78,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + [[package]] name = "mnel-provider-api" version = "0.1.0-alpha.0" @@ -82,6 +94,9 @@ version = "0.1.0-alpha.0" dependencies = [ "mnel-provider-api", "mnel-provider-sdk", + "serde", + "serde_json", + "sha2", ] [[package]] @@ -132,6 +147,67 @@ dependencies = [ "mnel-provider-api", ] +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha2" version = "0.10.9" @@ -143,12 +219,29 @@ dependencies = [ "digest", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + [[package]] name = "version_check" version = "0.9.5" @@ -160,3 +253,9 @@ name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/README.md b/README.md index 533a121..0de5084 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,13 @@ conventional neural-weight training. > unattended model execution, distributed scheduling, protected final custody, formal > MNCS/MNCDS conformance, or automatic RAVEL promotion. +The repository also contains a dependency-aware MNCS-family integration reference: +`mnel family-integration-reference` exposes a Forge Provider Protocol 0.1 adapter, validates +pinned Forge/Fabric/Commons/RAVEL/Language compatibility shapes, executes a bounded local +Fabric job through its public service, consumes the experimental MNCS receipt, and records +the result as diagnostic evidence. Live sibling availability is reported explicitly; no +external checkout is required for ordinary MNEL CI. + ## Core rule **Investigators and learned providers may propose knowledge. They may not declare it true.** diff --git a/compat/mncs-family-compatibility-0.1.json b/compat/mncs-family-compatibility-0.1.json new file mode 100644 index 0000000..40409fc --- /dev/null +++ b/compat/mncs-family-compatibility-0.1.json @@ -0,0 +1,14 @@ +{ + "schema": "mnel-family-compatibility/0.1", + "snapshot_version": "2026-08-08", + "projects": [ + {"repository": "mncs-forge-mcp", "commit": "7710ea606bd592e0be95957c96132e8732fbb955", "public_contract": {"version": "Provider Protocol 0.1", "types": ["capabilities", "analysis_request", "analysis_response"]}}, + {"repository": "mncs-fabric", "commit": "fd6a1e1fe617b77402a3d40c278776dd8f159fb0", "public_contract": {"version": "mncs-fabric public service 0.1", "types": ["FabricService", "job-plan", "execution-record", "reconcile"]}}, + {"repository": "machine-native-complexity-standard", "commit": "1f0a1f903379d740eeeb59fe8689aed398bfb47c", "public_contract": {"version": "execution receipt 0.1-experimental", "types": ["mncs-execution-receipt", "claim_boundary"]}}, + {"repository": "MNCS-Commons", "commit": "5afff351efd3563febb7b3149d7de711d3342a44", "public_contract": {"version": "commons.mncs.dev/v0alpha1", "types": ["Observation", "Replication", "Advisory", "commons-bundle/v0alpha1"]}}, + {"repository": "mncs-language", "commit": "26cd7f015cb857abe3f0601780de096e04dea7b4", "public_contract": {"version": "semantic identity boundary", "types": ["opaque semantic identity", "HIR identity"]}}, + {"repository": "RAVEL", "commit": "4b7c3c5503ec6bd11a7ffb96cbb32599cd1f342c", "public_contract": {"version": "ravel-development-record/0.6-preregistration", "types": ["candidate", "development record"], "promotion_authorized": false}} + ], + "authority": "diagnostic-only", + "limitations": ["Pinned shapes are compatibility evidence, not live conformance claims."] +} diff --git a/compat/ravel-development-record-0.6.json b/compat/ravel-development-record-0.6.json new file mode 100644 index 0000000..d020d78 --- /dev/null +++ b/compat/ravel-development-record-0.6.json @@ -0,0 +1,7 @@ +{ + "schema": "ravel-development-record/0.6-preregistration", + "candidate": {"candidate_id": "ravel-0.6-candidate-001", "parent_identity": "sha256:ravel-parent-fixture"}, + "authority": {"promotion_authorized": false}, + "formal_status": {"selection": "UNKNOWN", "promotion": "UNKNOWN"}, + "limitations": ["pinned MNEL compatibility fixture"] +} diff --git a/crates/mnel-provider-classical/Cargo.toml b/crates/mnel-provider-classical/Cargo.toml index 5c58084..7facd6f 100644 --- a/crates/mnel-provider-classical/Cargo.toml +++ b/crates/mnel-provider-classical/Cargo.toml @@ -10,6 +10,9 @@ description = "Deterministic Rust classical provider baseline for MNEL" [dependencies] mnel-provider-api = { path = "../mnel-provider-api" } mnel-provider-sdk = { path = "../mnel-provider-sdk" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" [lints] workspace = true diff --git a/crates/mnel-provider-classical/src/lib.rs b/crates/mnel-provider-classical/src/lib.rs index 437b3ee..92520ef 100644 --- a/crates/mnel-provider-classical/src/lib.rs +++ b/crates/mnel-provider-classical/src/lib.rs @@ -6,6 +6,10 @@ use mnel_provider_api::OUTPUT_ANOMALY_SCORE; use mnel_provider_sdk::{DiagnosticResult, Invocation, LearnedProvider, ProviderError}; +use serde::Deserialize; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; const STATE_COUNT: usize = 4; const TRANSITION_PROBABILITIES: [[f64; STATE_COUNT]; STATE_COUNT] = [ @@ -24,6 +28,228 @@ impl HiddenMarkovProvider { } } +/// The portable artifact projection emitted by MNEL's Python transition-frequency +/// reference provider. This parser is intentionally narrower than a general model +/// runtime and has no evaluator or promotion fields. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransitionFrequencyArtifact { + pub provider_id: String, + pub training_dataset_identity: String, + pub training_record_ids: Vec, + pub feature_extractor_identity: String, + pub training_code_identity: String, + pub calibration_identity: String, + pub calibration_dataset_identity: Option, + pub transition_counts: BTreeMap, + pub total_count: u64, + pub model_identity: String, + pub artifact_identity: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TransitionDiagnostic { + pub score_millionths: u64, + pub abstained: bool, + pub out_of_distribution: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ArtifactError(pub String); + +impl core::fmt::Display for ArtifactError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str(&self.0) + } +} + +impl std::error::Error for ArtifactError {} + +#[derive(Deserialize)] +struct RawTransitionArtifact { + schema: String, + provider_id: String, + training_dataset_identity: String, + training_record_ids: Vec, + feature_extractor_identity: String, + training_code_identity: String, + calibration_identity: String, + #[serde(default)] + calibration_dataset_identity: Option, + transition_counts: BTreeMap, + total_count: u64, + authority: String, + semantics: String, + model_identity: String, + artifact_identity: String, +} + +const ARTIFACT_SCHEMA: &str = "mnel-learned-provider-artifact/0.4"; +const PROVIDER_ID: &str = "mnel-reference-transition-frequency/0.4"; +const AUTHORITY: &str = "diagnostic-only"; +const SEMANTICS: &str = "learned-provider-artifact; diagnostic-only; not-a-verdict"; + +impl TransitionFrequencyArtifact { + pub fn from_json(bytes: &[u8]) -> Result { + if bytes.len() > 256 * 1024 || bytes.is_empty() { + return Err(ArtifactError( + "artifact is empty or exceeds 256 KiB".to_owned(), + )); + } + let value: Value = serde_json::from_slice(bytes) + .map_err(|error| ArtifactError(format!("artifact JSON is malformed: {error}")))?; + let raw: RawTransitionArtifact = serde_json::from_value(value.clone()) + .map_err(|error| ArtifactError(format!("artifact fields are malformed: {error}")))?; + if raw.schema != ARTIFACT_SCHEMA || raw.provider_id != PROVIDER_ID { + return Err(ArtifactError( + "unsupported transition artifact family".to_owned(), + )); + } + if raw.authority != AUTHORITY || raw.semantics != SEMANTICS { + return Err(ArtifactError( + "artifact authority or semantics are invalid".to_owned(), + )); + } + if raw.training_record_ids.is_empty() || raw.total_count == 0 || raw.total_count > 1_000_000 + { + return Err(ArtifactError( + "artifact training counts are outside bounds".to_owned(), + )); + } + if raw.transition_counts.is_empty() + || raw.transition_counts.values().any(|count| *count == 0) + || raw + .transition_counts + .values() + .try_fold(0_u64, |total, count| total.checked_add(*count)) + != Some(raw.total_count) + || raw + .transition_counts + .keys() + .any(|key| key.len() != 64 || !key.bytes().all(|byte| byte.is_ascii_hexdigit())) + { + return Err(ArtifactError("transition counts are malformed".to_owned())); + } + let supplied_artifact = raw.artifact_identity.clone(); + let mut artifact_value = value.clone(); + let object = artifact_value + .as_object_mut() + .ok_or_else(|| ArtifactError("artifact must be a JSON object".to_owned()))?; + object.remove("artifact_identity"); + if sha256_identity(&canonical_json(&artifact_value)) != supplied_artifact { + return Err(ArtifactError( + "artifact identity does not verify".to_owned(), + )); + } + let mut model_value = value; + let model_object = model_value + .as_object_mut() + .ok_or_else(|| ArtifactError("artifact must be a JSON object".to_owned()))?; + model_object.remove("artifact_identity"); + model_object.remove("model_identity"); + if sha256_identity(&canonical_json(&model_value)) != raw.model_identity { + return Err(ArtifactError("model identity does not verify".to_owned())); + } + Ok(Self { + provider_id: raw.provider_id, + training_dataset_identity: raw.training_dataset_identity, + training_record_ids: raw.training_record_ids, + feature_extractor_identity: raw.feature_extractor_identity, + training_code_identity: raw.training_code_identity, + calibration_identity: raw.calibration_identity, + calibration_dataset_identity: raw.calibration_dataset_identity, + transition_counts: raw.transition_counts, + total_count: raw.total_count, + model_identity: raw.model_identity, + artifact_identity: supplied_artifact, + }) + } + + /// Decode the compact MNEL-T1 transition view and reproduce the Python score. + pub fn infer_transition(&self, payload: &[u8]) -> Result { + if payload.len() < 11 || &payload[..7] != b"MNEL-T1" { + return Err(ArtifactError( + "transition snapshot header is invalid".to_owned(), + )); + } + let left_len = u16::from_be_bytes([payload[7], payload[8]]) as usize; + let right_len = u16::from_be_bytes([payload[9], payload[10]]) as usize; + if left_len == 0 || right_len == 0 || payload.len() != 11 + left_len + right_len { + return Err(ArtifactError( + "transition snapshot length is invalid".to_owned(), + )); + } + let left = &payload[11..11 + left_len]; + let right = &payload[11 + left_len..]; + let mut key_material = Vec::with_capacity(left.len() + right.len() + 1); + key_material.extend_from_slice(left); + key_material.push(0); + key_material.extend_from_slice(right); + let key = hex(&Sha256::digest(key_material)); + let count = self.transition_counts.get(&key).copied().unwrap_or(0); + if count == 0 { + return Ok(TransitionDiagnostic { + score_millionths: 0, + abstained: true, + out_of_distribution: true, + }); + } + Ok(TransitionDiagnostic { + score_millionths: count.saturating_mul(1_000_000) / self.total_count, + abstained: false, + out_of_distribution: false, + }) + } +} + +fn sha256_identity(value: &[u8]) -> String { + format!("sha256:{}", hex(&Sha256::digest(value))) +} + +fn hex(bytes: &[u8]) -> String { + const DIGITS: &[u8; 16] = b"0123456789abcdef"; + let mut result = String::with_capacity(bytes.len() * 2); + for byte in bytes { + result.push(DIGITS[usize::from(byte >> 4)] as char); + result.push(DIGITS[usize::from(byte & 0x0f)] as char); + } + result +} + +fn canonical_json(value: &Value) -> Vec { + match value { + Value::Null => b"null".to_vec(), + Value::Bool(value) => value.to_string().into_bytes(), + Value::Number(value) => value.to_string().into_bytes(), + Value::String(value) => serde_json::to_vec(value).unwrap_or_default(), + Value::Array(values) => { + let mut output = Vec::from(b"[".as_slice()); + for (index, item) in values.iter().enumerate() { + if index > 0 { + output.push(b','); + } + output.extend(canonical_json(item)); + } + output.push(b']'); + output + } + Value::Object(values) => { + let mut entries: Vec<(&String, &Value)> = values.iter().collect(); + entries.sort_by(|left, right| left.0.cmp(right.0)); + let mut output = Vec::from(b"{".as_slice()); + for (index, (key, item)) in entries.into_iter().enumerate() { + if index > 0 { + output.push(b','); + } + output.extend(serde_json::to_vec(key).unwrap_or_default()); + output.push(b':'); + output.extend(canonical_json(item)); + } + output.push(b'}'); + output + } + } +} + impl LearnedProvider for HiddenMarkovProvider { fn infer(&self, invocation: &Invocation<'_>) -> Result { let snapshot = invocation @@ -134,4 +360,100 @@ mod tests { }; assert_eq!(ood_error, ProviderError::OutOfDistribution); } + + #[test] + fn parses_python_artifact_and_reproduces_transition_inference() { + let mut base = serde_json::json!({ + "authority": AUTHORITY, + "calibration_identity": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "feature_extractor_identity": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "provider_id": PROVIDER_ID, + "schema": ARTIFACT_SCHEMA, + "semantics": SEMANTICS, + "total_count": 2, + "training_code_identity": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "training_dataset_identity": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "training_record_ids": ["sha256:7777777777777777777777777777777777777777777777777777777777777777"], + "transition_counts": {}, + }); + let mut key_material = b"cold".to_vec(); + key_material.push(0); + key_material.extend_from_slice(b"warm"); + let key = hex(&Sha256::digest(key_material)); + match base.as_object_mut() { + Some(object) => { + object.insert("transition_counts".to_owned(), serde_json::json!({key: 2})); + } + None => panic!("artifact fixture must be an object"), + } + let model_identity = sha256_identity(&canonical_json(&base)); + match base.as_object_mut() { + Some(object) => { + object.insert("model_identity".to_owned(), Value::String(model_identity)); + } + None => panic!("artifact fixture must be an object"), + } + let artifact_identity = sha256_identity(&canonical_json(&base)); + match base.as_object_mut() { + Some(object) => { + object.insert( + "artifact_identity".to_owned(), + Value::String(artifact_identity), + ); + } + None => panic!("artifact fixture must be an object"), + } + let artifact = match TransitionFrequencyArtifact::from_json(&canonical_json(&base)) { + Ok(artifact) => artifact, + Err(error) => panic!("artifact fixture should parse: {error}"), + }; + let mut payload = b"MNEL-T1".to_vec(); + payload.extend_from_slice(&(4_u16).to_be_bytes()); + payload.extend_from_slice(&(4_u16).to_be_bytes()); + payload.extend_from_slice(b"coldwarm"); + let result = match artifact.infer_transition(&payload) { + Ok(result) => result, + Err(error) => panic!("transition fixture should infer: {error}"), + }; + assert_eq!(result.score_millionths, 1_000_000); + assert!(!result.abstained); + assert!(!result.out_of_distribution); + let unknown = match artifact.infer_transition(b"MNEL-T1\0\x04\0\x04coldcool") { + Ok(result) => result, + Err(error) => panic!("unknown transition should be bounded: {error}"), + }; + assert!(unknown.abstained); + assert!(unknown.out_of_distribution); + } + + #[test] + fn rejects_artifact_identity_and_snapshot_length_drift() { + let error = match TransitionFrequencyArtifact::from_json( + b"{\"schema\":\"mnel-learned-provider-artifact/0.4\"}", + ) { + Ok(_) => panic!("incomplete artifact must be rejected"), + Err(error) => error, + }; + assert!(error.0.contains("malformed")); + } + + #[test] + fn parses_the_checked_in_python_artifact_fixture() { + let artifact = match TransitionFrequencyArtifact::from_json(include_bytes!( + "../tests/fixtures/transition-frequency-artifact.json" + )) { + Ok(artifact) => artifact, + Err(error) => panic!("checked-in artifact should parse: {error}"), + }; + let mut payload = b"MNEL-T1".to_vec(); + payload.extend_from_slice(&(4_u16).to_be_bytes()); + payload.extend_from_slice(&(4_u16).to_be_bytes()); + payload.extend_from_slice(b"coldwarm"); + let result = match artifact.infer_transition(&payload) { + Ok(result) => result, + Err(error) => panic!("checked-in artifact should infer: {error}"), + }; + assert_eq!(result.score_millionths, 1_000_000); + assert!(!result.abstained); + } } diff --git a/crates/mnel-provider-classical/tests/fixtures/transition-frequency-artifact.json b/crates/mnel-provider-classical/tests/fixtures/transition-frequency-artifact.json new file mode 100644 index 0000000..6f61e03 --- /dev/null +++ b/crates/mnel-provider-classical/tests/fixtures/transition-frequency-artifact.json @@ -0,0 +1 @@ +{"artifact_identity":"sha256:3ddf97d1780aabf06675fd32e9121d065125d91b6481daff51ec7f97264b9a28","authority":"diagnostic-only","calibration_dataset_identity":"sha256:6666666666666666666666666666666666666666666666666666666666666666","calibration_identity":"sha256:5555555555555555555555555555555555555555555555555555555555555555","feature_extractor_identity":"sha256:3333333333333333333333333333333333333333333333333333333333333333","model_identity":"sha256:b66dfc3589b2228103342ccc6dca0473309b1acf124e7e1a376819657b8c19b0","provider_id":"mnel-reference-transition-frequency/0.4","schema":"mnel-learned-provider-artifact/0.4","semantics":"learned-provider-artifact; diagnostic-only; not-a-verdict","total_count":1,"training_code_identity":"sha256:4444444444444444444444444444444444444444444444444444444444444444","training_dataset_identity":"sha256:1111111111111111111111111111111111111111111111111111111111111111","training_record_ids":["sha256:2222222222222222222222222222222222222222222222222222222222222222"],"transition_counts":{"e5293ff8acec8f419d53aa7bcc6790606614c6b0fde10faeef5272330c47a1ea":1}} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 933eda1..a76a193 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -167,6 +167,20 @@ visibility, budget, lineage, stopping, and rollback invariants. It is not an opt A learned router may eventually rank already-compatible providers, but it cannot expand capability, authority, disclosure, partition access, or cost ceilings. +### MNCS-family integration plane + +Forge is the preferred external control/evidence plane. The `mnel.forge_provider` module +implements only Provider Protocol 0.1 capabilities and bounded analysis responses. The +family reference path binds an MNEL study and provider artifact to a Fabric manifest, calls +the public `FabricService`, consumes the typed experimental execution receipt, and stores a +companion MNEL observation. Forge controls the declared workflow, Fabric records execution, +and the receipt preserves its own unasserted claim boundary; none of these records is an +MNEL evaluator verdict. + +Pinned compatibility snapshots for Forge, Fabric, the Complexity Standard, Commons, Language, +and RAVEL are shape evidence with exact source commits, not vendored implementations or +conformance claims. Commons output is inert interchange. RAVEL output is proposal context. + ## Concurrency Parallel workers must never mutate a shared active candidate in place. Each job binds: diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md index e9eabe2..7f7a0f5 100644 --- a/docs/INTEGRATIONS.md +++ b/docs/INTEGRATIONS.md @@ -33,9 +33,16 @@ verifier declarations, bounded snapshot views, preconditions, witnesses, registe mutations, independent comparison, health, and coverage. It is an adapter/test surface, not a substitute Forge implementation and does not claim MNCS/MNCDS conformance. -The external `mncs-forge-mcp` checkout is optional. The MNEL adapter contract is -identity-bound and provider-neutral; no developer-local Forge path is a runtime -dependency, and no hidden network or model service is invoked by the reference study. +MNEL also exposes a narrow external Forge Provider Protocol 0.1 adapter in +`mnel.forge_provider`. Its capabilities, bounded analysis requests, and one-line responses +are diagnostic-only; malformed, hidden-partition, and authority-expanding requests fail +closed. The project-scoped `mncs-forge.toml` declares the adapter without making Forge a +mandatory MNEL installation dependency. The current sibling Forge checkout can validate, +inspect, probe, and run the declared workflows when invoked with its source on `PYTHONPATH`. + +The family integration reference path records a pinned compatibility snapshot and reports +whether live sibling checkouts are available. A compatibility snapshot is not a conformance +claim and does not silently accept protocol drift. The 0.4 distillation study consumes the same identified diagnostic plane but remains an MNEL-side research harness. Its groups, strategies, retrieval results, learned-provider @@ -50,6 +57,17 @@ Fabric distributes identified experiment bundles, captures node capabilities, an reconciles observations. MNEL owns experiment semantics; Fabric owns bounded execution records. A Fabric `PASS` does not become an MNEL causal claim or formal MNCS result. +`mnel family-integration-reference` uses only Fabric's public `FabricService`: it binds a +provider-study identity and provider-artifact reference into a content-addressed local +manifest, executes a bounded local job, consumes the typed experimental receipt, and +normalizes the observation into the MNEL ledger. Repeated execution is labelled +`local-in-process-replication`; it is not multi-host independence, authenticated worker +enrollment, or protected custody. + +The same path emits an inert Commons Observation-shaped record and a RAVEL 0.6 proposal +context fixture. Neither is published or granted trust-domain, evaluator, freeze, selection, +or promotion authority. + ## RAVEL MNEL may submit a principle, strategy, expert, routing-policy, replay-policy, or other diff --git a/docs/LEARNED_PROVIDER_RUNTIME.md b/docs/LEARNED_PROVIDER_RUNTIME.md index 600864f..b2b11c2 100644 --- a/docs/LEARNED_PROVIDER_RUNTIME.md +++ b/docs/LEARNED_PROVIDER_RUNTIME.md @@ -209,3 +209,15 @@ artifact and does not establish a general language preference. Each stage must preserve the current diagnostic authority boundary and may terminate in `UNKNOWN` rather than silently widening capability. +## Portable transition artifact bridge + +The Python transition-frequency reference provider emits a deterministic +`mnel-learned-provider-artifact/0.4` JSON artifact. `mnel-provider-classical` now contains a +narrow Rust parser and reference inference surface for that artifact. It verifies the artifact +and model identities, authority/semantics fields, bounded transition counts, compact `MNEL-T1` +payload lengths, known-transition scores, and OOD abstention. A checked-in fixture is loaded by +both the Python and Rust tests. + +This is not yet native ABI execution of a Python-trained artifact. ABI v1 has no portable +model-artifact initialization request, and MNEL does not use ambient paths or global model +state to bridge that gap. The native export item therefore remains Started. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ed07eaa..5f720af 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -75,6 +75,9 @@ - **Implemented:** provider candidate/admission, transfer-pending, quarantine, retirement, and rollback records with explicit evidence checklists; - **Started:** broader provider portfolios and native export of Python-trained artifacts; +- **Implemented:** a bounded Rust parser/reference inference surface for the existing + transition-frequency artifact, with checked-in Python/Rust identity and score-equivalence + fixtures; ABI v1 host initialization remains open; - export Python-trained providers into the versioned native runtime boundary; - **Implemented:** compare the reference providers against seeded-random and explicit heuristic controls; the Rust HMM remains a separate not-applicable native baseline for @@ -94,12 +97,13 @@ ## 0.5 — MNCS Fabric execution +- **Started:** content-addressed local experiment material binding to Fabric manifests; +- **Started:** capability-aware local dispatch through the public `FabricService` boundary; +- **Started:** typed MNCS execution-receipt collection and MNEL normalization; +- **Started:** local duplicate/replay and same-node reconciliation evidence; - authenticated worker enrollment; -- content-addressed experiment bundles; -- capability-aware dispatch; - replicated and sharded trial matrices; -- node-loss, stale-result, duplicate, and replay handling; -- deterministic reconciliation and scaling measurements; +- remote node-loss handling, protected custody, and scaling measurements; - heterogeneous learned-provider placement by snapshot locality and node capability; - ABI compatibility and provider-artifact admission across Fabric nodes. diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index b967c6b..309adc6 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -96,6 +96,17 @@ native export remains outside this iteration rather than weakening the ABI bound Multiple local machines run the same operator-controlled stack. This is replication, not independent evaluation or protected custody. +### MNCS-family boundaries + +The family integration layer pins public sibling shapes and fails closed on unsupported +protocol drift. A sibling checkout, CLI, or binary is not trusted merely because it is +available: live commit identity is reported, provider identity and artifact identities are +bound, and external records remain opaque observations. Fabric duplicate/replay behavior is +preserved rather than converted into success. Receipt `claim_boundary` fields remain +unasserted. Commons records are inert and unpublished; RAVEL material is proposal context +only. Forge configuration and local sibling checkouts remain operator-controlled inputs and +must be reviewed for substitution or drift. + ## Current residual risks The foundation now validates and loads identified native provider libraries through a diff --git a/mncs-forge.toml b/mncs-forge.toml new file mode 100644 index 0000000..bd62a33 --- /dev/null +++ b/mncs-forge.toml @@ -0,0 +1,81 @@ +version = 1 +environment_allowlist = ["PATH", "LANG", "LC_ALL"] + +[project] +name = "MNEL family integration" +identity = "mnel-family-integration-v1" +root = "." + +[paths] +candidates = ["src"] +generated = ["build"] +contracts = ["docs"] +references = ["docs"] +evaluators = ["tests"] +acceptance_policies = ["README.md"] +development_evidence = ["build"] +protected = [] +outputs = ["build"] + +[limits] +timeout_seconds = 30 +output_bytes = 131072 + +[verifier_limits] +max_batch = 4 +request_bytes = 65536 +batch_timeout_seconds = 30 +witness_bytes = 16384 +stderr_bytes = 4096 +result_bytes = 131072 +max_changed_paths = 32 +max_dependency_identities = 32 +max_question_parameters = 16 + +[authority.development] +may_write_candidates = true +may_write_generated = true +may_run_providers = true + +[authority.evaluator] +candidate_read_only = true +authority_read_only = true +require_frozen_identities = true +withhold_repair_feedback = true + +[policies] +selection = "README.md" +useful_benefit_objective = "README.md" + +[[providers]] +id = "mnel-family-provider" +name = "MNEL family diagnostic provider" +identity = "mnel-family-provider-protocol-v1" +version = "0.1" +command = ["python", "-m", "mnel.forge_provider"] +transport = "stdio-jsonl" +required = false +capabilities = ["evidence_derivation", "mncs_bundle_validation", "provider_study_summary"] +supported_constructs = ["identified-study-summary", "provider-artifact-binding", "execution-receipt-binding"] +unsupported_constructs = ["hidden-transfer-content", "future-final-content", "evaluator-verdict", "ravel-promotion"] +limitations = ["diagnostic-only provider; no external evaluator authority"] + +[[workflows]] +name = "mnel-family-compatibility" +category = "mncs_bundle_validation" +mode = "development" +command = ["python", "-m", "mnel.forge_provider"] +provider_protocol = true +provider_id = "mnel-family-provider" +subject = "project" +disclosure = "compact" + +[[workflows]] +name = "mnel-provider-study-summary" +category = "evidence_derivation" +mode = "development" +command = ["python", "-m", "mnel.forge_provider"] +provider_protocol = true +provider_id = "mnel-family-provider" +subject = "candidate" +disclosure = "compact" diff --git a/schemas/mnel-family-integration.schema.json b/schemas/mnel-family-integration.schema.json new file mode 100644 index 0000000..c1dd876 --- /dev/null +++ b/schemas/mnel-family-integration.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://mnel.dev/schemas/mnel-family-integration.schema.json", + "type": "object", + "additionalProperties": false, + "required": ["schema", "study_identity", "identity_binding", "compatibility", "forge", "fabric", "availability", "authority", "semantics"], + "properties": { + "schema": {"const": "mnel-family-integration-report/0.1"}, + "study_identity": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "identity_binding": {"type": "object"}, + "compatibility": {"type": "object"}, + "forge": {"type": "object"}, + "fabric": {"type": "object"}, + "commons": {}, + "ravel": {}, + "language": {"type": "object"}, + "availability": {"type": "object"}, + "limitations": {"type": "array", "items": {"type": "string"}}, + "authority": {"const": "diagnostic-only"}, + "semantics": {"const": "family integration evidence; not-a-verdict"}, + "report_identity": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "ledger": {"type": "object"} + } +} diff --git a/src/mnel/cli.py b/src/mnel/cli.py index 0bd6d9c..cd3fe66 100644 --- a/src/mnel/cli.py +++ b/src/mnel/cli.py @@ -14,6 +14,7 @@ from .distillation import run_reference_distill_study from .forge_lifecycle import run_reference_forge_study from .provider_study import run_reference_portfolio_study +from .family_integration import run_reference_family_integration from .investigators import DEFAULT_ROLE_CONTRACTS from .learned_providers import ( DEFAULT_LEARNED_PROVIDER_REGISTRY, @@ -73,6 +74,10 @@ def parser() -> argparse.ArgumentParser: "provider-study-reference", description="Run the deterministic heterogeneous provider study" ) provider_study_reference.add_argument("--workspace", default=None) + family_integration_reference = commands.add_parser( + "family-integration-reference", description="Run the bounded MNCS-family integration study" + ) + family_integration_reference.add_argument("--workspace", default=None) return root @@ -85,7 +90,7 @@ def main(argv: list[str] | None = None) -> int: "python_supported": sys.version_info >= (3, 11), "workspace": str(Path(args.workspace).resolve()), "commands": {"elh": shutil.which("elh"), "mncs-fabric": shutil.which("mncs-fabric")}, - "forge": "provider adapter; no local executable assumed", + "forge": "MNCS Provider Protocol 0.1 adapter; external Forge remains optional", } print(json.dumps(result, indent=2, sort_keys=True)) return 0 if result["python_supported"] else 1 @@ -146,5 +151,8 @@ def main(argv: list[str] | None = None) -> int: if args.command == "provider-study-reference": print(json.dumps(run_reference_portfolio_study(args.workspace), indent=2, sort_keys=True)) return 0 + if args.command == "family-integration-reference": + print(json.dumps(run_reference_family_integration(args.workspace), indent=2, sort_keys=True)) + return 0 print(json.dumps(run_reference_study(args.workspace), indent=2, sort_keys=True)) return 0 diff --git a/src/mnel/family_integration.py b/src/mnel/family_integration.py new file mode 100644 index 0000000..0fa2882 --- /dev/null +++ b/src/mnel/family_integration.py @@ -0,0 +1,548 @@ +"""Bounded MNEL adapters for the current MNCS-family public boundaries. + +The adapters preserve external identities as opaque references. They are not a +Forge, Fabric, Commons, RAVEL, or MNCS implementation and never upgrade an +execution observation into a correctness or promotion decision. +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Any, Mapping + +from .core import EvidenceLedger, canonical_digest, canonical_json +from .forge_provider import handle_request +from .provider_study import run_reference_portfolio_study + +COMPAT_SCHEMA = "mnel-family-compatibility/0.1" +INTEGRATION_SCHEMA = "mnel-family-integration-report/0.1" +NORMALIZED_SCHEMA = "mnel-family-execution-evidence/0.1" +PINNED_COMMITS = { + "mncs-forge-mcp": "7710ea606bd592e0be95957c96132e8732fbb955", + "mncs-fabric": "fd6a1e1fe617b77402a3d40c278776dd8f159fb0", + "machine-native-complexity-standard": "1f0a1f903379d740eeeb59fe8689aed398bfb47c", + "MNCS-Commons": "5afff351efd3563febb7b3149d7de711d3342a44", + "mncs-language": "26cd7f015cb857abe3f0601780de096e04dea7b4", + "RAVEL": "4b7c3c5503ec6bd11a7ffb96cbb32599cd1f342c", +} +EXPECTED_CONTRACT_VERSIONS = { + "mncs-forge-mcp": "Provider Protocol 0.1", + "mncs-fabric": "mncs-fabric public service 0.1", + "machine-native-complexity-standard": "execution receipt 0.1-experimental", + "MNCS-Commons": "commons.mncs.dev/v0alpha1", + "mncs-language": "semantic identity boundary", + "RAVEL": "ravel-development-record/0.6-preregistration", +} +FORBIDDEN = {"verdict", "conformance", "promotion", "promotion_authorized", "evaluator_authority"} + + +class FamilyIntegrationError(ValueError): + pass + + +def _reject_authority(value: Any) -> None: + if isinstance(value, dict): + for key, child in value.items(): + inert = child is False or child is None or child in ("not-asserted", "UNKNOWN", "unknown") if isinstance(child, (str, bool)) or child is None else False + if str(key).lower() in FORBIDDEN and not inert: + raise FamilyIntegrationError(f"authority-expanding field: {key}") + _reject_authority(child) + elif isinstance(value, list): + for child in value: + _reject_authority(child) + + +def _sha(value: Any, label: str) -> str: + if not isinstance(value, str) or not value.startswith("sha256:") or len(value) != 71: + raise FamilyIntegrationError(f"{label} must be a sha256 identity") + return value + + +@dataclass(frozen=True, slots=True) +class FamilyIdentityBinding: + mnel_study_identity: str + mnel_experiment_identity: str + provider_artifact_identity: str + snapshot_identity: str | None = None + forge_request_identity: str | None = None + forge_result_identity: str | None = None + fabric_manifest_identity: str | None = None + fabric_job_identity: str | None = None + fabric_record_identity: str | None = None + mncs_receipt_identity: str | None = None + commons_record_identity: str | None = None + ravel_candidate_identity: str | None = None + language_semantic_identity: str | None = None + + def __post_init__(self) -> None: + for name in ( + "mnel_study_identity", + "mnel_experiment_identity", + "provider_artifact_identity", + ): + _sha(getattr(self, name), name) + for name in ( + "snapshot_identity", + "forge_request_identity", + "forge_result_identity", + "fabric_manifest_identity", + "fabric_job_identity", + "fabric_record_identity", + "mncs_receipt_identity", + "commons_record_identity", + "ravel_candidate_identity", + "language_semantic_identity", + ): + value = getattr(self, name) + if value is not None and not value.strip(): + raise FamilyIntegrationError(f"{name} cannot be empty") + + @property + def binding_identity(self) -> str: + return canonical_digest(self.to_dict(include_identity=False)) + + def to_dict(self, *, include_identity: bool = True) -> dict[str, Any]: + value = { + "schema": "mnel-family-identity-binding/0.1", + "mnel_study_identity": self.mnel_study_identity, + "mnel_experiment_identity": self.mnel_experiment_identity, + "provider_artifact_identity": self.provider_artifact_identity, + "optional_external_identities": { + key: getattr(self, key) + for key in ( + "snapshot_identity", + "forge_request_identity", + "forge_result_identity", + "fabric_manifest_identity", + "fabric_job_identity", + "fabric_record_identity", + "mncs_receipt_identity", + "commons_record_identity", + "ravel_candidate_identity", + "language_semantic_identity", + ) + if getattr(self, key) is not None + }, + "authority": "diagnostic-only", + "semantics": "cross-family identity binding; not-a-verdict", + } + if include_identity: + value["binding_identity"] = self.binding_identity + return value + + +def _git_head(root: Path) -> str | None: + try: + result = subprocess.run( + ["git", "-C", str(root), "rev-parse", "HEAD"], + capture_output=True, + text=True, + timeout=5, + check=False, + shell=False, + ) + except (OSError, subprocess.TimeoutExpired): + return None + return result.stdout.strip() if result.returncode == 0 else None + + +def _live_forge_probe(forge_root: Path) -> dict[str, Any]: + """Use the sibling Forge CLI in a temporary project state, never MNEL state.""" + + config_source = Path(__file__).parents[2] / "mncs-forge.toml" + if not (forge_root / "src" / "mncs_forge").is_dir() or not config_source.is_file(): + return {"status": "unavailable", "reason": "sibling Forge source or config is unavailable"} + try: + with tempfile.TemporaryDirectory(prefix="mnel-forge-probe-") as directory: + root = Path(directory) + (root / "README.md").write_text("temporary Forge control state\n", encoding="utf-8") + config = root / "mncs-forge.toml" + config.write_text(config_source.read_text(encoding="utf-8"), encoding="utf-8") + environment = dict(os.environ) + environment["PYTHONPATH"] = str(forge_root / "src") + os.pathsep + environment.get("PYTHONPATH", "") + command = [sys.executable, "-m", "mncs_forge.cli", "--config", str(config), "providers", "probe", "mnel-family-provider"] + completed = subprocess.run(command, cwd=root, env=environment, capture_output=True, text=True, timeout=20, check=False, shell=False) + if len(completed.stdout.encode()) > 128 * 1024 or len(completed.stderr.encode()) > 16 * 1024: + return {"status": "UNKNOWN", "reason": "Forge probe output exceeded adapter ceiling"} + try: + parsed = json.loads(completed.stdout) + except json.JSONDecodeError: + parsed = {"status": "UNKNOWN", "reason": "Forge probe returned malformed JSON"} + return {"status": "available" if completed.returncode == 0 and parsed.get("status") == "PASS" else "UNKNOWN", "returncode": completed.returncode, "result": parsed, "stderr": completed.stderr[:1024]} + except (OSError, subprocess.TimeoutExpired) as error: + return {"status": "unavailable", "reason": str(error)} + + +def validate_compatibility_fixture(path: str | Path, sibling_roots: Mapping[str, str | Path] | None = None) -> dict[str, Any]: + """Validate a pinned shape snapshot and, when supplied, its local checkout heads.""" + + try: + fixture = json.loads(Path(path).read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise FamilyIntegrationError(f"cannot read compatibility fixture: {error}") from error + if fixture.get("schema") != COMPAT_SCHEMA: + raise FamilyIntegrationError("unsupported family compatibility snapshot") + projects = fixture.get("projects") + if not isinstance(projects, list) or {item.get("repository") for item in projects} != set(PINNED_COMMITS): + raise FamilyIntegrationError("compatibility snapshot project set drifted") + results: list[dict[str, Any]] = [] + for item in projects: + repository = item.get("repository") + if item.get("commit") != PINNED_COMMITS[repository]: + raise FamilyIntegrationError(f"pinned commit drift for {repository}") + if not isinstance(item.get("public_contract"), dict) or item["public_contract"].get("version") != EXPECTED_CONTRACT_VERSIONS[repository]: + raise FamilyIntegrationError(f"public contract is incomplete for {repository}") + live = None + status = "fixture-only" + if sibling_roots and repository in sibling_roots: + root = Path(sibling_roots[repository]).resolve() + live = _git_head(root) + status = "live-exact" if live == item["commit"] else "live-drift" if live else "unavailable" + results.append({"repository": repository, "pinned_commit": item["commit"], "live_commit": live, "status": status, "public_contract": item["public_contract"]}) + return { + "schema": COMPAT_SCHEMA, + "fixture_identity": canonical_digest(fixture), + "status": "PASS" if all(item["status"] in {"fixture-only", "live-exact"} for item in results) else "UNKNOWN", + "projects": results, + "limitations": ["compatibility snapshots pin public shapes; they do not prove live implementation behavior"], + "authority": "diagnostic-only", + } + + +class FamilyEvidenceAdapter: + """Normalize Fabric observations without changing their claim boundary.""" + + @staticmethod + def normalize_fabric_execution( + record: Mapping[str, Any], + binding: FamilyIdentityBinding, + receipt: Mapping[str, Any] | None = None, + observed_provider_artifact_identity: str | None = None, + ) -> dict[str, Any]: + if record.get("schema_version") != "mncs-fabric.execution-record.v0.1": + raise FamilyIntegrationError("unsupported Fabric execution-record version") + record_identity = _sha(record.get("record_id"), "Fabric record_id") + if binding.fabric_record_identity and binding.fabric_record_identity != record_identity: + raise FamilyIntegrationError("Fabric record identity does not match binding") + if binding.fabric_manifest_identity and record.get("artifact_manifest_identity") != binding.fabric_manifest_identity: + raise FamilyIntegrationError("Fabric manifest identity does not match binding") + if record.get("candidate_identity") != binding.mnel_study_identity: + raise FamilyIntegrationError("Fabric candidate identity does not match MNEL study") + if observed_provider_artifact_identity is not None and observed_provider_artifact_identity != binding.provider_artifact_identity: + raise FamilyIntegrationError("observed provider artifact does not match binding") + if receipt is not None: + if receipt.get("schema_version") != "0.1-experimental" or receipt.get("record_type") != "mncs-execution-receipt": + raise FamilyIntegrationError("unsupported MNCS receipt version") + boundary = receipt.get("claim_boundary") + if not isinstance(boundary, dict) or any( + value not in {False, "not-asserted", "UNKNOWN", "unknown", None} + for value in boundary.values() + ): + raise FamilyIntegrationError("receipt claim boundary expanded authority") + value = { + "schema": NORMALIZED_SCHEMA, + "normalized_identity": canonical_digest({"record": record, "receipt": receipt}), + "external": {"project": "mncs-fabric", "protocol": record["schema_version"], "record_identity": record_identity}, + "binding": binding.to_dict(), + "execution_observation": dict(record), + "receipt_observation": dict(receipt) if receipt is not None else None, + "claim_boundary": "execution-observation-only", + "limitations": [ + "process completion and receipt status do not establish provider correctness or conformance", + "local/in-process replication is not multi-host independence or protected custody", + ], + "authority": "diagnostic-only", + "semantics": "normalized external execution evidence; not-a-verdict", + } + return value + + +class ReplayGuard: + """Reject conflicting reuse of one external record identity.""" + + def __init__(self) -> None: + self._records: dict[str, str] = {} + + def accept(self, record: Mapping[str, Any]) -> str: + identity = str(record.get("record_id", "")) + if not identity: + raise FamilyIntegrationError("external record has no identity") + digest = canonical_digest(dict(record)) + prior = self._records.get(identity) + if prior is not None and prior != digest: + raise FamilyIntegrationError("conflicting replay for external record") + self._records[identity] = digest + return identity + + +class CommonsInterchangeAdapter: + """Create an inert Commons Observation-shaped record; never publish it.""" + + @staticmethod + def observation(normalized: Mapping[str, Any]) -> dict[str, Any]: + external = normalized.get("external", {}) + record_id = str(external.get("record_identity", canonical_digest(normalized))) + value = { + "apiVersion": "commons.mncs.dev/v0alpha1", + "kind": "Observation", + "metadata": { + "recordId": "mnel-observation-" + record_id.removeprefix("sha256:")[:32], + "createdAt": "1970-01-01T00:00:00Z", + "author": {"type": "system", "id": "mnel-family-adapter"}, + "labels": ["mnel", "diagnostic-only", "fabric-execution"], + }, + "subject": {"type": "mnel-family-execution", "identity": record_id}, + "scope": {"context": {"type": "local-execution-observation"}, "limitations": ["diagnostic-only"]}, + "statement": {"summary": "Fabric execution was observed through the public local boundary."}, + "evidence": [{"id": record_id, "status": "UNKNOWN"}], + "dependencies": [], + "affectedContracts": [], + "provenance": {"producer": {"type": "adapter", "id": "mnel-family-adapter"}}, + "confidence": {"level": "unreported", "rationale": "execution observation is not a correctness claim"}, + "security": {"sensitivity": "public", "executableAttachments": False, "instructionsAreUntrusted": True}, + "lifecycle": {"initialState": "proposed", "reviewWhen": []}, + "relationships": [], + "details": { + "outcome": "UNKNOWN", + "trustDomain": "mnel-local", + "claimBoundary": "execution-observation-only", + }, + "extensions": {"mnel": {"normalized_identity": normalized.get("normalized_identity")}}, + } + _reject_authority(value) + return value + + +def _commons_bundle_smoke(observation: Mapping[str, Any], workspace: Path) -> dict[str, Any]: + """Exercise the installed Commons application boundary without publishing.""" + + try: + from mncs_commons.application.services import CommonsApplication + from mncs_commons.store import CommonsStore + except ImportError as error: + return {"status": "unavailable", "reason": str(error)} + try: + source = CommonsStore(workspace / "commons-store") + source.init() + record = CommonsApplication(source).add(observation) + bundle_path = workspace / "commons-bundle.zip" + created = CommonsApplication(source).create_bundle(bundle_path, roots=[record.digest]) + verified = CommonsApplication.verify_bundle(bundle_path) + target = CommonsStore(workspace / "commons-imported") + target.init() + imported = CommonsApplication.import_bundle(bundle_path, target) + return {"status": "available", "record_identity": record.digest, "create": created, "verify": verified, "import": imported, "trust": "inert-local-interchange"} + except (OSError, ValueError, RuntimeError) as error: + return {"status": "UNKNOWN", "reason": str(error)[:512]} + + +class RavelProposalAdapter: + @staticmethod + def proposal(binding: FamilyIdentityBinding, fixture: Mapping[str, Any]) -> dict[str, Any]: + if fixture.get("schema") != "ravel-development-record/0.6-preregistration": + raise FamilyIntegrationError("unsupported RAVEL development-record fixture") + authority = fixture.get("authority", {}) + if authority.get("promotion_authorized") is not False: + raise FamilyIntegrationError("RAVEL fixture unexpectedly grants promotion") + candidate = fixture.get("candidate", {}) + candidate_id = candidate.get("candidate_id") + if not isinstance(candidate_id, str) or not candidate_id: + raise FamilyIntegrationError("RAVEL candidate identity is missing") + value = { + "schema": "mnel-ravel-proposal-context/0.6", + "ravel_candidate_identity": candidate_id, + "parent_identity": candidate.get("parent_identity"), + "source_evidence_identities": [binding.mnel_study_identity, binding.provider_artifact_identity], + "declared_scope": "candidate-context-only", + "predicted_effects": [], + "rollback_target": None, + "authority": "proposal-only", + "semantics": "RAVEL proposal context; no freeze, selection, evaluation, or promotion", + } + _reject_authority(value) + value["proposal_identity"] = canonical_digest(value) + return value + + +def _run_fabric(binding: FamilyIdentityBinding, workspace: Path) -> dict[str, Any]: + try: + from mncs_fabric.artifacts import build_manifest + from mncs_fabric.receipts import build_execution_assurance, build_execution_receipt + from mncs_fabric.service import FabricService + except ImportError as error: + return {"availability": "unavailable", "reason": str(error)} + bundle = workspace / "fabric-bundle" + bundle.mkdir(parents=True, exist_ok=True) + (bundle / "result.json").write_text("{}\n", encoding="utf-8") + (bundle / "provider-artifact.json").write_text( + canonical_json( + { + "schema": "mnel-provider-artifact-reference/0.1", + "provider_artifact_identity": binding.provider_artifact_identity, + "study_identity": binding.mnel_study_identity, + "authority": "diagnostic-only", + } + ).decode("utf-8") + + "\n", + encoding="utf-8", + ) + task = ( + "import json\n" + f"json.dump({{'study_identity': {binding.mnel_study_identity!r}, 'provider_artifact_identity': {binding.provider_artifact_identity!r}}}, open('result.json', 'w', encoding='utf-8'), sort_keys=True)\n" + ) + (bundle / "task.py").write_text(task, encoding="utf-8") + manifest = build_manifest(bundle) + plan = { + "schema_version": "mncs-fabric.job-plan.v0.1", + "job_id": "mnel-family-study", + "candidate_identity": binding.mnel_study_identity, + "artifact_manifest_identity": manifest["manifest_identity"], + "argv": ["@python", "task.py"], + "working_directory": ".", + "timeout_seconds": 10, + "output_limit_bytes": 16 * 1024, + "environment": {"PYTHONHASHSEED": "0"}, + "required_capabilities": ["python"], + "result_paths": ["result.json"], + "network_policy": "DECLARED_OFFLINE", + } + service = FabricService() + service.validate_plan(plan) + record = service.execute_local(plan, bundle, manifest, "mnel-local", results_dir=workspace / "fabric-results", work_root=workspace) + service.verify_record(record) + receipt = build_execution_receipt(record, subject_family="MNEL", subject_kind="provider-study") + assurance = build_execution_assurance(receipt) + observed = json.loads((workspace / "fabric-results" / "result.json").read_text(encoding="utf-8")) if record.get("outcome") == "PASS" else {} + if observed and observed.get("provider_artifact_identity") != binding.provider_artifact_identity: + raise FamilyIntegrationError("Fabric result provider artifact identity mismatch") + fabric_binding = replace( + binding, + fabric_manifest_identity=manifest["manifest_identity"], + fabric_job_identity=record.get("job_identity"), + fabric_record_identity=record.get("record_id"), + mncs_receipt_identity=receipt.get("receipt_identity"), + ) + normalized = FamilyEvidenceAdapter.normalize_fabric_execution(record, fabric_binding, receipt) + duplicate = service.execute_local(plan, bundle, manifest, "mnel-local", results_dir=workspace / "fabric-results-duplicate", work_root=workspace) + replication = service.reconcile([record, duplicate], require_distinct_nodes=False) + bad_capability = dict(plan) + bad_capability["job_id"] = "mnel-family-study-capability-mismatch" + bad_capability["required_capabilities"] = ["capability-that-is-not-present"] + capability_record = service.execute_local(bad_capability, bundle, manifest, "mnel-local", work_root=workspace) + wrong_manifest = dict(plan) + wrong_manifest["job_id"] = "mnel-family-study-manifest-mismatch" + wrong_manifest["artifact_manifest_identity"] = "sha256:" + "f" * 64 + manifest_record = service.execute_local(wrong_manifest, bundle, manifest, "mnel-local", work_root=workspace) + malformed_plan = dict(plan) + malformed_plan["job_id"] = "mnel-family-study-plan-invalid" + malformed_plan["schema_version"] = "mncs-fabric.job-plan.v0.2" + malformed_record = service.execute_local(malformed_plan, bundle, manifest, "mnel-local", work_root=workspace) + corrupted = dict(record) + corrupted["record_id"] = "sha256:" + "0" * 64 + corrupted_verification = service.verify_record(corrupted) + return { + "availability": "available", + "manifest": manifest, + "plan": plan, + "execution_record": record, + "receipt": receipt, + "assurance": assurance, + "normalized": normalized, + "duplicate_record": duplicate, + "replication": {**replication, "scope": "local-in-process-replication", "limitations": ["same-node repetition is not independent multi-host evidence"]}, + "negative_cases": { + "capability_mismatch": {"outcome": capability_record.get("outcome"), "reason": capability_record.get("termination_reason")}, + "wrong_manifest": {"outcome": manifest_record.get("outcome"), "reason": manifest_record.get("termination_reason")}, + "unsupported_plan_version": {"outcome": malformed_record.get("outcome"), "reason": malformed_record.get("termination_reason")}, + "corrupt_record_identity": corrupted_verification, + }, + "provider_artifact_identity": binding.provider_artifact_identity, + } + + +def run_reference_family_integration(workspace: str | Path | None = None) -> dict[str, Any]: + """Run the dependency-aware local family integration reference study.""" + + root = Path(workspace).resolve() if workspace is not None else Path(tempfile.mkdtemp(prefix="mnel-family-")) + root.mkdir(parents=True, exist_ok=True) + portfolio = run_reference_portfolio_study() + report = portfolio["report"] + provider_identity = report["provider_artifact_identities"][0] + binding = FamilyIdentityBinding(report["study_identity"], canonical_digest({"study": report["study_identity"]}), provider_identity) + fixture_path = Path(__file__).parents[2] / "compat" / "mncs-family-compatibility-0.1.json" + sibling_roots = {} + sibling_parent = Path(__file__).resolve().parents[2].parent + for name in PINNED_COMMITS: + candidate = sibling_parent / name + if not candidate.is_dir(): + candidate = root.parent / name + if candidate.is_dir(): + sibling_roots[name] = candidate + compatibility = validate_compatibility_fixture(fixture_path, sibling_roots) + forge_live = _live_forge_probe(sibling_roots["mncs-forge-mcp"]) if "mncs-forge-mcp" in sibling_roots else {"status": "unavailable", "reason": "sibling Forge checkout not discovered"} + forge_request = { + "protocol_version": "0.1", + "type": "analysis_request", + "request_id": "sha256:" + "1" * 64, + "analysis": "provider_study_summary", + "component": {"candidate_identity": report["study_identity"], "identities": {"provider_artifact": provider_identity}}, + "limits": {"timeout_seconds": 5, "output_bytes": 16 * 1024}, + } + forge_response = handle_request(forge_request) + fabric = _run_fabric(binding, root) + commons = None + ravel = None + try: + commons = CommonsInterchangeAdapter.observation(fabric["normalized"]) if fabric.get("normalized") else {"status": "unavailable", "reason": "Fabric evidence unavailable"} + except KeyError: + commons = {"status": "unavailable", "reason": "Fabric evidence unavailable"} + commons_bundle = _commons_bundle_smoke(commons, root) if commons.get("kind") == "Observation" else {"status": "unavailable", "reason": "Commons observation was not produced"} + ravel_path = Path(__file__).parents[2] / "compat" / "ravel-development-record-0.6.json" + try: + ravel = RavelProposalAdapter.proposal(binding, json.loads(ravel_path.read_text(encoding="utf-8"))) + except (OSError, json.JSONDecodeError, FamilyIntegrationError) as error: + ravel = {"status": "fixture-unavailable", "reason": str(error)} + language = {"status": "fixture-only", "identity": "opaque-semantic-identity-fixture"} + result = { + "schema": INTEGRATION_SCHEMA, + "study_identity": report["study_identity"], + "identity_binding": binding.to_dict(), + "compatibility": compatibility, + "forge": {"protocol": "0.1", "provider_identity": "mnel-family-provider-protocol-v1", "response": forge_response, "live_probe": forge_live, "status": "live-forge-probe" if forge_live["status"] == "available" else "local-provider-boundary"}, + "fabric": fabric, + "commons": commons, + "commons_bundle": commons_bundle, + "ravel": ravel, + "language": language, + "availability": {"forge_live": forge_live["status"] == "available", "fabric_live": fabric.get("availability") == "available", "commons_fixture": commons is not None, "ravel_fixture": isinstance(ravel, dict), "language_fixture": True}, + "limitations": [ + "Forge is exercised through the protocol adapter here; the sibling executable is optional and not a runtime dependency", + "Fabric evidence is local-process execution only; no remote worker independence or protected custody is claimed", + "Commons output is inert and not published or accepted into a trust domain", + "RAVEL output is proposal context only", + ], + "authority": "diagnostic-only", + "semantics": "family integration evidence; not-a-verdict", + } + _reject_authority(result) + result["report_identity"] = canonical_digest(result) + if workspace is not None: + ledger = EvidenceLedger(root / "family-integration-evidence.jsonl") + ledger.append("family-compatibility-assessment", compatibility, actor="mnel-family-adapter") + ledger.append("forge-provider-response", forge_response, actor="mnel-family-adapter") + if fabric.get("execution_record"): + ledger.append("fabric-execution-record", fabric["execution_record"], actor="mnel-family-adapter") + ledger.append("mncs-execution-receipt", fabric["receipt"], actor="mnel-family-adapter") + ledger.append("mnel-normalized-execution-evidence", fabric["normalized"], actor="mnel-family-adapter") + ledger.append("commons-inert-observation", commons if isinstance(commons, dict) else {"status": "unavailable"}, actor="mnel-family-adapter") + ledger.append("commons-bundle-smoke", commons_bundle, actor="mnel-family-adapter") + ledger.append("ravel-proposal-context", ravel if isinstance(ravel, dict) else {"status": "unavailable"}, actor="mnel-family-adapter") + ledger.append("family-integration-report", result, actor="mnel-family-adapter") + result["ledger"] = ledger.summarize() + return result diff --git a/src/mnel/forge_provider.py b/src/mnel/forge_provider.py new file mode 100644 index 0000000..72e6deb --- /dev/null +++ b/src/mnel/forge_provider.py @@ -0,0 +1,219 @@ +"""MNCS Forge Provider Protocol 0.1 adapter for bounded MNEL diagnostics. + +This module is intentionally a narrow protocol boundary. It exposes summaries of +identified MNEL material; it does not expose arbitrary Python execution, hidden +partitions, evaluator results, or promotion operations. +""" + +from __future__ import annotations + +import json +import sys +from typing import Any + +from .core import canonical_digest, canonical_json + +PROTOCOL_VERSION = "0.1" +PROVIDER_ID = "mnel-family-provider" +PROVIDER_IDENTITY = "mnel-family-provider-protocol-v1" +PROVIDER_VERSION = "0.1" +MAX_REQUEST_BYTES = 64 * 1024 +MAX_RESPONSE_BYTES = 128 * 1024 +ANALYSES = ( + "evidence_derivation", + "mncs_bundle_validation", + "provider_study_summary", +) +FORBIDDEN_KEYS = frozenset( + { + "verdict", + "evaluator_verdict", + "evaluator_authority", + "conformance", + "mncs_conformance", + "mncds_conformance", + "promotion", + "promotion_authorized", + "ravel_promotion", + "hidden_transfer", + "future_final", + "future-final", + "hidden-transfer", + } +) + + +class ForgeProviderError(ValueError): + """A malformed or authority-expanding Provider Protocol request.""" + + +def _reject_forbidden(value: Any) -> None: + if isinstance(value, dict): + for key, child in value.items(): + if str(key).lower() in FORBIDDEN_KEYS: + raise ForgeProviderError(f"forbidden authority field: {key}") + _reject_forbidden(child) + elif isinstance(value, list): + for child in value: + _reject_forbidden(child) + + +def _provider() -> dict[str, str]: + return {"id": PROVIDER_ID, "identity": PROVIDER_IDENTITY, "version": PROVIDER_VERSION} + + +def capabilities(request_id: str | None = None) -> dict[str, Any]: + value: dict[str, Any] = { + "protocol_version": PROTOCOL_VERSION, + "type": "capabilities", + "provider": _provider(), + "analyses": list(ANALYSES), + "statuses": ["PASS", "FAIL", "UNKNOWN"], + "cancellation": False, + "health_checks": True, + "extensions": { + "supported_constructs": [ + "identified-study-summary", + "provider-artifact-binding", + "execution-receipt-binding", + ], + "unsupported_constructs": [ + "hidden-transfer-content", + "future-final-content", + "evaluator-verdict", + "ravel-promotion", + ], + "limitations": [ + "diagnostic-only summaries; no external authority is created", + "material must be supplied as bounded identities, not hidden records", + ], + }, + } + if request_id is not None: + value["request_id"] = request_id + return value + + +def _identity(value: Any, label: str) -> str: + if not isinstance(value, str) or not value.startswith("sha256:") or len(value) != 71: + raise ForgeProviderError(f"{label} must be a sha256 identity") + return value + + +def _analysis_response(request: dict[str, Any], status: str, summary: str, *, limitations: list[str]) -> dict[str, Any]: + response = { + "protocol_version": PROTOCOL_VERSION, + "type": "analysis_response", + "request_id": request.get("request_id"), + "provider": _provider(), + "status": status, + "summary": summary[:1024], + "witnesses_or_counterexamples": [], + "limitations": limitations[:16], + "extensions": { + "mnel": { + "authority": "diagnostic-only", + "semantics": "diagnostic-only; not-a-verdict", + "response_identity": canonical_digest( + {"request": request.get("request_id"), "status": status, "summary": summary} + ), + } + }, + } + return response + + +def handle_request(request: Any) -> dict[str, Any]: + """Validate and handle exactly one decoded Protocol 0.1 request.""" + + if not isinstance(request, dict): + raise ForgeProviderError("request must be an object") + _reject_forbidden(request) + if request.get("protocol_version") != PROTOCOL_VERSION: + raise ForgeProviderError("unsupported protocol version") + request_type = request.get("type") + if request_type == "capabilities": + request_id = request.get("request_id") + if request_id is not None and (not isinstance(request_id, str) or not request_id.strip()): + raise ForgeProviderError("request_id must be a non-empty string") + return capabilities(request_id) + if request_type != "analysis_request": + raise ForgeProviderError("request type must be capabilities or analysis_request") + request_id = request.get("request_id") + if not isinstance(request_id, str) or not request_id.strip(): + raise ForgeProviderError("analysis request_id is required") + analysis = request.get("analysis") + if analysis not in ANALYSES: + return _analysis_response( + request, + "UNKNOWN", + "requested analysis is unsupported", + limitations=["unsupported-analysis"], + ) + component = request.get("component", {}) + if not isinstance(component, dict): + raise ForgeProviderError("component must be an object") + for key in ("candidate_identity", "source_epoch"): + if key in component and not isinstance(component[key], (str, int)): + raise ForgeProviderError(f"component.{key} must be scalar") + identities = component.get("identities", {}) + if identities and not isinstance(identities, dict): + raise ForgeProviderError("component.identities must be an object") + for key, value in identities.items(): + if not isinstance(value, str) or not value.strip() or len(value) > 256: + raise ForgeProviderError(f"component.identities.{key} must be a bounded identity") + if any(token in str(key).lower() for token in ("snapshot", "artifact", "study")): + _identity(value, f"component.identities.{key}") + limits = request.get("limits", {}) + if not isinstance(limits, dict): + raise ForgeProviderError("limits must be an object") + if limits.get("output_bytes", MAX_RESPONSE_BYTES) > MAX_RESPONSE_BYTES: + raise ForgeProviderError("requested output limit exceeds provider ceiling") + return _analysis_response( + request, + "UNKNOWN", + f"bounded diagnostic analysis accepted: {analysis}", + limitations=["external evaluator and promotion authority remain outside MNEL"], + ) + + +def _error_response(message: str) -> dict[str, Any]: + return { + "protocol_version": PROTOCOL_VERSION, + "type": "error", + "provider": _provider(), + "code": "MNEL_PROVIDER_PROTOCOL_ERROR", + "message": message[:512], + "extensions": {}, + } + + +def process_line(raw: bytes) -> bytes: + """Process one complete input buffer, preserving Forge's one-line framing.""" + + if len(raw) > MAX_REQUEST_BYTES: + return canonical_json(_error_response("request exceeds byte ceiling")) + b"\n" + try: + text = raw.decode("utf-8") + lines = text.splitlines() + if len(lines) != 1 or not lines[0].strip() or text.endswith("\n\n"): + raise ForgeProviderError("request must contain exactly one JSON line") + request = json.loads(lines[0]) + response = handle_request(request) + except (UnicodeDecodeError, json.JSONDecodeError, ForgeProviderError) as error: + response = _error_response(str(error)) + encoded = canonical_json(response) + if len(encoded) > MAX_RESPONSE_BYTES: + encoded = canonical_json(_error_response("response exceeds byte ceiling")) + return encoded + b"\n" + + +def main() -> int: + raw = sys.stdin.buffer.read(MAX_REQUEST_BYTES + 1) + sys.stdout.buffer.write(process_line(raw)) + sys.stdout.buffer.flush() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_family_integration.py b/tests/test_family_integration.py new file mode 100644 index 0000000..89dd5b5 --- /dev/null +++ b/tests/test_family_integration.py @@ -0,0 +1,121 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from mnel.family_integration import ( + FamilyIdentityBinding, + FamilyIntegrationError, + FamilyEvidenceAdapter, + ReplayGuard, + RavelProposalAdapter, + CommonsInterchangeAdapter, + run_reference_family_integration, + validate_compatibility_fixture, +) +from mnel.forge_provider import handle_request, process_line +from mnel.forge_provider import ForgeProviderError +from mnel.reference_provider import TransitionFrequencyModel + + +class ForgeProviderProtocolTests(unittest.TestCase): + def test_capabilities_and_analysis_are_protocol_compatible(self): + capabilities = handle_request({"protocol_version": "0.1", "type": "capabilities", "request_id": "r"}) + self.assertEqual(capabilities["type"], "capabilities") + self.assertEqual(capabilities["provider"]["identity"], "mnel-family-provider-protocol-v1") + response = handle_request( + { + "protocol_version": "0.1", + "type": "analysis_request", + "request_id": "r", + "analysis": "provider_study_summary", + "component": {"candidate_identity": "forge-tree-sha256-v1:opaque", "identities": {"study": "sha256:" + "1" * 64}}, + "limits": {"output_bytes": 4096}, + } + ) + self.assertEqual(response["status"], "UNKNOWN") + self.assertEqual(response["extensions"]["mnel"]["authority"], "diagnostic-only") + + def test_protocol_rejects_framing_authority_and_bad_identity(self): + with self.assertRaises(ForgeProviderError): + handle_request({"protocol_version": "0.1", "type": "analysis_request", "request_id": "x", "analysis": "provider_study_summary", "promotion": True}) + with self.assertRaises(ForgeProviderError): + handle_request({"protocol_version": "0.1", "type": "analysis_request", "request_id": "x", "analysis": "provider_study_summary", "component": {"identities": {"snapshot": "bad"}}}) + response = json.loads(process_line(b"{}\n{}\n")) + self.assertEqual(response["type"], "error") + self.assertIn("extensions", response) + + def test_unsupported_analysis_is_unknown(self): + response = handle_request({"protocol_version": "0.1", "type": "analysis_request", "request_id": "x", "analysis": "not-supported"}) + self.assertEqual(response["status"], "UNKNOWN") + + +class FamilyCompatibilityTests(unittest.TestCase): + def test_pinned_fixture_and_drift_fail_closed(self): + path = Path(__file__).parents[1] / "compat" / "mncs-family-compatibility-0.1.json" + result = validate_compatibility_fixture(path) + self.assertEqual(result["status"], "PASS") + value = json.loads(path.read_text(encoding="utf-8")) + value["projects"][0]["public_contract"]["version"] = "Provider Protocol 0.2" + with tempfile.TemporaryDirectory() as directory: + mutated = Path(directory) / "compat.json" + mutated.write_text(json.dumps(value), encoding="utf-8") + with self.assertRaises(FamilyIntegrationError): + validate_compatibility_fixture(mutated) + + def test_replay_guard_rejects_conflict(self): + guard = ReplayGuard() + record = {"record_id": "sha256:" + "a" * 64, "value": 1} + self.assertEqual(guard.accept(record), record["record_id"]) + self.assertEqual(guard.accept(dict(record)), record["record_id"]) + with self.assertRaises(FamilyIntegrationError): + guard.accept({"record_id": record["record_id"], "value": 2}) + + +class FamilyReferenceStudyTests(unittest.TestCase): + def test_checked_in_native_artifact_fixture_reloads_in_python(self): + path = Path(__file__).parents[1] / "crates" / "mnel-provider-classical" / "tests" / "fixtures" / "transition-frequency-artifact.json" + model = TransitionFrequencyModel.load(path.read_bytes()) + self.assertEqual(model.artifact_identity, "sha256:3ddf97d1780aabf06675fd32e9121d065125d91b6481daff51ec7f97264b9a28") + + def test_reference_study_runs_and_preserves_receipt_boundary(self): + with tempfile.TemporaryDirectory() as directory: + result = run_reference_family_integration(directory) + self.assertEqual(result["fabric"]["availability"], "available") + self.assertEqual(result["fabric"]["execution_record"]["outcome"], "PASS") + self.assertTrue(result["fabric"]["normalized"]["normalized_identity"].startswith("sha256:")) + self.assertEqual(result["fabric"]["replication"]["scope"], "local-in-process-replication") + self.assertEqual(result["fabric"]["negative_cases"]["capability_mismatch"]["reason"], "CAPABILITY_UNAVAILABLE") + self.assertEqual(result["fabric"]["negative_cases"]["wrong_manifest"]["reason"], "INTEGRITY_FAILURE") + self.assertEqual(result["fabric"]["negative_cases"]["unsupported_plan_version"]["reason"], "PLAN_INVALID") + self.assertEqual(result["fabric"]["negative_cases"]["corrupt_record_identity"]["outcome"], "FAIL") + self.assertEqual(result["commons"]["kind"], "Observation") + self.assertIn(result["commons_bundle"]["status"], {"available", "unavailable", "UNKNOWN"}) + if result["commons_bundle"]["status"] == "available": + self.assertTrue(result["commons_bundle"]["verify"]["valid"]) + self.assertEqual(result["ravel"]["authority"], "proposal-only") + self.assertTrue(result["ledger"]["valid"]) + + def test_external_identity_mismatch_is_rejected(self): + binding = FamilyIdentityBinding("sha256:" + "1" * 64, "sha256:" + "2" * 64, "sha256:" + "3" * 64) + with self.assertRaises(FamilyIntegrationError): + FamilyEvidenceAdapter.normalize_fabric_execution( + {"schema_version": "mncs-fabric.execution-record.v0.1", "record_id": "sha256:" + "4" * 64, "candidate_identity": "sha256:" + "5" * 64}, binding + ) + + def test_provider_artifact_mismatch_is_rejected(self): + binding = FamilyIdentityBinding("sha256:" + "1" * 64, "sha256:" + "2" * 64, "sha256:" + "3" * 64) + record = {"schema_version": "mncs-fabric.execution-record.v0.1", "record_id": "sha256:" + "4" * 64, "candidate_identity": binding.mnel_study_identity} + with self.assertRaises(FamilyIntegrationError): + FamilyEvidenceAdapter.normalize_fabric_execution(record, binding, observed_provider_artifact_identity="sha256:" + "5" * 64) + + def test_ravel_promotion_injection_is_rejected(self): + binding = FamilyIdentityBinding("sha256:" + "1" * 64, "sha256:" + "2" * 64, "sha256:" + "3" * 64) + with self.assertRaises(FamilyIntegrationError): + RavelProposalAdapter.proposal(binding, {"schema": "ravel-development-record/0.6-preregistration", "candidate": {"candidate_id": "c"}, "authority": {"promotion_authorized": True}}) + + def test_commons_adapter_is_inert_observation(self): + value = CommonsInterchangeAdapter.observation({"external": {"record_identity": "sha256:" + "a" * 64}, "normalized_identity": "sha256:" + "b" * 64}) + self.assertEqual(value["kind"], "Observation") + self.assertEqual(value["details"]["outcome"], "UNKNOWN") + self.assertNotIn("promotion", value)