Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 90 additions & 21 deletions crates/originweave-evidence/src/action_outcome.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::error::Error;
use std::fmt::{self, Display, Formatter};

use originweave_core::{ActionIntentDigest, ActionKind, Origin};
use originweave_core::{ActionIntentDigest, ActionKind, ObservedNodeHandle, Origin};

use crate::{ProvenanceRecord, VerificationResult};

Expand Down Expand Up @@ -30,8 +30,12 @@ pub enum VerifiedActionOutcomeError {
/// Monotonic timestamp recorded when the post-condition was observed.
observed_at_milliseconds: u64,
},
/// Node-state success used the generic constructor without exact node authority.
NodeStateTargetRequired,
/// Node-state provenance belongs to an origin other than the governed action target.
PostConditionOriginMismatch,
/// The observed node differs from the exact governed action target node.
PostConditionNodeMismatch,
}

impl Display for VerifiedActionOutcomeError {
Expand All @@ -46,9 +50,15 @@ impl Display for VerifiedActionOutcomeError {
formatter,
"post-condition observation at {observed_at_milliseconds} ms predates action dispatch at {dispatched_at_milliseconds} ms"
),
Self::NodeStateTargetRequired => formatter.write_str(
"node-state post-condition requires the exact governed action target node",
),
Self::PostConditionOriginMismatch => formatter.write_str(
"node-state post-condition provenance must match the governed action target origin",
),
Self::PostConditionNodeMismatch => formatter.write_str(
"node-state post-condition must observe the exact governed action target node",
),
}
}
}
Expand All @@ -59,14 +69,15 @@ impl Error for VerifiedActionOutcomeError {}
///
/// Construction is intentionally fail-closed: a command acknowledgement, an
/// unverified observation, rejected provenance, an observation timestamp earlier
/// than the action dispatch, or node-state provenance from a different origin
/// cannot be represented by this type as successful action completion. Equal
/// dispatch and observation timestamps are permitted because a bounded adapter
/// may use a coarse monotonic clock.
/// than the action dispatch, or a node-state observation that is not bound to the
/// exact governed [`ObservedNodeHandle`] cannot be represented by this type as
/// successful action completion. Equal dispatch and observation timestamps are
/// permitted because a bounded adapter may use a coarse monotonic clock.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedActionOutcomeEvidence {
action: ActionKind,
target_origin: Origin,
target_node: Option<ObservedNodeHandle>,
intent_digest: ActionIntentDigest,
post_condition: PostConditionKind,
dispatched_at_milliseconds: u64,
Expand All @@ -75,12 +86,13 @@ pub struct VerifiedActionOutcomeEvidence {
}

impl VerifiedActionOutcomeEvidence {
/// Create successful action evidence from verified, temporally ordered provenance.
/// Create successful non-node action evidence from verified, ordered provenance.
///
/// Both timestamps must come from the same monotonic clock domain. The
/// observation may share the dispatch tick, but it may never predate it.
/// `NodeStateChanged` provenance must also originate from the canonical
/// action target; URL and network outcomes are not constrained by that rule.
/// `NodeStateChanged` is deliberately rejected here because origin-only
/// provenance cannot identify the exact node that was governed; callers must
/// use [`Self::new_node_state`] for that post-condition.
pub fn new(
action: ActionKind,
target_origin: Origin,
Expand All @@ -90,23 +102,18 @@ impl VerifiedActionOutcomeEvidence {
observed_at_milliseconds: u64,
provenance: ProvenanceRecord,
) -> Result<Self, VerifiedActionOutcomeError> {
if provenance.verification_result() != VerificationResult::Verified {
return Err(VerifiedActionOutcomeError::PostConditionNotVerified);
}
if observed_at_milliseconds < dispatched_at_milliseconds {
return Err(VerifiedActionOutcomeError::PostConditionPredatesDispatch {
dispatched_at_milliseconds,
observed_at_milliseconds,
});
}
if post_condition == PostConditionKind::NodeStateChanged
&& provenance.source_origin() != &target_origin
{
return Err(VerifiedActionOutcomeError::PostConditionOriginMismatch);
validate_common_post_condition(
dispatched_at_milliseconds,
observed_at_milliseconds,
&provenance,
)?;
if post_condition == PostConditionKind::NodeStateChanged {
return Err(VerifiedActionOutcomeError::NodeStateTargetRequired);
}
Ok(Self {
action,
target_origin,
target_node: None,
intent_digest,
post_condition,
dispatched_at_milliseconds,
Expand All @@ -115,6 +122,45 @@ impl VerifiedActionOutcomeEvidence {
})
}

/// Create node-state success evidence bound to one exact governed node.
///
/// `target_node` is the node authority used by the governed action;
/// `observed_node` is the independently observed node whose post-condition
/// was verified. Both must be exactly equal across browser session, browsing
/// context, canonical origin, document epoch, and node identifier. Provenance
/// must also originate from that canonical target origin.
pub fn new_node_state(
action: ActionKind,
target_node: ObservedNodeHandle,
intent_digest: ActionIntentDigest,
dispatched_at_milliseconds: u64,
observed_at_milliseconds: u64,
observed_node: ObservedNodeHandle,
provenance: ProvenanceRecord,
) -> Result<Self, VerifiedActionOutcomeError> {
validate_common_post_condition(
dispatched_at_milliseconds,
observed_at_milliseconds,
&provenance,
)?;
if provenance.source_origin() != target_node.origin() {
return Err(VerifiedActionOutcomeError::PostConditionOriginMismatch);
}
if observed_node != target_node {
return Err(VerifiedActionOutcomeError::PostConditionNodeMismatch);
}
Ok(Self {
action,
target_origin: target_node.origin().clone(),
target_node: Some(target_node),
intent_digest,
post_condition: PostConditionKind::NodeStateChanged,
dispatched_at_milliseconds,
observed_at_milliseconds,
provenance,
})
}

/// Return the typed action whose completion was verified.
#[must_use]
pub const fn action(&self) -> ActionKind {
Expand All @@ -127,6 +173,12 @@ impl VerifiedActionOutcomeEvidence {
&self.target_origin
}

/// Return the exact governed node for node-state evidence, when applicable.
#[must_use]
pub const fn target_node(&self) -> Option<&ObservedNodeHandle> {
self.target_node.as_ref()
}

/// Return the digest of the complete canonical action intent.
#[must_use]
pub const fn intent_digest(&self) -> &ActionIntentDigest {
Expand Down Expand Up @@ -157,3 +209,20 @@ impl VerifiedActionOutcomeEvidence {
&self.provenance
}
}

fn validate_common_post_condition(
dispatched_at_milliseconds: u64,
observed_at_milliseconds: u64,
provenance: &ProvenanceRecord,
) -> Result<(), VerifiedActionOutcomeError> {
if provenance.verification_result() != VerificationResult::Verified {
return Err(VerifiedActionOutcomeError::PostConditionNotVerified);
}
if observed_at_milliseconds < dispatched_at_milliseconds {
return Err(VerifiedActionOutcomeError::PostConditionPredatesDispatch {
dispatched_at_milliseconds,
observed_at_milliseconds,
});
}
Ok(())
}
Loading
Loading