From 1751f3fe783ab8f83df05c165c9a86548fc27a10 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:27:19 -0700 Subject: [PATCH 01/31] test(evidence): define schema-bound extraction contract --- .../tests/extraction_schema.rs | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 crates/originweave-evidence/tests/extraction_schema.rs diff --git a/crates/originweave-evidence/tests/extraction_schema.rs b/crates/originweave-evidence/tests/extraction_schema.rs new file mode 100644 index 000000000..2a769f35d --- /dev/null +++ b/crates/originweave-evidence/tests/extraction_schema.rs @@ -0,0 +1,246 @@ +use originweave_evidence::{ + ExtractionCardinality, ExtractionField, ExtractionSchema, ExtractionSchemaError, + ExtractionSourceChannel, ExtractionValueType, MAX_EXTRACTION_FIELD_COUNT, + MAX_EXTRACTION_IDENTIFIER_BYTES, +}; + +fn field( + identifier: &str, + value_type: ExtractionValueType, + cardinality: ExtractionCardinality, + required: bool, + source_channels: &[ExtractionSourceChannel], +) -> ExtractionField { + ExtractionField::new( + identifier, + value_type, + cardinality, + required, + source_channels, + ) + .expect("fixture field must be valid") +} + +#[test] +fn schema_binds_versioned_typed_fields_to_explicit_source_channels() { + let schema = ExtractionSchema::new( + "product-card-v1", + vec![ + field( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ + ExtractionSourceChannel::SemanticNode, + ExtractionSourceChannel::StructuredData, + ], + ), + field( + "unit_price", + ExtractionValueType::Decimal, + ExtractionCardinality::ZeroOrOne, + false, + &[ + ExtractionSourceChannel::TableCell, + ExtractionSourceChannel::NetworkResponse, + ], + ), + ], + ) + .expect("schema must be admitted"); + + assert_eq!(schema.version(), "product-card-v1"); + assert_eq!(schema.fields().len(), 2); + assert_eq!(schema.field("product_name").unwrap().identifier(), "product_name"); + assert_eq!( + schema.field("product_name").unwrap().value_type(), + ExtractionValueType::Text + ); + assert_eq!( + schema.field("product_name").unwrap().cardinality(), + ExtractionCardinality::One + ); + assert!(schema.field("product_name").unwrap().required()); + assert_eq!( + schema.field("product_name").unwrap().source_channels(), + &[ + ExtractionSourceChannel::SemanticNode, + ExtractionSourceChannel::StructuredData, + ] + ); + assert_eq!( + schema.field("unit_price").unwrap().value_type(), + ExtractionValueType::Decimal + ); + assert_eq!( + schema.field("unit_price").unwrap().cardinality(), + ExtractionCardinality::ZeroOrOne + ); + assert!(!schema.field("unit_price").unwrap().required()); + assert!(schema.field("missing_field").is_none()); +} + +#[test] +fn field_accepts_all_reviewed_value_and_source_channel_variants() { + let cases = [ + (ExtractionValueType::Text, ExtractionSourceChannel::SemanticNode), + (ExtractionValueType::Integer, ExtractionSourceChannel::StructuredData), + (ExtractionValueType::Decimal, ExtractionSourceChannel::TableCell), + (ExtractionValueType::Boolean, ExtractionSourceChannel::NetworkResponse), + ( + ExtractionValueType::Timestamp, + ExtractionSourceChannel::ModelInterpretation, + ), + ]; + + for (index, (value_type, source_channel)) in cases.into_iter().enumerate() { + let field = field( + &format!("field_{index}"), + value_type, + ExtractionCardinality::Many, + false, + &[source_channel], + ); + assert_eq!(field.value_type(), value_type); + assert_eq!(field.cardinality(), ExtractionCardinality::Many); + assert_eq!(field.source_channels(), &[source_channel]); + } +} + +#[test] +fn field_rejects_empty_malformed_or_overlong_identifiers() { + assert_eq!( + ExtractionField::new( + "", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + ), + Err(ExtractionSchemaError::InvalidIdentifier) + ); + assert_eq!( + ExtractionField::new( + "Product Name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + ), + Err(ExtractionSchemaError::InvalidIdentifier) + ); + assert_eq!( + ExtractionField::new( + "1product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + ), + Err(ExtractionSchemaError::InvalidIdentifier) + ); + assert_eq!( + ExtractionField::new( + &"a".repeat(MAX_EXTRACTION_IDENTIFIER_BYTES + 1), + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + ), + Err(ExtractionSchemaError::LimitExceeded) + ); +} + +#[test] +fn field_requires_a_nonempty_duplicate_free_source_channel_set() { + assert_eq!( + ExtractionField::new( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[], + ), + Err(ExtractionSchemaError::MissingSourceChannel) + ); + assert_eq!( + ExtractionField::new( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ + ExtractionSourceChannel::SemanticNode, + ExtractionSourceChannel::SemanticNode, + ], + ), + Err(ExtractionSchemaError::DuplicateSourceChannel) + ); +} + +#[test] +fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overflow() { + assert_eq!( + ExtractionSchema::new("Product Schema", vec![field( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + )]), + Err(ExtractionSchemaError::InvalidIdentifier) + ); + assert_eq!( + ExtractionSchema::new( + &"a".repeat(MAX_EXTRACTION_IDENTIFIER_BYTES + 1), + vec![field( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + )], + ), + Err(ExtractionSchemaError::LimitExceeded) + ); + assert_eq!( + ExtractionSchema::new("product-card-v1", vec![]), + Err(ExtractionSchemaError::MissingField) + ); + + let duplicate = field( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + ); + let duplicate_again = field( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::ZeroOrOne, + false, + &[ExtractionSourceChannel::StructuredData], + ); + assert_eq!( + ExtractionSchema::new("product-card-v1", vec![duplicate, duplicate_again]), + Err(ExtractionSchemaError::DuplicateField) + ); + + let too_many_fields = (0..=MAX_EXTRACTION_FIELD_COUNT) + .map(|index| { + field( + &format!("field_{index}"), + ExtractionValueType::Text, + ExtractionCardinality::ZeroOrOne, + false, + &[ExtractionSourceChannel::SemanticNode], + ) + }) + .collect(); + assert_eq!( + ExtractionSchema::new("product-card-v1", too_many_fields), + Err(ExtractionSchemaError::LimitExceeded) + ); +} From f088a84967264414893c3908cddf645d01d74056 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:30:04 -0700 Subject: [PATCH 02/31] test(evidence): canonicalize extraction schema regression formatting --- .../tests/extraction_schema.rs | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/crates/originweave-evidence/tests/extraction_schema.rs b/crates/originweave-evidence/tests/extraction_schema.rs index 2a769f35d..ed91045bf 100644 --- a/crates/originweave-evidence/tests/extraction_schema.rs +++ b/crates/originweave-evidence/tests/extraction_schema.rs @@ -52,7 +52,10 @@ fn schema_binds_versioned_typed_fields_to_explicit_source_channels() { assert_eq!(schema.version(), "product-card-v1"); assert_eq!(schema.fields().len(), 2); - assert_eq!(schema.field("product_name").unwrap().identifier(), "product_name"); + assert_eq!( + schema.field("product_name").unwrap().identifier(), + "product_name" + ); assert_eq!( schema.field("product_name").unwrap().value_type(), ExtractionValueType::Text @@ -84,10 +87,22 @@ fn schema_binds_versioned_typed_fields_to_explicit_source_channels() { #[test] fn field_accepts_all_reviewed_value_and_source_channel_variants() { let cases = [ - (ExtractionValueType::Text, ExtractionSourceChannel::SemanticNode), - (ExtractionValueType::Integer, ExtractionSourceChannel::StructuredData), - (ExtractionValueType::Decimal, ExtractionSourceChannel::TableCell), - (ExtractionValueType::Boolean, ExtractionSourceChannel::NetworkResponse), + ( + ExtractionValueType::Text, + ExtractionSourceChannel::SemanticNode, + ), + ( + ExtractionValueType::Integer, + ExtractionSourceChannel::StructuredData, + ), + ( + ExtractionValueType::Decimal, + ExtractionSourceChannel::TableCell, + ), + ( + ExtractionValueType::Boolean, + ExtractionSourceChannel::NetworkResponse, + ), ( ExtractionValueType::Timestamp, ExtractionSourceChannel::ModelInterpretation, @@ -182,13 +197,16 @@ fn field_requires_a_nonempty_duplicate_free_source_channel_set() { #[test] fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overflow() { assert_eq!( - ExtractionSchema::new("Product Schema", vec![field( - "product_name", - ExtractionValueType::Text, - ExtractionCardinality::One, - true, - &[ExtractionSourceChannel::SemanticNode], - )]), + ExtractionSchema::new( + "Product Schema", + vec![field( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + )] + ), Err(ExtractionSchemaError::InvalidIdentifier) ); assert_eq!( From 9bd08ae6157a478b44cfe2dcea0c92758285594d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:34:40 -0700 Subject: [PATCH 03/31] feat(evidence): add bounded extraction schema contracts --- .../src/extraction_schema.rs | 216 ++++++++++++++++++ crates/originweave-evidence/src/lib.rs | 6 + .../tests/extraction_schema.rs | 10 + 3 files changed, 232 insertions(+) create mode 100644 crates/originweave-evidence/src/extraction_schema.rs diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs new file mode 100644 index 000000000..cbcfde709 --- /dev/null +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -0,0 +1,216 @@ +//! Versioned schema contracts for typed evidence extraction. +//! +//! These value objects describe what may be extracted and which reviewed +//! evidence channels may support each field. They do not read browser data, +//! disclose protected values, persist artifacts, execute models, or grant any +//! browser, network, secret, approval, or storage authority. + +use std::collections::BTreeSet; + +/// Maximum encoded byte length for an extraction schema or field identifier. +pub const MAX_EXTRACTION_IDENTIFIER_BYTES: usize = 128; +/// Maximum number of fields admitted by one extraction schema. +pub const MAX_EXTRACTION_FIELD_COUNT: usize = 256; + +/// The typed value contract for one extracted field. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ExtractionValueType { + /// Bounded textual data. + Text, + /// A whole-number value. + Integer, + /// A decimal numeric value. + Decimal, + /// A boolean value. + Boolean, + /// A timestamp value whose concrete normalization is defined by the schema version. + Timestamp, +} + +/// The number of values admitted for one extracted field. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ExtractionCardinality { + /// Exactly one value is admitted. + One, + /// Zero or one value is admitted. + ZeroOrOne, + /// A bounded collection may be admitted by a later extraction runtime. + Many, +} + +/// A reviewed evidence channel that may support an extracted value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ExtractionSourceChannel { + /// A semantic browser node with an independently validated identity. + SemanticNode, + /// Embedded structured metadata such as JSON-LD, RDFa, or Microdata. + StructuredData, + /// A bounded table-cell observation. + TableCell, + /// A bounded network response whose origin and response identity are independently verified. + NetworkResponse, + /// A separately approved model interpretation backed by explicit evidence identifiers. + ModelInterpretation, +} + +/// A validation failure while constructing an extraction schema contract. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExtractionSchemaError { + /// A schema or field identifier was empty or outside the accepted identifier grammar. + InvalidIdentifier, + /// An identifier or field collection exceeded its bounded limit. + LimitExceeded, + /// A field did not declare any reviewed source channel. + MissingSourceChannel, + /// A field declared the same source channel more than once. + DuplicateSourceChannel, + /// A schema did not contain any field definitions. + MissingField, + /// A schema declared the same field identifier more than once. + DuplicateField, +} + +/// One typed field declared by a versioned extraction schema. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtractionField { + identifier: String, + value_type: ExtractionValueType, + cardinality: ExtractionCardinality, + required: bool, + source_channels: Vec, +} + +impl ExtractionField { + /// Validate and construct one extraction field contract. + pub fn new( + identifier: &str, + value_type: ExtractionValueType, + cardinality: ExtractionCardinality, + required: bool, + source_channels: &[ExtractionSourceChannel], + ) -> Result { + validate_identifier(identifier)?; + if source_channels.is_empty() { + return Err(ExtractionSchemaError::MissingSourceChannel); + } + + let mut seen_channels = BTreeSet::new(); + for source_channel in source_channels { + if !seen_channels.insert(*source_channel) { + return Err(ExtractionSchemaError::DuplicateSourceChannel); + } + } + + Ok(Self { + identifier: identifier.to_owned(), + value_type, + cardinality, + required, + source_channels: source_channels.to_vec(), + }) + } + + /// Return the stable field identifier. + #[must_use] + pub fn identifier(&self) -> &str { + &self.identifier + } + + /// Return the declared value type. + #[must_use] + pub const fn value_type(&self) -> ExtractionValueType { + self.value_type + } + + /// Return the declared cardinality. + #[must_use] + pub const fn cardinality(&self) -> ExtractionCardinality { + self.cardinality + } + + /// Return whether the field must be present in a conforming extraction result. + #[must_use] + pub const fn required(&self) -> bool { + self.required + } + + /// Return the reviewed source channels that may support this field. + #[must_use] + pub fn source_channels(&self) -> &[ExtractionSourceChannel] { + &self.source_channels + } +} + +/// A bounded versioned collection of typed extraction-field contracts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExtractionSchema { + version: String, + fields: Vec, +} + +impl ExtractionSchema { + /// Validate and construct one versioned extraction schema. + pub fn new( + version: &str, + fields: Vec, + ) -> Result { + validate_identifier(version)?; + if fields.is_empty() { + return Err(ExtractionSchemaError::MissingField); + } + if fields.len() > MAX_EXTRACTION_FIELD_COUNT { + return Err(ExtractionSchemaError::LimitExceeded); + } + + let mut field_identifiers = BTreeSet::new(); + for field in &fields { + if !field_identifiers.insert(field.identifier()) { + return Err(ExtractionSchemaError::DuplicateField); + } + } + + Ok(Self { + version: version.to_owned(), + fields, + }) + } + + /// Return the immutable schema version identifier. + #[must_use] + pub fn version(&self) -> &str { + &self.version + } + + /// Return the schema's ordered field definitions. + #[must_use] + pub fn fields(&self) -> &[ExtractionField] { + &self.fields + } + + /// Find one field by its stable identifier. + #[must_use] + pub fn field(&self, identifier: &str) -> Option<&ExtractionField> { + self.fields + .iter() + .find(|field| field.identifier() == identifier) + } +} + +fn validate_identifier(identifier: &str) -> Result<(), ExtractionSchemaError> { + if identifier.len() > MAX_EXTRACTION_IDENTIFIER_BYTES { + return Err(ExtractionSchemaError::LimitExceeded); + } + + let mut bytes = identifier.bytes(); + let Some(first_byte) = bytes.next() else { + return Err(ExtractionSchemaError::InvalidIdentifier); + }; + if !first_byte.is_ascii_lowercase() { + return Err(ExtractionSchemaError::InvalidIdentifier); + } + if bytes.any(|byte| !matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'_' | b'-')) { + return Err(ExtractionSchemaError::InvalidIdentifier); + } + + Ok(()) +} diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index ad183e9eb..c15d4ded8 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -7,8 +7,14 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] +mod extraction_schema; mod sensitive_access; +pub use extraction_schema::{ + ExtractionCardinality, ExtractionField, ExtractionSchema, ExtractionSchemaError, + ExtractionSourceChannel, ExtractionValueType, MAX_EXTRACTION_FIELD_COUNT, + MAX_EXTRACTION_IDENTIFIER_BYTES, +}; pub use sensitive_access::{ MAX_SENSITIVE_FIELD_COUNT, MAX_SENSITIVE_IDENTIFIER_BYTES, SensitiveAccessClass, SensitiveAccessEvidence, SensitiveAccessEvidenceInput, SensitiveAccessOutcome, diff --git a/crates/originweave-evidence/tests/extraction_schema.rs b/crates/originweave-evidence/tests/extraction_schema.rs index ed91045bf..2ac696611 100644 --- a/crates/originweave-evidence/tests/extraction_schema.rs +++ b/crates/originweave-evidence/tests/extraction_schema.rs @@ -145,6 +145,16 @@ fn field_rejects_empty_malformed_or_overlong_identifiers() { ), Err(ExtractionSchemaError::InvalidIdentifier) ); + assert_eq!( + ExtractionField::new( + "product name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::SemanticNode], + ), + Err(ExtractionSchemaError::InvalidIdentifier) + ); assert_eq!( ExtractionField::new( "1product_name", From a76a3bcf316a5e9737e445510553745a1441737e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:36:15 -0700 Subject: [PATCH 04/31] style(evidence): apply canonical rustfmt --- crates/originweave-evidence/src/extraction_schema.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs index cbcfde709..e4d1b302c 100644 --- a/crates/originweave-evidence/src/extraction_schema.rs +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -150,10 +150,7 @@ pub struct ExtractionSchema { impl ExtractionSchema { /// Validate and construct one versioned extraction schema. - pub fn new( - version: &str, - fields: Vec, - ) -> Result { + pub fn new(version: &str, fields: Vec) -> Result { validate_identifier(version)?; if fields.is_empty() { return Err(ExtractionSchemaError::MissingField); From 2bf2166ef52b1d0c33e0b5be30f94756599e98f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:38:53 -0700 Subject: [PATCH 05/31] test(evidence): satisfy fail-closed clippy contracts --- .../tests/extraction_schema.rs | 81 +++++++++++-------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/crates/originweave-evidence/tests/extraction_schema.rs b/crates/originweave-evidence/tests/extraction_schema.rs index 2ac696611..07377944b 100644 --- a/crates/originweave-evidence/tests/extraction_schema.rs +++ b/crates/originweave-evidence/tests/extraction_schema.rs @@ -10,7 +10,7 @@ fn field( cardinality: ExtractionCardinality, required: bool, source_channels: &[ExtractionSourceChannel], -) -> ExtractionField { +) -> Result { ExtractionField::new( identifier, value_type, @@ -18,11 +18,11 @@ fn field( required, source_channels, ) - .expect("fixture field must be valid") } #[test] -fn schema_binds_versioned_typed_fields_to_explicit_source_channels() { +fn schema_binds_versioned_typed_fields_to_explicit_source_channels( +) -> Result<(), ExtractionSchemaError> { let schema = ExtractionSchema::new( "product-card-v1", vec![ @@ -35,7 +35,7 @@ fn schema_binds_versioned_typed_fields_to_explicit_source_channels() { ExtractionSourceChannel::SemanticNode, ExtractionSourceChannel::StructuredData, ], - ), + )?, field( "unit_price", ExtractionValueType::Decimal, @@ -45,47 +45,61 @@ fn schema_binds_versioned_typed_fields_to_explicit_source_channels() { ExtractionSourceChannel::TableCell, ExtractionSourceChannel::NetworkResponse, ], - ), + )?, ], - ) - .expect("schema must be admitted"); + )?; assert_eq!(schema.version(), "product-card-v1"); assert_eq!(schema.fields().len(), 2); assert_eq!( - schema.field("product_name").unwrap().identifier(), - "product_name" + schema.field("product_name").map(ExtractionField::identifier), + Some("product_name") ); assert_eq!( - schema.field("product_name").unwrap().value_type(), - ExtractionValueType::Text + schema.field("product_name").map(ExtractionField::value_type), + Some(ExtractionValueType::Text) ); assert_eq!( - schema.field("product_name").unwrap().cardinality(), - ExtractionCardinality::One + schema + .field("product_name") + .map(ExtractionField::cardinality), + Some(ExtractionCardinality::One) ); - assert!(schema.field("product_name").unwrap().required()); assert_eq!( - schema.field("product_name").unwrap().source_channels(), - &[ - ExtractionSourceChannel::SemanticNode, - ExtractionSourceChannel::StructuredData, - ] + schema.field("product_name").map(ExtractionField::required), + Some(true) + ); + let expected_product_sources = [ + ExtractionSourceChannel::SemanticNode, + ExtractionSourceChannel::StructuredData, + ]; + assert_eq!( + schema + .field("product_name") + .map(ExtractionField::source_channels), + Some(expected_product_sources.as_slice()) + ); + assert_eq!( + schema.field("unit_price").map(ExtractionField::value_type), + Some(ExtractionValueType::Decimal) ); assert_eq!( - schema.field("unit_price").unwrap().value_type(), - ExtractionValueType::Decimal + schema + .field("unit_price") + .map(ExtractionField::cardinality), + Some(ExtractionCardinality::ZeroOrOne) ); assert_eq!( - schema.field("unit_price").unwrap().cardinality(), - ExtractionCardinality::ZeroOrOne + schema.field("unit_price").map(ExtractionField::required), + Some(false) ); - assert!(!schema.field("unit_price").unwrap().required()); assert!(schema.field("missing_field").is_none()); + Ok(()) } #[test] -fn field_accepts_all_reviewed_value_and_source_channel_variants() { +fn field_accepts_all_reviewed_value_and_source_channel_variants( +) -> Result<(), ExtractionSchemaError> { let cases = [ ( ExtractionValueType::Text, @@ -116,11 +130,12 @@ fn field_accepts_all_reviewed_value_and_source_channel_variants() { ExtractionCardinality::Many, false, &[source_channel], - ); + )?; assert_eq!(field.value_type(), value_type); assert_eq!(field.cardinality(), ExtractionCardinality::Many); assert_eq!(field.source_channels(), &[source_channel]); } + Ok(()) } #[test] @@ -205,7 +220,8 @@ fn field_requires_a_nonempty_duplicate_free_source_channel_set() { } #[test] -fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overflow() { +fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overflow( +) -> Result<(), ExtractionSchemaError> { assert_eq!( ExtractionSchema::new( "Product Schema", @@ -215,7 +231,7 @@ fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overfl ExtractionCardinality::One, true, &[ExtractionSourceChannel::SemanticNode], - )] + )?] ), Err(ExtractionSchemaError::InvalidIdentifier) ); @@ -228,7 +244,7 @@ fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overfl ExtractionCardinality::One, true, &[ExtractionSourceChannel::SemanticNode], - )], + )?], ), Err(ExtractionSchemaError::LimitExceeded) ); @@ -243,14 +259,14 @@ fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overfl ExtractionCardinality::One, true, &[ExtractionSourceChannel::SemanticNode], - ); + )?; let duplicate_again = field( "product_name", ExtractionValueType::Text, ExtractionCardinality::ZeroOrOne, false, &[ExtractionSourceChannel::StructuredData], - ); + )?; assert_eq!( ExtractionSchema::new("product-card-v1", vec![duplicate, duplicate_again]), Err(ExtractionSchemaError::DuplicateField) @@ -266,9 +282,10 @@ fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overfl &[ExtractionSourceChannel::SemanticNode], ) }) - .collect(); + .collect::, _>>()?; assert_eq!( ExtractionSchema::new("product-card-v1", too_many_fields), Err(ExtractionSchemaError::LimitExceeded) ); + Ok(()) } From f7d0c041f357169036c4af3b574da7993d27ee89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:40:17 -0700 Subject: [PATCH 06/31] style(evidence): apply canonical test formatting --- .../tests/extraction_schema.rs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/originweave-evidence/tests/extraction_schema.rs b/crates/originweave-evidence/tests/extraction_schema.rs index 07377944b..cbb89955e 100644 --- a/crates/originweave-evidence/tests/extraction_schema.rs +++ b/crates/originweave-evidence/tests/extraction_schema.rs @@ -21,8 +21,8 @@ fn field( } #[test] -fn schema_binds_versioned_typed_fields_to_explicit_source_channels( -) -> Result<(), ExtractionSchemaError> { +fn schema_binds_versioned_typed_fields_to_explicit_source_channels() +-> Result<(), ExtractionSchemaError> { let schema = ExtractionSchema::new( "product-card-v1", vec![ @@ -52,11 +52,15 @@ fn schema_binds_versioned_typed_fields_to_explicit_source_channels( assert_eq!(schema.version(), "product-card-v1"); assert_eq!(schema.fields().len(), 2); assert_eq!( - schema.field("product_name").map(ExtractionField::identifier), + schema + .field("product_name") + .map(ExtractionField::identifier), Some("product_name") ); assert_eq!( - schema.field("product_name").map(ExtractionField::value_type), + schema + .field("product_name") + .map(ExtractionField::value_type), Some(ExtractionValueType::Text) ); assert_eq!( @@ -84,9 +88,7 @@ fn schema_binds_versioned_typed_fields_to_explicit_source_channels( Some(ExtractionValueType::Decimal) ); assert_eq!( - schema - .field("unit_price") - .map(ExtractionField::cardinality), + schema.field("unit_price").map(ExtractionField::cardinality), Some(ExtractionCardinality::ZeroOrOne) ); assert_eq!( @@ -98,8 +100,8 @@ fn schema_binds_versioned_typed_fields_to_explicit_source_channels( } #[test] -fn field_accepts_all_reviewed_value_and_source_channel_variants( -) -> Result<(), ExtractionSchemaError> { +fn field_accepts_all_reviewed_value_and_source_channel_variants() +-> Result<(), ExtractionSchemaError> { let cases = [ ( ExtractionValueType::Text, @@ -220,8 +222,8 @@ fn field_requires_a_nonempty_duplicate_free_source_channel_set() { } #[test] -fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overflow( -) -> Result<(), ExtractionSchemaError> { +fn schema_rejects_invalid_version_empty_fields_duplicate_fields_and_field_overflow() +-> Result<(), ExtractionSchemaError> { assert_eq!( ExtractionSchema::new( "Product Schema", From 512bc3e050f156550f86c6d06e9b6f5af461dec7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:44:26 -0700 Subject: [PATCH 07/31] docs(changelog): record extraction schema contract --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d17419927..4638b5f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. - Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves. -- Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior and `NotConfigured` revocation evidence. +- Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior. - Credential-free TLS evidence containing canonical origin, requested and observed peer, DNS/IP reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. - Credential-free connection and redirect evidence containing canonical addresses, destination classes, target digests, hop numbers, and approved-address counts. - Credential-free verified TCP evidence containing the logical origin, requested socket, observed peer, destination class, successful attempt number, and per-attempt timeout. @@ -24,6 +24,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding. - Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits. - Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state. +- Versioned schema-bound extraction contracts with bounded identifiers and field counts, typed value/cardinality metadata, explicit duplicate-free reviewed source channels, and fail-closed schema validation. - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge. - Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, and TLS service-identity ADR documentation. From d56090a24a34a6a6f369dab9c0774f9aab0fdd7d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:44:59 -0700 Subject: [PATCH 08/31] docs(changelog): preserve revocation evidence wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4638b5f62..a67578c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. - Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves. -- Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior. +- Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior, and `NotConfigured` revocation evidence. - Credential-free TLS evidence containing canonical origin, requested and observed peer, DNS/IP reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. - Credential-free connection and redirect evidence containing canonical addresses, destination classes, target digests, hop numbers, and approved-address counts. - Credential-free verified TCP evidence containing the logical origin, requested socket, observed peer, destination class, successful attempt number, and per-attempt timeout. From 8fdb1b9fff63a54a179880f3abcb1421b59837b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 14:45:38 -0700 Subject: [PATCH 09/31] docs(changelog): keep TLS policy text unchanged --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a67578c1b..8a9a59e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. - Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves. -- Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior, and `NotConfigured` revocation evidence. +- Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior and `NotConfigured` revocation evidence. - Credential-free TLS evidence containing canonical origin, requested and observed peer, DNS/IP reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. - Credential-free connection and redirect evidence containing canonical addresses, destination classes, target digests, hop numbers, and approved-address counts. - Credential-free verified TCP evidence containing the logical origin, requested socket, observed peer, destination class, successful attempt number, and per-attempt timeout. From b336a361e4b1631b9f748da133b512707686e1fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 16:19:31 -0700 Subject: [PATCH 10/31] test(evidence): require explicit extraction normalization --- .../tests/extraction_normalization.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 crates/originweave-evidence/tests/extraction_normalization.rs diff --git a/crates/originweave-evidence/tests/extraction_normalization.rs b/crates/originweave-evidence/tests/extraction_normalization.rs new file mode 100644 index 000000000..112995b65 --- /dev/null +++ b/crates/originweave-evidence/tests/extraction_normalization.rs @@ -0,0 +1,78 @@ +use originweave_evidence::{ + ExtractionCardinality, ExtractionField, ExtractionNormalizationRule, ExtractionSchemaError, + ExtractionSourceChannel, ExtractionValueType, +}; + +#[test] +fn extraction_fields_require_an_explicit_typed_normalization_rule() +-> Result<(), ExtractionSchemaError> { + let text = ExtractionField::new_with_normalization( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + ExtractionNormalizationRule::TrimTextWhitespace, + &[ExtractionSourceChannel::SemanticNode], + )?; + assert_eq!( + text.normalization_rule(), + ExtractionNormalizationRule::TrimTextWhitespace + ); + + let timestamp = ExtractionField::new_with_normalization( + "captured_at", + ExtractionValueType::Timestamp, + ExtractionCardinality::One, + true, + ExtractionNormalizationRule::Rfc3339Utc, + &[ExtractionSourceChannel::NetworkResponse], + )?; + assert_eq!( + timestamp.normalization_rule(), + ExtractionNormalizationRule::Rfc3339Utc + ); + Ok(()) +} + +#[test] +fn extraction_fields_fail_closed_on_type_incompatible_normalization() { + assert_eq!( + ExtractionField::new_with_normalization( + "captured_at", + ExtractionValueType::Timestamp, + ExtractionCardinality::One, + true, + ExtractionNormalizationRule::TrimTextWhitespace, + &[ExtractionSourceChannel::NetworkResponse], + ), + Err(ExtractionSchemaError::InvalidNormalizationRule) + ); + assert_eq!( + ExtractionField::new_with_normalization( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + ExtractionNormalizationRule::Rfc3339Utc, + &[ExtractionSourceChannel::SemanticNode], + ), + Err(ExtractionSchemaError::InvalidNormalizationRule) + ); +} + +#[test] +fn existing_fields_default_to_verbatim_normalization() +-> Result<(), ExtractionSchemaError> { + let field = ExtractionField::new( + "unit_price", + ExtractionValueType::Decimal, + ExtractionCardinality::ZeroOrOne, + false, + &[ExtractionSourceChannel::StructuredData], + )?; + assert_eq!( + field.normalization_rule(), + ExtractionNormalizationRule::Verbatim + ); + Ok(()) +} From 0325ad36175fee828f7552aa03c740464fcb9667 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 16:24:10 -0700 Subject: [PATCH 11/31] feat(evidence): bind extraction normalization rules --- .../src/extraction_schema.rs | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs index e4d1b302c..f7ba37fc7 100644 --- a/crates/originweave-evidence/src/extraction_schema.rs +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -53,6 +53,17 @@ pub enum ExtractionSourceChannel { ModelInterpretation, } +/// A deterministic normalization rule declared for one extracted field. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ExtractionNormalizationRule { + /// Preserve the typed source value without text normalization. + Verbatim, + /// Trim surrounding whitespace from a textual value. + TrimTextWhitespace, + /// Normalize a timestamp into an RFC 3339 UTC representation. + Rfc3339Utc, +} + /// A validation failure while constructing an extraction schema contract. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExtractionSchemaError { @@ -64,6 +75,8 @@ pub enum ExtractionSchemaError { MissingSourceChannel, /// A field declared the same source channel more than once. DuplicateSourceChannel, + /// The declared normalization rule was incompatible with the field value type. + InvalidNormalizationRule, /// A schema did not contain any field definitions. MissingField, /// A schema declared the same field identifier more than once. @@ -77,23 +90,56 @@ pub struct ExtractionField { value_type: ExtractionValueType, cardinality: ExtractionCardinality, required: bool, + normalization_rule: ExtractionNormalizationRule, source_channels: Vec, } impl ExtractionField { - /// Validate and construct one extraction field contract. + /// Validate and construct one extraction field contract with verbatim normalization. pub fn new( identifier: &str, value_type: ExtractionValueType, cardinality: ExtractionCardinality, required: bool, source_channels: &[ExtractionSourceChannel], + ) -> Result { + Self::new_with_normalization( + identifier, + value_type, + cardinality, + required, + ExtractionNormalizationRule::Verbatim, + source_channels, + ) + } + + /// Validate and construct one extraction field with an explicit normalization rule. + pub fn new_with_normalization( + identifier: &str, + value_type: ExtractionValueType, + cardinality: ExtractionCardinality, + required: bool, + normalization_rule: ExtractionNormalizationRule, + source_channels: &[ExtractionSourceChannel], ) -> Result { validate_identifier(identifier)?; if source_channels.is_empty() { return Err(ExtractionSchemaError::MissingSourceChannel); } + let normalization_is_compatible = match normalization_rule { + ExtractionNormalizationRule::Verbatim => true, + ExtractionNormalizationRule::TrimTextWhitespace => { + value_type == ExtractionValueType::Text + } + ExtractionNormalizationRule::Rfc3339Utc => { + value_type == ExtractionValueType::Timestamp + } + }; + if !normalization_is_compatible { + return Err(ExtractionSchemaError::InvalidNormalizationRule); + } + let mut seen_channels = BTreeSet::new(); for source_channel in source_channels { if !seen_channels.insert(*source_channel) { @@ -106,6 +152,7 @@ impl ExtractionField { value_type, cardinality, required, + normalization_rule, source_channels: source_channels.to_vec(), }) } @@ -134,6 +181,12 @@ impl ExtractionField { self.required } + /// Return the deterministic normalization rule declared for this field. + #[must_use] + pub const fn normalization_rule(&self) -> ExtractionNormalizationRule { + self.normalization_rule + } + /// Return the reviewed source channels that may support this field. #[must_use] pub fn source_channels(&self) -> &[ExtractionSourceChannel] { From 04299d12f5caca6cbd1af127a8fa6fd8908889e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 16:24:49 -0700 Subject: [PATCH 12/31] feat(evidence): export extraction normalization contract --- crates/originweave-evidence/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index c15d4ded8..6a719685e 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -11,8 +11,8 @@ mod extraction_schema; mod sensitive_access; pub use extraction_schema::{ - ExtractionCardinality, ExtractionField, ExtractionSchema, ExtractionSchemaError, - ExtractionSourceChannel, ExtractionValueType, MAX_EXTRACTION_FIELD_COUNT, + ExtractionCardinality, ExtractionField, ExtractionNormalizationRule, ExtractionSchema, + ExtractionSchemaError, ExtractionSourceChannel, ExtractionValueType, MAX_EXTRACTION_FIELD_COUNT, MAX_EXTRACTION_IDENTIFIER_BYTES, }; pub use sensitive_access::{ From f6901af8518fb39bb66cc4a5e062006ff6b39949 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 16:25:19 -0700 Subject: [PATCH 13/31] style(evidence): apply normalization regression rustfmt --- crates/originweave-evidence/tests/extraction_normalization.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-evidence/tests/extraction_normalization.rs b/crates/originweave-evidence/tests/extraction_normalization.rs index 112995b65..63afd39e6 100644 --- a/crates/originweave-evidence/tests/extraction_normalization.rs +++ b/crates/originweave-evidence/tests/extraction_normalization.rs @@ -61,8 +61,7 @@ fn extraction_fields_fail_closed_on_type_incompatible_normalization() { } #[test] -fn existing_fields_default_to_verbatim_normalization() --> Result<(), ExtractionSchemaError> { +fn existing_fields_default_to_verbatim_normalization() -> Result<(), ExtractionSchemaError> { let field = ExtractionField::new( "unit_price", ExtractionValueType::Decimal, From a527c433c62813d1346e050ce252b309b8d96b2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 16:29:01 -0700 Subject: [PATCH 14/31] style(evidence): apply canonical normalization formatting --- crates/originweave-evidence/src/extraction_schema.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs index f7ba37fc7..d8f978e74 100644 --- a/crates/originweave-evidence/src/extraction_schema.rs +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -132,9 +132,7 @@ impl ExtractionField { ExtractionNormalizationRule::TrimTextWhitespace => { value_type == ExtractionValueType::Text } - ExtractionNormalizationRule::Rfc3339Utc => { - value_type == ExtractionValueType::Timestamp - } + ExtractionNormalizationRule::Rfc3339Utc => value_type == ExtractionValueType::Timestamp, }; if !normalization_is_compatible { return Err(ExtractionSchemaError::InvalidNormalizationRule); From 69bc738bd45a1b61a4673b122dc3eec8814baa22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 16:29:49 -0700 Subject: [PATCH 15/31] style(evidence): apply canonical extraction export formatting --- crates/originweave-evidence/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 6a719685e..05ae7c3f1 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -12,8 +12,8 @@ mod sensitive_access; pub use extraction_schema::{ ExtractionCardinality, ExtractionField, ExtractionNormalizationRule, ExtractionSchema, - ExtractionSchemaError, ExtractionSourceChannel, ExtractionValueType, MAX_EXTRACTION_FIELD_COUNT, - MAX_EXTRACTION_IDENTIFIER_BYTES, + ExtractionSchemaError, ExtractionSourceChannel, ExtractionValueType, + MAX_EXTRACTION_FIELD_COUNT, MAX_EXTRACTION_IDENTIFIER_BYTES, }; pub use sensitive_access::{ MAX_SENSITIVE_FIELD_COUNT, MAX_SENSITIVE_IDENTIFIER_BYTES, SensitiveAccessClass, From 6e526fa90d93a01b744090dcce8daf72970d01dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:04:34 -0700 Subject: [PATCH 16/31] docs(adr): bind extraction schema version semantics --- docs/adr/0106-provenance-evidence-model.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/adr/0106-provenance-evidence-model.md b/docs/adr/0106-provenance-evidence-model.md index 0e2741f37..60dbb929c 100644 --- a/docs/adr/0106-provenance-evidence-model.md +++ b/docs/adr/0106-provenance-evidence-model.md @@ -33,29 +33,47 @@ OriginWeave maintains provenance-native evidence with stable identifiers for ses WARC and PROV are interoperability/export contracts, not substitutes for OriginWeave's internal authorization or evidence schema. A WARC record can contain untrusted or sensitive payload bytes and therefore inherits capture, retention, encryption, and export policy. A PROV entity/activity/agent relation records derivation or responsibility; it cannot manufacture authentication, authorization, durable completion, or tenant ownership not established by the producing system. +### Versioned extraction-schema binding + +A versioned `ExtractionSchema` is the binding contract for typed extraction before any capture persistence or export format is allowed to claim semantic authority. Each schema version contains an ordered, non-empty set of unique `ExtractionField` definitions. Schema-version and field identifiers are bounded to 128 encoded bytes, begin with a lowercase ASCII letter, and thereafter admit only lowercase ASCII letters, digits, `_`, or `-`. One schema admits at most 256 fields. + +Every extraction field binds its stable identifier to a value type, cardinality, required/optional status, deterministic normalization rule, and a non-empty duplicate-free set of reviewed source-channel classes. `Verbatim` is the compatibility default used by the existing constructor. `TrimTextWhitespace` is admitted only for text fields and `Rfc3339Utc` only for timestamp fields; type-incompatible normalization fails closed. A `ModelInterpretation` source channel is classification metadata only and does not grant model execution, approval, disclosure, browser, network, secret, or storage authority. + +At this value-object boundary, the version identifier is immutable schema identity; there is deliberately no registry that silently treats two different field contracts as compatible merely because their version strings compare or sort in a particular way. Callers changing a field identifier, value type, cardinality, required status, normalization rule, or admitted source-channel set must use a distinct reviewed schema version and perform any migration/compatibility decision at an explicit higher layer. The current schema object does not itself read browser data, materialize extracted values, persist artifacts, execute models, or change governance policy. Those capabilities require separately authorized runtime boundaries and are not implied by schema construction. + ## Consequences Capture becomes a designed product surface rather than incidental logging. Storage and retention need budgets. Consumers can distinguish a model claim from source evidence and an action request from verified completion. Export adapters can target WARC, provenance graphs, audit streams, or buyer-specific schemas. +A schema consumer can also determine the exact field/type/cardinality/normalization/source contract it reviewed rather than relying on free-form extraction instructions. Schema evolution is explicit instead of being inferred from mutable field definitions; runtime compatibility, migrations, durable storage, and extracted-value validation remain separate implementation work until those boundaries are delivered. + ## Failure and degraded behavior If mandatory evidence cannot be recorded durably enough for a governed state-changing action, the action fails before execution or reports an explicit unverifiable failure; it is never marked proved. Read-only operations may degrade to reduced evidence only when the API contract declares that mode. Corrupt or incomplete evidence is quarantined rather than silently accepted. +Invalid or oversized extraction identifiers, empty or duplicate field sets, missing or duplicate source channels, and type-incompatible normalization rules fail during schema construction. A caller must not reinterpret such a failure as an empty/default-success schema or silently substitute another source channel. + ## Security / privacy / governance impact Evidence is tenant-scoped, selectively disclosed, encrypted as appropriate, retention-bounded, and auditable. Credential-bearing headers, cookies, secret values, and sensitive form data are excluded or transformed according to explicit schema policy. Integrity metadata and immutable artifact identities support tamper detection without claiming external certification. `docs/DATA_GOVERNANCE.md` defines the disclosure/retention boundary for protected content and derived artifacts. +The extraction-schema contract does not modify governance authority. It describes admissible typed fields and reviewed evidence-channel classes only. In particular, declaring `NetworkResponse` or `ModelInterpretation` does not authorize network access, model execution, protected-data disclosure, approvals, retention, or export; those remain governed by their existing owning boundaries. + ## Tests and acceptance evidence Require provenance-link tests, credential-leak tests, integrity/corruption tests, crash-recovery tests, WARC/export conformance where implemented, PROV relation/schema tests where implemented, retention/deletion tests, tenant-isolation tests, and end-to-end checks that state-changing actions link request, policy, approval, execution, and post-condition as separate records. Export tests must prove that disabled or unauthorized source bodies never appear merely because metadata provenance is exportable. +The extraction-schema boundary additionally requires tests for the identifier grammar and limits, field-count bound, duplicate identifiers, source-channel presence and uniqueness, every reviewed value/cardinality/source-channel variant, deterministic normalization selection, incompatible normalization rejection, and the backward-compatible `Verbatim` constructor default. + ## Migration and rollback Introduce stable evidence identifiers and schema versions before changing export formats. Migrations preserve old evidence semantics or explicitly mark unavailable fields. Rollback may revert an exporter but cannot collapse mandatory action and policy evidence into opaque logs. +Extraction contract changes that alter field identity or semantics require a new reviewed schema version rather than mutating the meaning of an existing version. Rolling back a consumer may stop accepting a newer version, but it must not reinterpret that newer contract as an older one or silently discard required fields. + ## Open follow-ups -Finalize canonical evidence schemas, content-retention defaults, signing/attestation strategy, cross-system export identifiers, and buyer-controlled disclosure policies. +Finalize canonical evidence schemas, content-retention defaults, signing/attestation strategy, cross-system export identifiers, and buyer-controlled disclosure policies. Add the runtime that validates concrete extracted values against an `ExtractionSchema`, plus explicit migration/compatibility policy when durable schema registration is introduced. ## Supersession / reversal conditions From fd34bb5e928210520715a90a4a247531890723fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:06:15 -0700 Subject: [PATCH 17/31] test(evidence): require extraction schema standard errors --- .../tests/extraction_schema_error_contract.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 crates/originweave-evidence/tests/extraction_schema_error_contract.rs diff --git a/crates/originweave-evidence/tests/extraction_schema_error_contract.rs b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs new file mode 100644 index 000000000..40cc6d79b --- /dev/null +++ b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs @@ -0,0 +1,44 @@ +use std::error::Error as _; + +use originweave_evidence::ExtractionSchemaError; + +fn assert_standard_error_contract() {} + +#[test] +fn extraction_schema_errors_implement_standard_error_contract() { + assert_standard_error_contract::(); + + for (error, message) in [ + ( + ExtractionSchemaError::InvalidIdentifier, + "invalid extraction schema identifier", + ), + ( + ExtractionSchemaError::LimitExceeded, + "extraction schema limit exceeded", + ), + ( + ExtractionSchemaError::MissingSourceChannel, + "extraction field requires at least one source channel", + ), + ( + ExtractionSchemaError::DuplicateSourceChannel, + "extraction field contains a duplicate source channel", + ), + ( + ExtractionSchemaError::InvalidNormalizationRule, + "extraction normalization rule is incompatible with the field value type", + ), + ( + ExtractionSchemaError::MissingField, + "extraction schema requires at least one field", + ), + ( + ExtractionSchemaError::DuplicateField, + "extraction schema contains a duplicate field identifier", + ), + ] { + assert_eq!(error.to_string(), message); + assert!(error.source().is_none()); + } +} From ef02ee8607b52b6a0955240f959f95d8e9b3df60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:07:58 -0700 Subject: [PATCH 18/31] fix(evidence): expose extraction schema standard errors --- .../src/extraction_schema.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs index d8f978e74..abffc4679 100644 --- a/crates/originweave-evidence/src/extraction_schema.rs +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -5,7 +5,7 @@ //! disclose protected values, persist artifacts, execute models, or grant any //! browser, network, secret, approval, or storage authority. -use std::collections::BTreeSet; +use std::{collections::BTreeSet, fmt}; /// Maximum encoded byte length for an extraction schema or field identifier. pub const MAX_EXTRACTION_IDENTIFIER_BYTES: usize = 128; @@ -83,6 +83,24 @@ pub enum ExtractionSchemaError { DuplicateField, } +impl fmt::Display for ExtractionSchemaError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::InvalidIdentifier => "invalid extraction schema identifier", + Self::LimitExceeded => "extraction schema limit exceeded", + Self::MissingSourceChannel => "extraction field requires at least one source channel", + Self::DuplicateSourceChannel => "extraction field contains a duplicate source channel", + Self::InvalidNormalizationRule => { + "extraction normalization rule is incompatible with the field value type" + } + Self::MissingField => "extraction schema requires at least one field", + Self::DuplicateField => "extraction schema contains a duplicate field identifier", + }) + } +} + +impl std::error::Error for ExtractionSchemaError {} + /// One typed field declared by a versioned extraction schema. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExtractionField { From 40988c3caaeb6a962d971e965e008bdea04c2c37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:10:59 -0700 Subject: [PATCH 19/31] docs(changelog): record extraction schema error contract --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9a59e77..0136ba881 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding. - Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits. - Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state. -- Versioned schema-bound extraction contracts with bounded identifiers and field counts, typed value/cardinality metadata, explicit duplicate-free reviewed source channels, and fail-closed schema validation. +- Versioned schema-bound extraction contracts with bounded identifiers and field counts, typed value/cardinality metadata, explicit duplicate-free reviewed source channels, fail-closed schema validation, and deterministic `Display`/`std::error::Error` contracts for public schema failures. - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge. - Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, and TLS service-identity ADR documentation. From 2fdc3802b058d53d43c5c6f0f3f1361eb1acb4b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:49:40 -0700 Subject: [PATCH 20/31] test(evidence): make identifier error contract scope-neutral --- .../tests/extraction_schema_error_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-evidence/tests/extraction_schema_error_contract.rs b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs index 40cc6d79b..1ba248a1d 100644 --- a/crates/originweave-evidence/tests/extraction_schema_error_contract.rs +++ b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs @@ -11,7 +11,7 @@ fn extraction_schema_errors_implement_standard_error_contract() { for (error, message) in [ ( ExtractionSchemaError::InvalidIdentifier, - "invalid extraction schema identifier", + "invalid extraction schema or field identifier", ), ( ExtractionSchemaError::LimitExceeded, From 4b1164c01f0b8a888c6b3dc9bc32ad65184ec171 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:49:55 -0700 Subject: [PATCH 21/31] test(evidence): require canonical source-channel set identity --- .../tests/extraction_source_channel_set.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 crates/originweave-evidence/tests/extraction_source_channel_set.rs diff --git a/crates/originweave-evidence/tests/extraction_source_channel_set.rs b/crates/originweave-evidence/tests/extraction_source_channel_set.rs new file mode 100644 index 000000000..1f5070e8a --- /dev/null +++ b/crates/originweave-evidence/tests/extraction_source_channel_set.rs @@ -0,0 +1,40 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + ExtractionCardinality, ExtractionField, ExtractionSourceChannel, ExtractionValueType, +}; + +#[test] +fn equivalent_source_channel_sets_have_canonical_identity() { + let semantic_then_network = ExtractionField::new( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ + ExtractionSourceChannel::SemanticNode, + ExtractionSourceChannel::NetworkResponse, + ], + ) + .expect("reviewed source set must be valid"); + let network_then_semantic = ExtractionField::new( + "product_name", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ + ExtractionSourceChannel::NetworkResponse, + ExtractionSourceChannel::SemanticNode, + ], + ) + .expect("equivalent reviewed source set must be valid"); + + assert_eq!(semantic_then_network, network_then_semantic); + assert_eq!( + network_then_semantic.source_channels(), + &[ + ExtractionSourceChannel::SemanticNode, + ExtractionSourceChannel::NetworkResponse, + ] + ); +} From f483af9829dd064e6d9ae17411fa2af9963d4cc2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 15:51:47 -0700 Subject: [PATCH 22/31] fix(evidence): canonicalize extraction source-channel sets --- crates/originweave-evidence/src/extraction_schema.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs index abffc4679..ca2ba4881 100644 --- a/crates/originweave-evidence/src/extraction_schema.rs +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -86,7 +86,7 @@ pub enum ExtractionSchemaError { impl fmt::Display for ExtractionSchemaError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(match self { - Self::InvalidIdentifier => "invalid extraction schema identifier", + Self::InvalidIdentifier => "invalid extraction schema or field identifier", Self::LimitExceeded => "extraction schema limit exceeded", Self::MissingSourceChannel => "extraction field requires at least one source channel", Self::DuplicateSourceChannel => "extraction field contains a duplicate source channel", @@ -169,7 +169,7 @@ impl ExtractionField { cardinality, required, normalization_rule, - source_channels: source_channels.to_vec(), + source_channels: seen_channels.into_iter().collect(), }) } From a1535d843f25f3916cdb721336a9a2dfefdfc3a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:49:26 -0700 Subject: [PATCH 23/31] docs(evidence): bind ExtractionSchema doctoring authority --- docs/doctoring.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/doctoring.md b/docs/doctoring.md index f0133bb5d..d5e733c6c 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -82,6 +82,8 @@ RFC 9309 standardizes robots parsing, matching, error handling, and caching. It W3C PROV-O supplies interoperable Entity, Activity, Agent, derivation, attribution, and responsibility concepts. ISO 28500:2017, confirmed in 2023, defines WARC storage for protocol payloads, control information, metadata, transformations, duplicate detection, integrity, and segmentation. OriginWeave uses source hashes and locators in the safety kernel, then adds WARC and PROV adapters as separately testable modules. +The versioned `ExtractionSchema` is an admission and interpretation contract for typed extracted fields: each field is bounded, declares a value type, cardinality, normalization rule, and a canonical duplicate-free set of reviewed source-channel classes. That declaration does not create browser, network, model, secret, storage, retention, disclosure, or governance authority. PROV/WARC interoperability is therefore layered after the schema contract rather than inferred from it. + ### AI risk and prompt injection NIST AI 600-1 provides generative-AI lifecycle risk guidance. WASP demonstrates that web-navigation agents can follow low-effort indirect prompt injections. OriginWeave therefore separates trusted instructions, untrusted observations, and protected secrets at type and process boundaries rather than rely on prompting alone. From 36de84d2f08b1ae2bdbe5992c941834f2240b80c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:09:05 -0700 Subject: [PATCH 24/31] test(evidence): reject contradictory field cardinality --- .../tests/extraction_schema.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/originweave-evidence/tests/extraction_schema.rs b/crates/originweave-evidence/tests/extraction_schema.rs index cbb89955e..7e932244b 100644 --- a/crates/originweave-evidence/tests/extraction_schema.rs +++ b/crates/originweave-evidence/tests/extraction_schema.rs @@ -140,6 +140,26 @@ fn field_accepts_all_reviewed_value_and_source_channel_variants() Ok(()) } +#[test] +fn field_rejects_contradictory_required_cardinality_contracts() { + assert!(ExtractionField::new( + "optional_exactly_one", + ExtractionValueType::Text, + ExtractionCardinality::One, + false, + &[ExtractionSourceChannel::SemanticNode], + ) + .is_err()); + assert!(ExtractionField::new( + "required_zero_or_one", + ExtractionValueType::Text, + ExtractionCardinality::ZeroOrOne, + true, + &[ExtractionSourceChannel::SemanticNode], + ) + .is_err()); +} + #[test] fn field_rejects_empty_malformed_or_overlong_identifiers() { assert_eq!( From c5a87a64d96b12ef0399b72cb7d0ede3bb7e33d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:10:09 -0700 Subject: [PATCH 25/31] style(evidence): format cardinality regression --- .../tests/extraction_schema.rs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/originweave-evidence/tests/extraction_schema.rs b/crates/originweave-evidence/tests/extraction_schema.rs index 7e932244b..6cc7af539 100644 --- a/crates/originweave-evidence/tests/extraction_schema.rs +++ b/crates/originweave-evidence/tests/extraction_schema.rs @@ -142,22 +142,26 @@ fn field_accepts_all_reviewed_value_and_source_channel_variants() #[test] fn field_rejects_contradictory_required_cardinality_contracts() { - assert!(ExtractionField::new( - "optional_exactly_one", - ExtractionValueType::Text, - ExtractionCardinality::One, - false, - &[ExtractionSourceChannel::SemanticNode], - ) - .is_err()); - assert!(ExtractionField::new( - "required_zero_or_one", - ExtractionValueType::Text, - ExtractionCardinality::ZeroOrOne, - true, - &[ExtractionSourceChannel::SemanticNode], - ) - .is_err()); + assert!( + ExtractionField::new( + "optional_exactly_one", + ExtractionValueType::Text, + ExtractionCardinality::One, + false, + &[ExtractionSourceChannel::SemanticNode], + ) + .is_err() + ); + assert!( + ExtractionField::new( + "required_zero_or_one", + ExtractionValueType::Text, + ExtractionCardinality::ZeroOrOne, + true, + &[ExtractionSourceChannel::SemanticNode], + ) + .is_err() + ); } #[test] From 212d48d3a673389eb77544c1d673452e2ff6afc7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:12:28 -0700 Subject: [PATCH 26/31] fix(evidence): reject contradictory field cardinality --- .../originweave-evidence/src/extraction_schema.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/originweave-evidence/src/extraction_schema.rs b/crates/originweave-evidence/src/extraction_schema.rs index ca2ba4881..14a86a24c 100644 --- a/crates/originweave-evidence/src/extraction_schema.rs +++ b/crates/originweave-evidence/src/extraction_schema.rs @@ -71,6 +71,8 @@ pub enum ExtractionSchemaError { InvalidIdentifier, /// An identifier or field collection exceeded its bounded limit. LimitExceeded, + /// A field's required flag contradicted its declared cardinality. + InvalidCardinalityRequirement, /// A field did not declare any reviewed source channel. MissingSourceChannel, /// A field declared the same source channel more than once. @@ -88,6 +90,9 @@ impl fmt::Display for ExtractionSchemaError { formatter.write_str(match self { Self::InvalidIdentifier => "invalid extraction schema or field identifier", Self::LimitExceeded => "extraction schema limit exceeded", + Self::InvalidCardinalityRequirement => { + "extraction field required flag is incompatible with the declared cardinality" + } Self::MissingSourceChannel => "extraction field requires at least one source channel", Self::DuplicateSourceChannel => "extraction field contains a duplicate source channel", Self::InvalidNormalizationRule => { @@ -141,6 +146,16 @@ impl ExtractionField { source_channels: &[ExtractionSourceChannel], ) -> Result { validate_identifier(identifier)?; + + let cardinality_requirement_is_compatible = match cardinality { + ExtractionCardinality::One => required, + ExtractionCardinality::ZeroOrOne => !required, + ExtractionCardinality::Many => true, + }; + if !cardinality_requirement_is_compatible { + return Err(ExtractionSchemaError::InvalidCardinalityRequirement); + } + if source_channels.is_empty() { return Err(ExtractionSchemaError::MissingSourceChannel); } From 67545c69a2c19dcd73871e1db6c8d3f72e7aca83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:12:55 -0700 Subject: [PATCH 27/31] test(evidence): bind cardinality failure type --- .../tests/extraction_schema.rs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/originweave-evidence/tests/extraction_schema.rs b/crates/originweave-evidence/tests/extraction_schema.rs index 6cc7af539..fc875ef0f 100644 --- a/crates/originweave-evidence/tests/extraction_schema.rs +++ b/crates/originweave-evidence/tests/extraction_schema.rs @@ -137,30 +137,39 @@ fn field_accepts_all_reviewed_value_and_source_channel_variants() assert_eq!(field.cardinality(), ExtractionCardinality::Many); assert_eq!(field.source_channels(), &[source_channel]); } + + let required_many = field( + "required_many", + ExtractionValueType::Text, + ExtractionCardinality::Many, + true, + &[ExtractionSourceChannel::SemanticNode], + )?; + assert!(required_many.required()); Ok(()) } #[test] fn field_rejects_contradictory_required_cardinality_contracts() { - assert!( + assert_eq!( ExtractionField::new( "optional_exactly_one", ExtractionValueType::Text, ExtractionCardinality::One, false, &[ExtractionSourceChannel::SemanticNode], - ) - .is_err() + ), + Err(ExtractionSchemaError::InvalidCardinalityRequirement) ); - assert!( + assert_eq!( ExtractionField::new( "required_zero_or_one", ExtractionValueType::Text, ExtractionCardinality::ZeroOrOne, true, &[ExtractionSourceChannel::SemanticNode], - ) - .is_err() + ), + Err(ExtractionSchemaError::InvalidCardinalityRequirement) ); } From f2e6f2d4248d41d8b65c85d5ff1a6dfb53ea9006 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:13:06 -0700 Subject: [PATCH 28/31] test(evidence): cover cardinality error contract --- .../tests/extraction_schema_error_contract.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-evidence/tests/extraction_schema_error_contract.rs b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs index 1ba248a1d..b4897d90f 100644 --- a/crates/originweave-evidence/tests/extraction_schema_error_contract.rs +++ b/crates/originweave-evidence/tests/extraction_schema_error_contract.rs @@ -17,6 +17,10 @@ fn extraction_schema_errors_implement_standard_error_contract() { ExtractionSchemaError::LimitExceeded, "extraction schema limit exceeded", ), + ( + ExtractionSchemaError::InvalidCardinalityRequirement, + "extraction field required flag is incompatible with the declared cardinality", + ), ( ExtractionSchemaError::MissingSourceChannel, "extraction field requires at least one source channel", From c38b9665774d6b3754e572bed527737b5e179833 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:13:41 -0700 Subject: [PATCH 29/31] docs(evidence): bind cardinality presence semantics --- docs/adr/0106-provenance-evidence-model.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0106-provenance-evidence-model.md b/docs/adr/0106-provenance-evidence-model.md index 60dbb929c..09cb0d7ca 100644 --- a/docs/adr/0106-provenance-evidence-model.md +++ b/docs/adr/0106-provenance-evidence-model.md @@ -37,7 +37,7 @@ WARC and PROV are interoperability/export contracts, not substitutes for OriginW A versioned `ExtractionSchema` is the binding contract for typed extraction before any capture persistence or export format is allowed to claim semantic authority. Each schema version contains an ordered, non-empty set of unique `ExtractionField` definitions. Schema-version and field identifiers are bounded to 128 encoded bytes, begin with a lowercase ASCII letter, and thereafter admit only lowercase ASCII letters, digits, `_`, or `-`. One schema admits at most 256 fields. -Every extraction field binds its stable identifier to a value type, cardinality, required/optional status, deterministic normalization rule, and a non-empty duplicate-free set of reviewed source-channel classes. `Verbatim` is the compatibility default used by the existing constructor. `TrimTextWhitespace` is admitted only for text fields and `Rfc3339Utc` only for timestamp fields; type-incompatible normalization fails closed. A `ModelInterpretation` source channel is classification metadata only and does not grant model execution, approval, disclosure, browser, network, secret, or storage authority. +Every extraction field binds its stable identifier to a value type, cardinality, required/optional status, deterministic normalization rule, and a non-empty duplicate-free set of reviewed source-channel classes. Cardinality and required status form one internally consistent presence contract: `One` is necessarily required, `ZeroOrOne` is necessarily optional, and `Many` may be marked required or optional because this value-object layer does not yet define a minimum collection item count. Contradictory `One`/optional or `ZeroOrOne`/required declarations fail closed during field construction. `Verbatim` is the compatibility default used by the existing constructor. `TrimTextWhitespace` is admitted only for text fields and `Rfc3339Utc` only for timestamp fields; type-incompatible normalization fails closed. A `ModelInterpretation` source channel is classification metadata only and does not grant model execution, approval, disclosure, browser, network, secret, or storage authority. At this value-object boundary, the version identifier is immutable schema identity; there is deliberately no registry that silently treats two different field contracts as compatible merely because their version strings compare or sort in a particular way. Callers changing a field identifier, value type, cardinality, required status, normalization rule, or admitted source-channel set must use a distinct reviewed schema version and perform any migration/compatibility decision at an explicit higher layer. The current schema object does not itself read browser data, materialize extracted values, persist artifacts, execute models, or change governance policy. Those capabilities require separately authorized runtime boundaries and are not implied by schema construction. @@ -51,7 +51,7 @@ A schema consumer can also determine the exact field/type/cardinality/normalizat If mandatory evidence cannot be recorded durably enough for a governed state-changing action, the action fails before execution or reports an explicit unverifiable failure; it is never marked proved. Read-only operations may degrade to reduced evidence only when the API contract declares that mode. Corrupt or incomplete evidence is quarantined rather than silently accepted. -Invalid or oversized extraction identifiers, empty or duplicate field sets, missing or duplicate source channels, and type-incompatible normalization rules fail during schema construction. A caller must not reinterpret such a failure as an empty/default-success schema or silently substitute another source channel. +Invalid or oversized extraction identifiers, contradictory cardinality/required declarations, empty or duplicate field sets, missing or duplicate source channels, and type-incompatible normalization rules fail during schema construction. A caller must not reinterpret such a failure as an empty/default-success schema or silently substitute another source channel. ## Security / privacy / governance impact @@ -63,7 +63,7 @@ The extraction-schema contract does not modify governance authority. It describe Require provenance-link tests, credential-leak tests, integrity/corruption tests, crash-recovery tests, WARC/export conformance where implemented, PROV relation/schema tests where implemented, retention/deletion tests, tenant-isolation tests, and end-to-end checks that state-changing actions link request, policy, approval, execution, and post-condition as separate records. Export tests must prove that disabled or unauthorized source bodies never appear merely because metadata provenance is exportable. -The extraction-schema boundary additionally requires tests for the identifier grammar and limits, field-count bound, duplicate identifiers, source-channel presence and uniqueness, every reviewed value/cardinality/source-channel variant, deterministic normalization selection, incompatible normalization rejection, and the backward-compatible `Verbatim` constructor default. +The extraction-schema boundary additionally requires tests for the identifier grammar and limits, field-count bound, duplicate identifiers, source-channel presence and uniqueness, every reviewed value/cardinality/source-channel variant, consistent cardinality/required combinations and contradictory-combination rejection, deterministic normalization selection, incompatible normalization rejection, and the backward-compatible `Verbatim` constructor default. ## Migration and rollback From 6a17e5f8e29f77a08bfee0983ad680da8b863e61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 18:23:26 -0700 Subject: [PATCH 30/31] test(docs): pin RFC 5280 author identity --- tests/test_doctoring_reference_contract.py | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/test_doctoring_reference_contract.py diff --git a/tests/test_doctoring_reference_contract.py b/tests/test_doctoring_reference_contract.py new file mode 100644 index 000000000..bdeded44f --- /dev/null +++ b/tests/test_doctoring_reference_contract.py @@ -0,0 +1,28 @@ +"""Regression contracts for standards references that bind OriginWeave design claims.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +DOCTORING = ROOT / "docs" / "doctoring.md" + + +class DoctoringReferenceContractTests(unittest.TestCase): + """Keep cited primary-standard authorship aligned with the canonical source.""" + + def test_rfc_5280_reference_uses_canonical_author_initials(self) -> None: + """RFC 5280 must credit Sharon Boeyen as S. Boeyen, matching RFC Editor metadata.""" + text = DOCTORING.read_text(encoding="utf-8") + expected = ( + "Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., & Polk, W. " + "(2008). *Internet X.509 public key infrastructure certificate and certificate " + "revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. " + "https://doi.org/10.17487/RFC5280" + ) + self.assertIn(expected, text) + + +if __name__ == "__main__": + unittest.main() From b35d739017aa5d361b605be48045be50b5a35f6f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 18:26:19 -0700 Subject: [PATCH 31/31] fix(docs): correct RFC 5280 author identity --- docs/doctoring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index aa2d1efc8..fcd9dd4f0 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -120,7 +120,7 @@ Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chro Chromium Authors. (2026). *URL canonicalizer unit tests* [Source code]. Chromium. https://chromium.googlesource.com/chromium/src/+/446d05d21720f0b3505ec21057b3e9f909784262/url/url_canon_unittest.cc -Cooper, D., Santesson, S., Farrell, S., Boeyen, R., Housley, R., & Polk, W. (2008). *Internet X.509 public key infrastructure certificate and certificate revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. https://doi.org/10.17487/RFC5280 +Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., & Polk, W. (2008). *Internet X.509 public key infrastructure certificate and certificate revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. https://doi.org/10.17487/RFC5280 Cotton, M., Vegoda, L., Bonica, R., & Haberman, B. (2013). *Special-purpose IP address registries* (RFC 6890). Internet Engineering Task Force. https://doi.org/10.17487/RFC6890