From 603ae1c629408e670ff07f11464adea674bb8969 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:37:34 +0900 Subject: [PATCH 01/19] test(core): require bounded semantic node observation --- .../tests/semantic_node_observation.rs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 crates/originweave-core/tests/semantic_node_observation.rs diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs new file mode 100644 index 00000000..595963ed --- /dev/null +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -0,0 +1,56 @@ +use std::collections::BTreeSet; +use std::error::Error; + +use originweave_core::{ + BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeActionKind, ObservationChannel, Origin, + ObservedNodeHandle, SemanticNodeObservation, SemanticNodeObservationInput, +}; + +fn observed_node() -> Result> { + Ok(ObservedNodeHandle::new( + BrowserSessionId::new(7)?, + BrowsingContextId::new(11)?, + Origin::parse("https://example.com")?, + DocumentEpoch::new(3)?, + 17, + )?) +} + +#[test] +fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), Box> { + let handle = observed_node()?; + let observation = SemanticNodeObservation::new(SemanticNodeObservationInput { + handle: handle.clone(), + role: "textbox".to_owned(), + accessible_name: "Email address".to_owned(), + visible_text: Some("name@example.test".to_owned()), + enabled: true, + visible: true, + selected: None, + supported_actions: BTreeSet::from([NodeActionKind::Click, NodeActionKind::TypeText]), + evidence_channels: BTreeSet::from([ + ObservationChannel::Accessibility, + ObservationChannel::Dom, + ]), + })?; + + assert_eq!(observation.handle(), &handle); + assert_eq!(observation.role(), "textbox"); + assert_eq!(observation.accessible_name(), "Email address"); + assert_eq!(observation.visible_text(), Some("name@example.test")); + assert!(observation.is_enabled()); + assert!(observation.is_visible()); + assert_eq!(observation.is_selected(), None); + assert_eq!( + observation.supported_actions(), + &BTreeSet::from([NodeActionKind::Click, NodeActionKind::TypeText]) + ); + assert_eq!( + observation.evidence_channels(), + &BTreeSet::from([ + ObservationChannel::Accessibility, + ObservationChannel::Dom, + ]) + ); + Ok(()) +} From f876711ef2cf4ab7223bb1063bb95b24b8200f99 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:40:20 +0900 Subject: [PATCH 02/19] style(core): format semantic observation RED contract --- .../originweave-core/tests/semantic_node_observation.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index 595963ed..f2826ace 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -2,8 +2,8 @@ use std::collections::BTreeSet; use std::error::Error; use originweave_core::{ - BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeActionKind, ObservationChannel, Origin, - ObservedNodeHandle, SemanticNodeObservation, SemanticNodeObservationInput, + BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeActionKind, ObservationChannel, + ObservedNodeHandle, Origin, SemanticNodeObservation, SemanticNodeObservationInput, }; fn observed_node() -> Result> { @@ -47,10 +47,7 @@ fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), Box Date: Tue, 11 Aug 2026 01:41:52 +0900 Subject: [PATCH 03/19] test(core): isolate semantic observation RED failure --- .../tests/semantic_node_observation.rs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index f2826ace..a57496c0 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -1,23 +1,21 @@ use std::collections::BTreeSet; -use std::error::Error; use originweave_core::{ BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeActionKind, ObservationChannel, ObservedNodeHandle, Origin, SemanticNodeObservation, SemanticNodeObservationInput, }; -fn observed_node() -> Result> { - Ok(ObservedNodeHandle::new( - BrowserSessionId::new(7)?, - BrowsingContextId::new(11)?, - Origin::parse("https://example.com")?, - DocumentEpoch::new(3)?, - 17, - )?) +fn observed_node() -> Result { + let browser_session = BrowserSessionId::new(7).map_err(|error| error.to_string())?; + let browsing_context = BrowsingContextId::new(11).map_err(|error| error.to_string())?; + let origin = Origin::parse("https://example.com").map_err(|error| format!("{error:?}"))?; + let document_epoch = DocumentEpoch::new(3).map_err(|error| error.to_string())?; + ObservedNodeHandle::new(browser_session, browsing_context, origin, document_epoch, 17) + .map_err(|error| error.to_string()) } #[test] -fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), Box> { +fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), String> { let handle = observed_node()?; let observation = SemanticNodeObservation::new(SemanticNodeObservationInput { handle: handle.clone(), @@ -32,7 +30,8 @@ fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), Box Date: Tue, 11 Aug 2026 01:43:25 +0900 Subject: [PATCH 04/19] test(core): specify bounded semantic observation failures --- .../tests/semantic_node_observation.rs | 118 ++++++++++++++++-- 1 file changed, 108 insertions(+), 10 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index a57496c0..a970c42a 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -2,7 +2,9 @@ use std::collections::BTreeSet; use originweave_core::{ BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeActionKind, ObservationChannel, - ObservedNodeHandle, Origin, SemanticNodeObservation, SemanticNodeObservationInput, + ObservedNodeHandle, Origin, SemanticNodeObservation, SemanticNodeObservationError, + SemanticNodeObservationInput, MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_ROLE_BYTES, + MAX_VISIBLE_TEXT_BYTES, }; fn observed_node() -> Result { @@ -14,14 +16,16 @@ fn observed_node() -> Result { .map_err(|error| error.to_string()) } -#[test] -fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), String> { - let handle = observed_node()?; - let observation = SemanticNodeObservation::new(SemanticNodeObservationInput { - handle: handle.clone(), - role: "textbox".to_owned(), - accessible_name: "Email address".to_owned(), - visible_text: Some("name@example.test".to_owned()), +fn semantic_input( + role: String, + accessible_name: String, + visible_text: Option, +) -> Result { + Ok(SemanticNodeObservationInput { + handle: observed_node()?, + role, + accessible_name, + visible_text, enabled: true, visible: true, selected: None, @@ -31,7 +35,17 @@ fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), String> ObservationChannel::Dom, ]), }) - .map_err(|error| error.to_string())?; +} + +#[test] +fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), String> { + let input = semantic_input( + "textbox".to_owned(), + "Email address".to_owned(), + Some("name@example.test".to_owned()), + )?; + let handle = input.handle.clone(); + let observation = SemanticNodeObservation::new(input).map_err(|error| error.to_string())?; assert_eq!(observation.handle(), &handle); assert_eq!(observation.role(), "textbox"); @@ -50,3 +64,87 @@ fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), String> ); Ok(()) } + +#[test] +fn reviewed_text_bounds_are_inclusive_and_visible_text_is_optional() -> Result<(), String> { + let boundary = SemanticNodeObservation::new(semantic_input( + "r".repeat(MAX_SEMANTIC_ROLE_BYTES), + "n".repeat(MAX_ACCESSIBLE_NAME_BYTES), + Some("v".repeat(MAX_VISIBLE_TEXT_BYTES)), + )?) + .map_err(|error| error.to_string())?; + assert_eq!(boundary.role().len(), MAX_SEMANTIC_ROLE_BYTES); + assert_eq!(boundary.accessible_name().len(), MAX_ACCESSIBLE_NAME_BYTES); + assert_eq!(boundary.visible_text().map(str::len), Some(MAX_VISIBLE_TEXT_BYTES)); + + let without_text = SemanticNodeObservation::new(semantic_input( + "button".to_owned(), + String::new(), + None, + )?) + .map_err(|error| error.to_string())?; + assert_eq!(without_text.visible_text(), None); + Ok(()) +} + +#[test] +fn semantic_node_rejects_unbounded_or_missing_role_text() -> Result<(), String> { + let empty_role = SemanticNodeObservation::new(semantic_input( + String::new(), + "name".to_owned(), + None, + )?) + .err(); + assert_eq!(empty_role, Some(SemanticNodeObservationError::EmptyRole)); + + let long_role = SemanticNodeObservation::new(semantic_input( + "r".repeat(MAX_SEMANTIC_ROLE_BYTES + 1), + "name".to_owned(), + None, + )?) + .err(); + assert_eq!(long_role, Some(SemanticNodeObservationError::RoleTooLong)); + + let long_name = SemanticNodeObservation::new(semantic_input( + "button".to_owned(), + "n".repeat(MAX_ACCESSIBLE_NAME_BYTES + 1), + None, + )?) + .err(); + assert_eq!( + long_name, + Some(SemanticNodeObservationError::AccessibleNameTooLong) + ); + + let long_visible_text = SemanticNodeObservation::new(semantic_input( + "button".to_owned(), + "name".to_owned(), + Some("v".repeat(MAX_VISIBLE_TEXT_BYTES + 1)), + )?) + .err(); + assert_eq!( + long_visible_text, + Some(SemanticNodeObservationError::VisibleTextTooLong) + ); + Ok(()) +} + +#[test] +fn semantic_node_errors_are_stable_and_credential_free() { + assert_eq!( + SemanticNodeObservationError::EmptyRole.to_string(), + "semantic node role must not be empty" + ); + assert_eq!( + SemanticNodeObservationError::RoleTooLong.to_string(), + "semantic node role exceeds 64 UTF-8 bytes" + ); + assert_eq!( + SemanticNodeObservationError::AccessibleNameTooLong.to_string(), + "semantic node accessible name exceeds 512 UTF-8 bytes" + ); + assert_eq!( + SemanticNodeObservationError::VisibleTextTooLong.to_string(), + "semantic node visible text exceeds 4096 UTF-8 bytes" + ); +} From 4aae3bce287c62fe8aa27194692f09dffd600d09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:44:49 +0900 Subject: [PATCH 05/19] feat(core): scaffold semantic observation module --- crates/originweave-core/src/semantic_observation.rs | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/originweave-core/src/semantic_observation.rs diff --git a/crates/originweave-core/src/semantic_observation.rs b/crates/originweave-core/src/semantic_observation.rs new file mode 100644 index 00000000..f87d0a75 --- /dev/null +++ b/crates/originweave-core/src/semantic_observation.rs @@ -0,0 +1,4 @@ +use std::collections::BTreeSet; +use std::fmt; + +use crate::ObservedNodeHandle; From 59e00a77fb6ad747e6551a396e9e7780152dd063 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:45:28 +0900 Subject: [PATCH 06/19] feat(core): implement bounded semantic observation --- .../src/semantic_observation.rs | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/crates/originweave-core/src/semantic_observation.rs b/crates/originweave-core/src/semantic_observation.rs index f87d0a75..18028c4b 100644 --- a/crates/originweave-core/src/semantic_observation.rs +++ b/crates/originweave-core/src/semantic_observation.rs @@ -2,3 +2,202 @@ use std::collections::BTreeSet; use std::fmt; use crate::ObservedNodeHandle; + +/// Maximum UTF-8 byte length retained for one semantic node role. +pub const MAX_SEMANTIC_ROLE_BYTES: usize = 64; +/// Maximum UTF-8 byte length retained for one semantic node accessible name. +pub const MAX_ACCESSIBLE_NAME_BYTES: usize = 512; +/// Maximum UTF-8 byte length retained for one semantic node visible-text excerpt. +pub const MAX_VISIBLE_TEXT_BYTES: usize = 4_096; + +/// A node-local typed action advertised by an observation adapter. +/// +/// This is descriptive evidence only and never grants execution authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum NodeActionKind { + /// Activate the node using browser-native click semantics. + Click, + /// Insert bounded non-secret text using browser-native input semantics. + TypeText, + /// Select one option using browser-native selection semantics. + SelectOption, + /// Set a checkable control to an explicit checked state. + SetChecked, + /// Scroll the node into the viewport without activating it. + ScrollIntoView, +} + +/// A structured evidence channel that contributed to a semantic observation. +/// +/// Channel provenance never converts page-provided content into trusted instruction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ObservationChannel { + /// Experimental structured browser tool metadata, such as WebMCP when available. + WebMcp, + /// Structured data interpreted by a versioned adapter. + StructuredData, + /// Browser accessibility-tree evidence. + Accessibility, + /// Browser DOM evidence used through a bounded adapter. + Dom, + /// Browser layout evidence used through a bounded adapter. + Layout, + /// Bounded visual evidence used when structured channels are insufficient. + Visual, +} + +/// Caller-owned fields used to construct one bounded semantic node observation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticNodeObservationInput { + /// Exact OriginWeave authority handle for the observed node. + pub handle: ObservedNodeHandle, + /// Bounded semantic or accessibility role. + pub role: String, + /// Bounded accessible name; an empty name is valid. + pub accessible_name: String, + /// Optional bounded visible-text excerpt. + pub visible_text: Option, + /// Whether the adapter observed the node as enabled. + pub enabled: bool, + /// Whether the adapter observed the node as visible. + pub visible: bool, + /// Optional selected state when that concept applies. + pub selected: Option, + /// Finite typed actions the adapter reports as meaningful for this node. + pub supported_actions: BTreeSet, + /// Finite evidence channels that contributed to this observation. + pub evidence_channels: BTreeSet, +} + +/// A bounded semantic view of one authority-bound browser node. +/// +/// The value carries no raw HTML, protocol-local identifier, or independent authorization. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticNodeObservation { + handle: ObservedNodeHandle, + role: String, + accessible_name: String, + visible_text: Option, + enabled: bool, + visible: bool, + selected: Option, + supported_actions: BTreeSet, + evidence_channels: BTreeSet, +} + +impl SemanticNodeObservation { + /// Validate reviewed text budgets and create one semantic observation. + pub fn new(input: SemanticNodeObservationInput) -> Result { + if input.role.is_empty() { + return Err(SemanticNodeObservationError::EmptyRole); + } + if input.role.len() > MAX_SEMANTIC_ROLE_BYTES { + return Err(SemanticNodeObservationError::RoleTooLong); + } + if input.accessible_name.len() > MAX_ACCESSIBLE_NAME_BYTES { + return Err(SemanticNodeObservationError::AccessibleNameTooLong); + } + if input + .visible_text + .as_ref() + .is_some_and(|text| text.len() > MAX_VISIBLE_TEXT_BYTES) + { + return Err(SemanticNodeObservationError::VisibleTextTooLong); + } + Ok(Self { + handle: input.handle, + role: input.role, + accessible_name: input.accessible_name, + visible_text: input.visible_text, + enabled: input.enabled, + visible: input.visible, + selected: input.selected, + supported_actions: input.supported_actions, + evidence_channels: input.evidence_channels, + }) + } + + /// Return the exact authority-bound node handle. + #[must_use] + pub const fn handle(&self) -> &ObservedNodeHandle { + &self.handle + } + + /// Return the bounded semantic role. + #[must_use] + pub fn role(&self) -> &str { + &self.role + } + + /// Return the bounded accessible name. + #[must_use] + pub fn accessible_name(&self) -> &str { + &self.accessible_name + } + + /// Return the optional bounded visible-text excerpt. + #[must_use] + pub fn visible_text(&self) -> Option<&str> { + self.visible_text.as_deref() + } + + /// Return whether the node was observed as enabled. + #[must_use] + pub const fn is_enabled(&self) -> bool { + self.enabled + } + + /// Return whether the node was observed as visible. + #[must_use] + pub const fn is_visible(&self) -> bool { + self.visible + } + + /// Return the optional selected state. + #[must_use] + pub const fn is_selected(&self) -> Option { + self.selected + } + + /// Return the adapter-advertised node action set. + #[must_use] + pub const fn supported_actions(&self) -> &BTreeSet { + &self.supported_actions + } + + /// Return the evidence-channel provenance set. + #[must_use] + pub const fn evidence_channels(&self) -> &BTreeSet { + &self.evidence_channels + } +} + +/// A bounded validation failure for one semantic node observation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SemanticNodeObservationError { + /// The semantic role was empty. + EmptyRole, + /// The role exceeded [`MAX_SEMANTIC_ROLE_BYTES`]. + RoleTooLong, + /// The accessible name exceeded [`MAX_ACCESSIBLE_NAME_BYTES`]. + AccessibleNameTooLong, + /// The visible-text excerpt exceeded [`MAX_VISIBLE_TEXT_BYTES`]. + VisibleTextTooLong, +} + +impl fmt::Display for SemanticNodeObservationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyRole => formatter.write_str("semantic node role must not be empty"), + Self::RoleTooLong => formatter.write_str("semantic node role exceeds 64 UTF-8 bytes"), + Self::AccessibleNameTooLong => { + formatter.write_str("semantic node accessible name exceeds 512 UTF-8 bytes") + } + Self::VisibleTextTooLong => { + formatter.write_str("semantic node visible text exceeds 4096 UTF-8 bytes") + } + } + } +} + +impl std::error::Error for SemanticNodeObservationError {} From 3754f47f59a81d17ad16204bf2f24c988dbc7a51 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:45:52 +0900 Subject: [PATCH 07/19] feat(core): export semantic observation contract --- crates/originweave-core/src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index bdd1b2aa..fdbc4d60 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -1,8 +1,8 @@ //! Shared security and governance contracts for OriginWeave. //! -//! This crate keeps the long-lived value contracts in `contracts` and the -//! protocol-identifier registry in a focused module so browser adapters can -//! evolve without turning raw CDP or WebDriver identifiers into authority. +//! This crate keeps the long-lived value contracts in `contracts`, the +//! protocol-identifier registry in a focused module, and bounded semantic +//! observations in a separate authority-preserving module. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -11,8 +11,14 @@ mod browser_registry; #[cfg(test)] mod browser_registry_coverage; mod contracts; +mod semantic_observation; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, }; pub use contracts::*; +pub use semantic_observation::{ + NodeActionKind, ObservationChannel, SemanticNodeObservation, SemanticNodeObservationError, + SemanticNodeObservationInput, MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_ROLE_BYTES, + MAX_VISIBLE_TEXT_BYTES, +}; From 10a40bea2fe754a51cf3ca8c87bfc92dc82df848 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:47:08 +0900 Subject: [PATCH 08/19] style(core): apply rustfmt to semantic exports --- crates/originweave-core/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index fdbc4d60..c45bba45 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -18,7 +18,7 @@ pub use browser_registry::{ }; pub use contracts::*; pub use semantic_observation::{ - NodeActionKind, ObservationChannel, SemanticNodeObservation, SemanticNodeObservationError, - SemanticNodeObservationInput, MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_ROLE_BYTES, - MAX_VISIBLE_TEXT_BYTES, + MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_ROLE_BYTES, MAX_VISIBLE_TEXT_BYTES, NodeActionKind, + ObservationChannel, SemanticNodeObservation, SemanticNodeObservationError, + SemanticNodeObservationInput, }; From 84f609e31314f50f364c2f60163cd712acf60185 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:48:15 +0900 Subject: [PATCH 09/19] style(core): apply rustfmt to semantic observation tests --- .../tests/semantic_node_observation.rs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index a970c42a..3871426a 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -1,10 +1,10 @@ use std::collections::BTreeSet; use originweave_core::{ - BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeActionKind, ObservationChannel, + BrowserSessionId, BrowsingContextId, DocumentEpoch, MAX_ACCESSIBLE_NAME_BYTES, + MAX_SEMANTIC_ROLE_BYTES, MAX_VISIBLE_TEXT_BYTES, NodeActionKind, ObservationChannel, ObservedNodeHandle, Origin, SemanticNodeObservation, SemanticNodeObservationError, - SemanticNodeObservationInput, MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_ROLE_BYTES, - MAX_VISIBLE_TEXT_BYTES, + SemanticNodeObservationInput, }; fn observed_node() -> Result { @@ -12,8 +12,14 @@ fn observed_node() -> Result { let browsing_context = BrowsingContextId::new(11).map_err(|error| error.to_string())?; let origin = Origin::parse("https://example.com").map_err(|error| format!("{error:?}"))?; let document_epoch = DocumentEpoch::new(3).map_err(|error| error.to_string())?; - ObservedNodeHandle::new(browser_session, browsing_context, origin, document_epoch, 17) - .map_err(|error| error.to_string()) + ObservedNodeHandle::new( + browser_session, + browsing_context, + origin, + document_epoch, + 17, + ) + .map_err(|error| error.to_string()) } fn semantic_input( @@ -75,26 +81,22 @@ fn reviewed_text_bounds_are_inclusive_and_visible_text_is_optional() -> Result<( .map_err(|error| error.to_string())?; assert_eq!(boundary.role().len(), MAX_SEMANTIC_ROLE_BYTES); assert_eq!(boundary.accessible_name().len(), MAX_ACCESSIBLE_NAME_BYTES); - assert_eq!(boundary.visible_text().map(str::len), Some(MAX_VISIBLE_TEXT_BYTES)); + assert_eq!( + boundary.visible_text().map(str::len), + Some(MAX_VISIBLE_TEXT_BYTES) + ); - let without_text = SemanticNodeObservation::new(semantic_input( - "button".to_owned(), - String::new(), - None, - )?) - .map_err(|error| error.to_string())?; + let without_text = + SemanticNodeObservation::new(semantic_input("button".to_owned(), String::new(), None)?) + .map_err(|error| error.to_string())?; assert_eq!(without_text.visible_text(), None); Ok(()) } #[test] fn semantic_node_rejects_unbounded_or_missing_role_text() -> Result<(), String> { - let empty_role = SemanticNodeObservation::new(semantic_input( - String::new(), - "name".to_owned(), - None, - )?) - .err(); + let empty_role = + SemanticNodeObservation::new(semantic_input(String::new(), "name".to_owned(), None)?).err(); assert_eq!(empty_role, Some(SemanticNodeObservationError::EmptyRole)); let long_role = SemanticNodeObservation::new(semantic_input( From 939ab063d62bdc5ba1f88cbc044ed5921d98d7ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:53:40 +0900 Subject: [PATCH 10/19] docs(changelog): record semantic observation slice --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4bd39c..7bb12e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,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. +- Authority-bound, bounded semantic node observations with typed node-local action evidence and explicit observation-channel provenance for the first Chromium vertical slice; observation metadata grants no execution authority. - 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 bda159a512e0d90b9d36e64408dab6820b164145 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:08:50 +0900 Subject: [PATCH 11/19] test(core): require semantic observation provenance --- .../tests/semantic_node_observation.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index 3871426a..9a86d010 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -93,6 +93,19 @@ fn reviewed_text_bounds_are_inclusive_and_visible_text_is_optional() -> Result<( Ok(()) } +#[test] +fn semantic_node_requires_observation_provenance() -> Result<(), String> { + let mut input = semantic_input("button".to_owned(), "Submit".to_owned(), None)?; + input.evidence_channels.clear(); + + let error = SemanticNodeObservation::new(input).err(); + assert_eq!( + error, + Some(SemanticNodeObservationError::MissingEvidenceChannel) + ); + Ok(()) +} + #[test] fn semantic_node_rejects_unbounded_or_missing_role_text() -> Result<(), String> { let empty_role = From df54c613c5c4858fc2de8103669b2139de8c053b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:12:25 +0900 Subject: [PATCH 12/19] fix(core): require semantic observation provenance --- crates/originweave-core/src/semantic_observation.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/semantic_observation.rs b/crates/originweave-core/src/semantic_observation.rs index 18028c4b..1d3ed52c 100644 --- a/crates/originweave-core/src/semantic_observation.rs +++ b/crates/originweave-core/src/semantic_observation.rs @@ -86,7 +86,7 @@ pub struct SemanticNodeObservation { } impl SemanticNodeObservation { - /// Validate reviewed text budgets and create one semantic observation. + /// Validate reviewed text budgets and provenance before creating one semantic observation. pub fn new(input: SemanticNodeObservationInput) -> Result { if input.role.is_empty() { return Err(SemanticNodeObservationError::EmptyRole); @@ -104,6 +104,9 @@ impl SemanticNodeObservation { { return Err(SemanticNodeObservationError::VisibleTextTooLong); } + if input.evidence_channels.is_empty() { + return Err(SemanticNodeObservationError::MissingEvidenceChannel); + } Ok(Self { handle: input.handle, role: input.role, @@ -165,7 +168,7 @@ impl SemanticNodeObservation { &self.supported_actions } - /// Return the evidence-channel provenance set. + /// Return the non-empty evidence-channel provenance set. #[must_use] pub const fn evidence_channels(&self) -> &BTreeSet { &self.evidence_channels @@ -183,6 +186,8 @@ pub enum SemanticNodeObservationError { AccessibleNameTooLong, /// The visible-text excerpt exceeded [`MAX_VISIBLE_TEXT_BYTES`]. VisibleTextTooLong, + /// No evidence channel was supplied for the observation. + MissingEvidenceChannel, } impl fmt::Display for SemanticNodeObservationError { @@ -196,6 +201,9 @@ impl fmt::Display for SemanticNodeObservationError { Self::VisibleTextTooLong => { formatter.write_str("semantic node visible text exceeds 4096 UTF-8 bytes") } + Self::MissingEvidenceChannel => { + formatter.write_str("semantic node observation requires at least one evidence channel") + } } } } From 3f52c1c75fd42d274fbeec13c67cbf0bb6a8488b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:13:01 +0900 Subject: [PATCH 13/19] test(core): cover provenance validation error --- crates/originweave-core/tests/semantic_node_observation.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index 9a86d010..dd9e6732 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -162,4 +162,8 @@ fn semantic_node_errors_are_stable_and_credential_free() { SemanticNodeObservationError::VisibleTextTooLong.to_string(), "semantic node visible text exceeds 4096 UTF-8 bytes" ); + assert_eq!( + SemanticNodeObservationError::MissingEvidenceChannel.to_string(), + "semantic node observation requires at least one evidence channel" + ); } From 661091dcc52f0a52e7a6a636b0f4bcea5469f82d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:18:11 +0900 Subject: [PATCH 14/19] style(core): apply rustfmt to provenance error --- crates/originweave-core/src/semantic_observation.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/semantic_observation.rs b/crates/originweave-core/src/semantic_observation.rs index 1d3ed52c..611a214a 100644 --- a/crates/originweave-core/src/semantic_observation.rs +++ b/crates/originweave-core/src/semantic_observation.rs @@ -201,9 +201,8 @@ impl fmt::Display for SemanticNodeObservationError { Self::VisibleTextTooLong => { formatter.write_str("semantic node visible text exceeds 4096 UTF-8 bytes") } - Self::MissingEvidenceChannel => { - formatter.write_str("semantic node observation requires at least one evidence channel") - } + Self::MissingEvidenceChannel => formatter + .write_str("semantic node observation requires at least one evidence channel"), } } } From b1bd4f8bd3b5597dac8ad3c40530beba7288e8ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:26:03 +0900 Subject: [PATCH 15/19] test(core): require bounded semantic relationships --- .../tests/semantic_node_observation.rs | 118 +++++++++++++++++- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index dd9e6732..b4d500d4 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -2,12 +2,12 @@ use std::collections::BTreeSet; use originweave_core::{ BrowserSessionId, BrowsingContextId, DocumentEpoch, MAX_ACCESSIBLE_NAME_BYTES, - MAX_SEMANTIC_ROLE_BYTES, MAX_VISIBLE_TEXT_BYTES, NodeActionKind, ObservationChannel, - ObservedNodeHandle, Origin, SemanticNodeObservation, SemanticNodeObservationError, - SemanticNodeObservationInput, + MAX_SEMANTIC_CHILDREN, MAX_SEMANTIC_ROLE_BYTES, MAX_VISIBLE_TEXT_BYTES, NodeActionKind, + ObservationChannel, ObservedNodeHandle, Origin, SemanticNodeObservation, + SemanticNodeObservationError, SemanticNodeObservationInput, }; -fn observed_node() -> Result { +fn observed_node_with_id(node_id: u64) -> Result { let browser_session = BrowserSessionId::new(7).map_err(|error| error.to_string())?; let browsing_context = BrowsingContextId::new(11).map_err(|error| error.to_string())?; let origin = Origin::parse("https://example.com").map_err(|error| format!("{error:?}"))?; @@ -17,11 +17,15 @@ fn observed_node() -> Result { browsing_context, origin, document_epoch, - 17, + node_id, ) .map_err(|error| error.to_string()) } +fn observed_node() -> Result { + observed_node_with_id(17) +} + fn semantic_input( role: String, accessible_name: String, @@ -29,6 +33,8 @@ fn semantic_input( ) -> Result { Ok(SemanticNodeObservationInput { handle: observed_node()?, + parent: None, + children: Vec::new(), role, accessible_name, visible_text, @@ -54,6 +60,8 @@ fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), String> let observation = SemanticNodeObservation::new(input).map_err(|error| error.to_string())?; assert_eq!(observation.handle(), &handle); + assert_eq!(observation.parent(), None); + assert!(observation.children().is_empty()); assert_eq!(observation.role(), "textbox"); assert_eq!(observation.accessible_name(), "Email address"); assert_eq!(observation.visible_text(), Some("name@example.test")); @@ -71,6 +79,90 @@ fn semantic_node_preserves_authority_and_bounded_surface() -> Result<(), String> Ok(()) } +#[test] +fn semantic_node_preserves_bounded_authority_scoped_relationships() -> Result<(), String> { + let parent = observed_node_with_id(16)?; + let first_child = observed_node_with_id(18)?; + let second_child = observed_node_with_id(19)?; + let mut input = semantic_input("group".to_owned(), "Account".to_owned(), None)?; + input.parent = Some(parent.clone()); + input.children = vec![first_child.clone(), second_child.clone()]; + + let observation = SemanticNodeObservation::new(input).map_err(|error| error.to_string())?; + assert_eq!(observation.parent(), Some(&parent)); + assert_eq!(observation.children(), &[first_child, second_child]); + Ok(()) +} + +#[test] +fn semantic_node_bounds_child_relationship_count() -> Result<(), String> { + let mut boundary = semantic_input("list".to_owned(), "Items".to_owned(), None)?; + boundary.children = (0..MAX_SEMANTIC_CHILDREN) + .map(|offset| observed_node_with_id(100 + offset as u64)) + .collect::, _>>()?; + let observation = SemanticNodeObservation::new(boundary).map_err(|error| error.to_string())?; + assert_eq!(observation.children().len(), MAX_SEMANTIC_CHILDREN); + + let mut overflow = semantic_input("list".to_owned(), "Items".to_owned(), None)?; + overflow.children = (0..=MAX_SEMANTIC_CHILDREN) + .map(|offset| observed_node_with_id(1_000 + offset as u64)) + .collect::, _>>()?; + assert_eq!( + SemanticNodeObservation::new(overflow).err(), + Some(SemanticNodeObservationError::TooManyChildren) + ); + Ok(()) +} + +#[test] +fn semantic_node_rejects_relationships_outside_exact_authority() -> Result<(), String> { + let mut input = semantic_input("group".to_owned(), "Account".to_owned(), None)?; + let different_origin = Origin::parse("https://other.example") + .map_err(|error| format!("{error:?}"))?; + input.parent = Some( + ObservedNodeHandle::new( + BrowserSessionId::new(7).map_err(|error| error.to_string())?, + BrowsingContextId::new(11).map_err(|error| error.to_string())?, + different_origin, + DocumentEpoch::new(3).map_err(|error| error.to_string())?, + 16, + ) + .map_err(|error| error.to_string())?, + ); + + assert_eq!( + SemanticNodeObservation::new(input).err(), + Some(SemanticNodeObservationError::RelationshipAuthorityMismatch) + ); + Ok(()) +} + +#[test] +fn semantic_node_rejects_self_and_duplicate_child_relationships() -> Result<(), String> { + let mut self_parent = semantic_input("group".to_owned(), "Account".to_owned(), None)?; + self_parent.parent = Some(self_parent.handle.clone()); + assert_eq!( + SemanticNodeObservation::new(self_parent).err(), + Some(SemanticNodeObservationError::SelfRelationship) + ); + + let mut self_child = semantic_input("group".to_owned(), "Account".to_owned(), None)?; + self_child.children = vec![self_child.handle.clone()]; + assert_eq!( + SemanticNodeObservation::new(self_child).err(), + Some(SemanticNodeObservationError::SelfRelationship) + ); + + let child = observed_node_with_id(18)?; + let mut duplicate = semantic_input("group".to_owned(), "Account".to_owned(), None)?; + duplicate.children = vec![child.clone(), child]; + assert_eq!( + SemanticNodeObservation::new(duplicate).err(), + Some(SemanticNodeObservationError::DuplicateChild) + ); + Ok(()) +} + #[test] fn reviewed_text_bounds_are_inclusive_and_visible_text_is_optional() -> Result<(), String> { let boundary = SemanticNodeObservation::new(semantic_input( @@ -166,4 +258,20 @@ fn semantic_node_errors_are_stable_and_credential_free() { SemanticNodeObservationError::MissingEvidenceChannel.to_string(), "semantic node observation requires at least one evidence channel" ); + assert_eq!( + SemanticNodeObservationError::TooManyChildren.to_string(), + "semantic node observation exceeds 128 child relationships" + ); + assert_eq!( + SemanticNodeObservationError::RelationshipAuthorityMismatch.to_string(), + "semantic node relationship crosses its session, context, origin, or document authority" + ); + assert_eq!( + SemanticNodeObservationError::SelfRelationship.to_string(), + "semantic node observation cannot relate the node to itself" + ); + assert_eq!( + SemanticNodeObservationError::DuplicateChild.to_string(), + "semantic node observation contains a duplicate child relationship" + ); } From e8be794fe46b167ba8446da1a0f9c4725a4a5ab6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 05:08:28 +0900 Subject: [PATCH 16/19] feat(core): bound semantic node relationships to exact authority --- .../src/semantic_observation.rs | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/semantic_observation.rs b/crates/originweave-core/src/semantic_observation.rs index 611a214a..950cff6d 100644 --- a/crates/originweave-core/src/semantic_observation.rs +++ b/crates/originweave-core/src/semantic_observation.rs @@ -9,6 +9,8 @@ pub const MAX_SEMANTIC_ROLE_BYTES: usize = 64; pub const MAX_ACCESSIBLE_NAME_BYTES: usize = 512; /// Maximum UTF-8 byte length retained for one semantic node visible-text excerpt. pub const MAX_VISIBLE_TEXT_BYTES: usize = 4_096; +/// Maximum number of child relationships retained for one semantic node observation. +pub const MAX_SEMANTIC_CHILDREN: usize = 128; /// A node-local typed action advertised by an observation adapter. /// @@ -51,6 +53,10 @@ pub enum ObservationChannel { pub struct SemanticNodeObservationInput { /// Exact OriginWeave authority handle for the observed node. pub handle: ObservedNodeHandle, + /// Optional exact-authority parent relationship. + pub parent: Option, + /// Bounded exact-authority child relationships in adapter-observed order. + pub children: Vec, /// Bounded semantic or accessibility role. pub role: String, /// Bounded accessible name; an empty name is valid. @@ -75,6 +81,8 @@ pub struct SemanticNodeObservationInput { #[derive(Debug, Clone, PartialEq, Eq)] pub struct SemanticNodeObservation { handle: ObservedNodeHandle, + parent: Option, + children: Vec, role: String, accessible_name: String, visible_text: Option, @@ -86,7 +94,7 @@ pub struct SemanticNodeObservation { } impl SemanticNodeObservation { - /// Validate reviewed text budgets and provenance before creating one semantic observation. + /// Validate reviewed text, relationship, authority, and provenance bounds. pub fn new(input: SemanticNodeObservationInput) -> Result { if input.role.is_empty() { return Err(SemanticNodeObservationError::EmptyRole); @@ -107,8 +115,22 @@ impl SemanticNodeObservation { if input.evidence_channels.is_empty() { return Err(SemanticNodeObservationError::MissingEvidenceChannel); } + if input.children.len() > MAX_SEMANTIC_CHILDREN { + return Err(SemanticNodeObservationError::TooManyChildren); + } + if let Some(parent) = input.parent.as_ref() { + validate_relationship(&input.handle, parent)?; + } + for (index, child) in input.children.iter().enumerate() { + validate_relationship(&input.handle, child)?; + if input.children[..index].contains(child) { + return Err(SemanticNodeObservationError::DuplicateChild); + } + } Ok(Self { handle: input.handle, + parent: input.parent, + children: input.children, role: input.role, accessible_name: input.accessible_name, visible_text: input.visible_text, @@ -126,6 +148,18 @@ impl SemanticNodeObservation { &self.handle } + /// Return the optional exact-authority parent relationship. + #[must_use] + pub const fn parent(&self) -> Option<&ObservedNodeHandle> { + self.parent.as_ref() + } + + /// Return the bounded exact-authority child relationships in observed order. + #[must_use] + pub fn children(&self) -> &[ObservedNodeHandle] { + &self.children + } + /// Return the bounded semantic role. #[must_use] pub fn role(&self) -> &str { @@ -175,6 +209,23 @@ impl SemanticNodeObservation { } } +fn validate_relationship( + handle: &ObservedNodeHandle, + related: &ObservedNodeHandle, +) -> Result<(), SemanticNodeObservationError> { + if handle == related { + return Err(SemanticNodeObservationError::SelfRelationship); + } + if handle.browser_session() != related.browser_session() + || handle.browsing_context() != related.browsing_context() + || handle.origin() != related.origin() + || handle.document_epoch() != related.document_epoch() + { + return Err(SemanticNodeObservationError::RelationshipAuthorityMismatch); + } + Ok(()) +} + /// A bounded validation failure for one semantic node observation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SemanticNodeObservationError { @@ -188,6 +239,14 @@ pub enum SemanticNodeObservationError { VisibleTextTooLong, /// No evidence channel was supplied for the observation. MissingEvidenceChannel, + /// The child relationship list exceeded [`MAX_SEMANTIC_CHILDREN`]. + TooManyChildren, + /// A relationship crossed the observation's session, context, origin, or document authority. + RelationshipAuthorityMismatch, + /// The observation attempted to relate the node to itself. + SelfRelationship, + /// The child relationship list contained the same exact handle more than once. + DuplicateChild, } impl fmt::Display for SemanticNodeObservationError { @@ -203,6 +262,17 @@ impl fmt::Display for SemanticNodeObservationError { } Self::MissingEvidenceChannel => formatter .write_str("semantic node observation requires at least one evidence channel"), + Self::TooManyChildren => { + formatter.write_str("semantic node observation exceeds 128 child relationships") + } + Self::RelationshipAuthorityMismatch => formatter.write_str( + "semantic node relationship crosses its session, context, origin, or document authority", + ), + Self::SelfRelationship => { + formatter.write_str("semantic node observation cannot relate the node to itself") + } + Self::DuplicateChild => formatter + .write_str("semantic node observation contains a duplicate child relationship"), } } } From 632e72421e2008ff434bbc0a2027dc28334fa73c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 05:08:49 +0900 Subject: [PATCH 17/19] feat(core): export semantic relationship bound --- crates/originweave-core/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index c45bba45..9d75d9e6 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -18,7 +18,7 @@ pub use browser_registry::{ }; pub use contracts::*; pub use semantic_observation::{ - MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_ROLE_BYTES, MAX_VISIBLE_TEXT_BYTES, NodeActionKind, - ObservationChannel, SemanticNodeObservation, SemanticNodeObservationError, - SemanticNodeObservationInput, + MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_CHILDREN, MAX_SEMANTIC_ROLE_BYTES, + MAX_VISIBLE_TEXT_BYTES, NodeActionKind, ObservationChannel, SemanticNodeObservation, + SemanticNodeObservationError, SemanticNodeObservationInput, }; From dbe75ca557fc6f501b0e54846c81dffa58812ced Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 05:09:41 +0900 Subject: [PATCH 18/19] style(core): apply canonical semantic relationship formatting --- crates/originweave-core/tests/semantic_node_observation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index b4d500d4..13f6ade5 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -117,8 +117,8 @@ fn semantic_node_bounds_child_relationship_count() -> Result<(), String> { #[test] fn semantic_node_rejects_relationships_outside_exact_authority() -> Result<(), String> { let mut input = semantic_input("group".to_owned(), "Account".to_owned(), None)?; - let different_origin = Origin::parse("https://other.example") - .map_err(|error| format!("{error:?}"))?; + let different_origin = + Origin::parse("https://other.example").map_err(|error| format!("{error:?}"))?; input.parent = Some( ObservedNodeHandle::new( BrowserSessionId::new(7).map_err(|error| error.to_string())?, From 94fd284fe41746eeba9edc05d9753903b1c41ebf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 05:19:22 +0900 Subject: [PATCH 19/19] test(core): cover each semantic relationship authority axis --- .../tests/semantic_node_observation.rs | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_observation.rs b/crates/originweave-core/tests/semantic_node_observation.rs index 13f6ade5..0e75d698 100644 --- a/crates/originweave-core/tests/semantic_node_observation.rs +++ b/crates/originweave-core/tests/semantic_node_observation.rs @@ -7,11 +7,20 @@ use originweave_core::{ SemanticNodeObservationError, SemanticNodeObservationInput, }; -fn observed_node_with_id(node_id: u64) -> Result { - let browser_session = BrowserSessionId::new(7).map_err(|error| error.to_string())?; - let browsing_context = BrowsingContextId::new(11).map_err(|error| error.to_string())?; - let origin = Origin::parse("https://example.com").map_err(|error| format!("{error:?}"))?; - let document_epoch = DocumentEpoch::new(3).map_err(|error| error.to_string())?; +fn observed_node_with_authority( + browser_session_id: u64, + browsing_context_id: u64, + origin_value: &str, + document_epoch_value: u64, + node_id: u64, +) -> Result { + let browser_session = + BrowserSessionId::new(browser_session_id).map_err(|error| error.to_string())?; + let browsing_context = + BrowsingContextId::new(browsing_context_id).map_err(|error| error.to_string())?; + let origin = Origin::parse(origin_value).map_err(|error| format!("{error:?}"))?; + let document_epoch = + DocumentEpoch::new(document_epoch_value).map_err(|error| error.to_string())?; ObservedNodeHandle::new( browser_session, browsing_context, @@ -22,6 +31,10 @@ fn observed_node_with_id(node_id: u64) -> Result { .map_err(|error| error.to_string()) } +fn observed_node_with_id(node_id: u64) -> Result { + observed_node_with_authority(7, 11, "https://example.com", 3, node_id) +} + fn observed_node() -> Result { observed_node_with_id(17) } @@ -115,23 +128,33 @@ fn semantic_node_bounds_child_relationship_count() -> Result<(), String> { } #[test] -fn semantic_node_rejects_relationships_outside_exact_authority() -> Result<(), String> { - let mut input = semantic_input("group".to_owned(), "Account".to_owned(), None)?; - let different_origin = - Origin::parse("https://other.example").map_err(|error| format!("{error:?}"))?; - input.parent = Some( - ObservedNodeHandle::new( - BrowserSessionId::new(7).map_err(|error| error.to_string())?, - BrowsingContextId::new(11).map_err(|error| error.to_string())?, - different_origin, - DocumentEpoch::new(3).map_err(|error| error.to_string())?, - 16, - ) - .map_err(|error| error.to_string())?, - ); +fn semantic_node_rejects_each_relationship_authority_axis() -> Result<(), String> { + let mismatched_parents = [ + observed_node_with_authority(8, 11, "https://example.com", 3, 16)?, + observed_node_with_authority(7, 12, "https://example.com", 3, 16)?, + observed_node_with_authority(7, 11, "https://other.example", 3, 16)?, + observed_node_with_authority(7, 11, "https://example.com", 4, 16)?, + ]; + + for parent in mismatched_parents { + let mut input = semantic_input("group".to_owned(), "Account".to_owned(), None)?; + input.parent = Some(parent); + assert_eq!( + SemanticNodeObservation::new(input).err(), + Some(SemanticNodeObservationError::RelationshipAuthorityMismatch) + ); + } + let mut child_input = semantic_input("group".to_owned(), "Account".to_owned(), None)?; + child_input.children = vec![observed_node_with_authority( + 7, + 11, + "https://other.example", + 3, + 18, + )?]; assert_eq!( - SemanticNodeObservation::new(input).err(), + SemanticNodeObservation::new(child_input).err(), Some(SemanticNodeObservationError::RelationshipAuthorityMismatch) ); Ok(())