-
Notifications
You must be signed in to change notification settings - Fork 0
feat(relation): enforce input-process-outcome event-time order #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a9ff1e9
48734bf
0da560e
87be4ad
c8b3d49
94d4ff4
d3410b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| [package] | ||
| name = "retrospective_edge" | ||
| description = "Retrospective reporting is not a transition and not a translation." | ||
| version.workspace = true | ||
| edition.workspace = true | ||
| rust-version.workspace = true | ||
| license.workspace = true | ||
| authors.workspace = true | ||
| repository.workspace = true | ||
| homepage.workspace = true | ||
| readme.workspace = true | ||
| keywords.workspace = true | ||
| categories.workspace = true | ||
| publish = false | ||
|
|
||
| [lints] | ||
| workspace = true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| //! Fail-closed retrospective-edge errors. | ||
|
|
||
| use std::fmt; | ||
|
|
||
| /// A fail-closed retrospective-edge error. | ||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| #[non_exhaustive] | ||
| pub enum RetrospectiveEdgeError { | ||
| /// A retrospective report was treated as a state transition. | ||
| RetrospectiveIsNotTransition, | ||
| /// A retrospective report was treated as a translation. | ||
| RetrospectiveIsNotTranslation, | ||
| /// A recovery slice was empty or length-mismatched. | ||
| InvalidEdgePayload, | ||
| } | ||
|
|
||
| impl fmt::Display for RetrospectiveEdgeError { | ||
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| let message = match self { | ||
| Self::RetrospectiveIsNotTransition => { | ||
| "retrospective reporting is not a state transition" | ||
| } | ||
| Self::RetrospectiveIsNotTranslation => "retrospective reporting is not a translation", | ||
| Self::InvalidEdgePayload => "invalid retrospective-edge payload", | ||
| }; | ||
| formatter.write_str(message) | ||
| } | ||
| } | ||
|
|
||
| impl std::error::Error for RetrospectiveEdgeError {} | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::RetrospectiveEdgeError; | ||
|
|
||
| #[test] | ||
| fn error_messages_are_stable() { | ||
| for (error, message) in [ | ||
| ( | ||
| RetrospectiveEdgeError::RetrospectiveIsNotTransition, | ||
| "retrospective reporting is not a state transition", | ||
| ), | ||
| ( | ||
| RetrospectiveEdgeError::RetrospectiveIsNotTranslation, | ||
| "retrospective reporting is not a translation", | ||
| ), | ||
| ( | ||
| RetrospectiveEdgeError::InvalidEdgePayload, | ||
| "invalid retrospective-edge payload", | ||
| ), | ||
| ] { | ||
| assert_eq!(error.to_string(), message); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| //! Retrospective reporting versus contemporaneous forward reporting. | ||
|
|
||
| use crate::RetrospectiveEdgeError; | ||
|
|
||
| /// Closed vocabulary of reporting edges that may point at earlier event time. | ||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| pub enum RetrospectiveKind { | ||
| /// A later report about an earlier event (provenance; may point backward). | ||
| RetrospectiveReport, | ||
| /// A contemporaneous report that is not a translation or a transition. | ||
| ForwardReport, | ||
| } | ||
|
|
||
| impl RetrospectiveKind { | ||
| /// Return the stable wire kind name. | ||
| #[must_use] | ||
| pub const fn wire_name(self) -> &'static str { | ||
| match self { | ||
| Self::RetrospectiveReport => "retrospectively_reports", | ||
| Self::ForwardReport => "forward_report", | ||
| } | ||
| } | ||
|
|
||
| /// Parse a stable wire kind name. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`RetrospectiveEdgeError::InvalidEdgePayload`] for unrecognized | ||
| /// names. | ||
| pub fn from_wire_name(name: &str) -> Result<Self, RetrospectiveEdgeError> { | ||
| match name { | ||
| "retrospectively_reports" => Ok(Self::RetrospectiveReport), | ||
| "forward_report" => Ok(Self::ForwardReport), | ||
| _ => Err(RetrospectiveEdgeError::InvalidEdgePayload), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Refuse to treat a retrospective report as a forward state transition. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`RetrospectiveEdgeError::RetrospectiveIsNotTransition`] when | ||
| /// `kind` is [`RetrospectiveKind::RetrospectiveReport`]. | ||
| pub fn refuse_retrospective_as_transition( | ||
| kind: RetrospectiveKind, | ||
| ) -> Result<(), RetrospectiveEdgeError> { | ||
| match kind { | ||
| RetrospectiveKind::RetrospectiveReport => { | ||
| Err(RetrospectiveEdgeError::RetrospectiveIsNotTransition) | ||
| } | ||
| RetrospectiveKind::ForwardReport => Ok(()), | ||
| } | ||
| } | ||
|
|
||
| /// Refuse to treat a retrospective report as a translation. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`RetrospectiveEdgeError::RetrospectiveIsNotTranslation`] when | ||
| /// `kind` is [`RetrospectiveKind::RetrospectiveReport`]. | ||
| pub fn refuse_retrospective_as_translation( | ||
| kind: RetrospectiveKind, | ||
| ) -> Result<(), RetrospectiveEdgeError> { | ||
| match kind { | ||
| RetrospectiveKind::RetrospectiveReport => { | ||
| Err(RetrospectiveEdgeError::RetrospectiveIsNotTranslation) | ||
| } | ||
| RetrospectiveKind::ForwardReport => Ok(()), | ||
| } | ||
| } | ||
|
Comment on lines
+45
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| /// Fraction of recovered reporting kinds that match known truth. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`RetrospectiveEdgeError::InvalidEdgePayload`] when either slice is | ||
| /// empty or the lengths differ. | ||
| pub fn identity_recovery_rate( | ||
| truth: &[RetrospectiveKind], | ||
| decided: &[RetrospectiveKind], | ||
| ) -> Result<f64, RetrospectiveEdgeError> { | ||
| if truth.is_empty() || truth.len() != decided.len() { | ||
| return Err(RetrospectiveEdgeError::InvalidEdgePayload); | ||
| } | ||
| let mut matches = 0_u32; | ||
| for (truth_kind, decided_kind) in truth.iter().zip(decided) { | ||
| if truth_kind == decided_kind { | ||
| matches += 1; | ||
| } | ||
| } | ||
| Ok(f64::from(matches) / truth.len() as f64) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::{ | ||
| RetrospectiveKind, identity_recovery_rate, refuse_retrospective_as_transition, | ||
| refuse_retrospective_as_translation, | ||
| }; | ||
| use crate::RetrospectiveEdgeError; | ||
|
|
||
| #[test] | ||
| fn local_branches_cover_kinds_payloads_and_wire_names() { | ||
| assert_eq!( | ||
| refuse_retrospective_as_transition(RetrospectiveKind::RetrospectiveReport), | ||
| Err(RetrospectiveEdgeError::RetrospectiveIsNotTransition) | ||
| ); | ||
| assert_eq!( | ||
| refuse_retrospective_as_translation(RetrospectiveKind::RetrospectiveReport), | ||
| Err(RetrospectiveEdgeError::RetrospectiveIsNotTranslation) | ||
| ); | ||
| refuse_retrospective_as_transition(RetrospectiveKind::ForwardReport).expect("forward"); | ||
| refuse_retrospective_as_translation(RetrospectiveKind::ForwardReport).expect("forward"); | ||
| for kind in [ | ||
| RetrospectiveKind::RetrospectiveReport, | ||
| RetrospectiveKind::ForwardReport, | ||
| ] { | ||
| assert_eq!( | ||
| RetrospectiveKind::from_wire_name(kind.wire_name()).expect("round-trip"), | ||
| kind | ||
| ); | ||
| } | ||
| assert_eq!( | ||
| RetrospectiveKind::from_wire_name("translates"), | ||
| Err(RetrospectiveEdgeError::InvalidEdgePayload) | ||
| ); | ||
| let matched = identity_recovery_rate( | ||
| &[RetrospectiveKind::RetrospectiveReport], | ||
| &[RetrospectiveKind::RetrospectiveReport], | ||
| ) | ||
| .expect("rate"); | ||
| assert!((matched - 1.0).abs() < f64::EPSILON); | ||
| assert_eq!( | ||
| identity_recovery_rate(&[], &[]), | ||
| Err(RetrospectiveEdgeError::InvalidEdgePayload) | ||
| ); | ||
| assert_eq!( | ||
| identity_recovery_rate(&[RetrospectiveKind::RetrospectiveReport], &[]), | ||
| Err(RetrospectiveEdgeError::InvalidEdgePayload) | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #![forbid(unsafe_code)] | ||
| #![deny(missing_docs)] | ||
| #![allow(clippy::cast_precision_loss)] | ||
| //! Retrospective reporting is not a transition and not a translation. | ||
| //! | ||
| //! A later report may point at earlier event time. It never becomes an | ||
| //! input-process-outcome edge or a translation (ADR 0002/0003). | ||
|
|
||
| mod error; | ||
| mod kind; | ||
|
|
||
| /// Fail-closed retrospective-edge errors. | ||
| pub use error::RetrospectiveEdgeError; | ||
| /// Closed vocabulary of reporting edges that may point at earlier event time. | ||
| pub use kind::RetrospectiveKind; | ||
| /// Fraction of recovered reporting kinds that match known truth. | ||
| pub use kind::identity_recovery_rate; | ||
| /// Refuse to treat a retrospective report as a forward state transition. | ||
| pub use kind::refuse_retrospective_as_transition; | ||
| /// Refuse to treat a retrospective report as a translation. | ||
| pub use kind::refuse_retrospective_as_translation; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| //! Integration contract for the `retrospective_edge` package identity. | ||
|
|
||
| #[test] | ||
| fn package_identity_is_stable() { | ||
| let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); | ||
| assert_eq!(observed, "retrospective_edge"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| //! Retrospective reporting is not a transition and not a translation. | ||
|
|
||
| use retrospective_edge::{ | ||
| RetrospectiveEdgeError, RetrospectiveKind, identity_recovery_rate, | ||
| refuse_retrospective_as_transition, refuse_retrospective_as_translation, | ||
| }; | ||
|
|
||
| #[test] | ||
| fn retrospective_reporting_cannot_become_a_transition_or_a_translation() { | ||
| assert_eq!( | ||
| refuse_retrospective_as_transition(RetrospectiveKind::RetrospectiveReport), | ||
| Err(RetrospectiveEdgeError::RetrospectiveIsNotTransition) | ||
| ); | ||
| assert_eq!( | ||
| refuse_retrospective_as_translation(RetrospectiveKind::RetrospectiveReport), | ||
| Err(RetrospectiveEdgeError::RetrospectiveIsNotTranslation) | ||
| ); | ||
| refuse_retrospective_as_transition(RetrospectiveKind::ForwardReport).expect("forward"); | ||
| refuse_retrospective_as_translation(RetrospectiveKind::ForwardReport).expect("forward"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn recovered_kinds_match_known_truth_better_than_a_translation_collapse() { | ||
| let truth = [ | ||
| RetrospectiveKind::RetrospectiveReport, | ||
| RetrospectiveKind::ForwardReport, | ||
| RetrospectiveKind::RetrospectiveReport, | ||
| ]; | ||
| let recovered = truth; | ||
| let collapsed = [ | ||
| RetrospectiveKind::ForwardReport, | ||
| RetrospectiveKind::ForwardReport, | ||
| RetrospectiveKind::ForwardReport, | ||
| ]; | ||
| let recovered_rate = identity_recovery_rate(&truth, &recovered).expect("recovered"); | ||
| let collapsed_rate = identity_recovery_rate(&truth, &collapsed).expect("collapsed"); | ||
| let expected = { | ||
| let mut matches = 0_u32; | ||
| for (truth_kind, decided_kind) in truth.iter().zip(recovered.iter()) { | ||
| if truth_kind == decided_kind { | ||
| matches += 1; | ||
| } | ||
| } | ||
| f64::from(matches) / f64::from(u32::try_from(truth.len()).expect("len")) | ||
| }; | ||
| assert!((recovered_rate - expected).abs() < f64::EPSILON); | ||
| assert!(recovered_rate > collapsed_rate); | ||
| } | ||
|
|
||
| #[test] | ||
| fn empty_or_mismatched_kind_payloads_fail_closed() { | ||
| assert_eq!( | ||
| identity_recovery_rate(&[], &[]), | ||
| Err(RetrospectiveEdgeError::InvalidEdgePayload) | ||
| ); | ||
| assert_eq!( | ||
| identity_recovery_rate(&[RetrospectiveKind::RetrospectiveReport], &[]), | ||
| Err(RetrospectiveEdgeError::InvalidEdgePayload) | ||
| ); | ||
| assert_eq!( | ||
| identity_recovery_rate( | ||
| &[ | ||
| RetrospectiveKind::RetrospectiveReport, | ||
| RetrospectiveKind::ForwardReport | ||
| ], | ||
| &[RetrospectiveKind::RetrospectiveReport] | ||
| ), | ||
| Err(RetrospectiveEdgeError::InvalidEdgePayload) | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | |
| | Rust workspace/quality foundation | ADR 0007 | workspace/CI/repository contract | implemented-main | | ||
| | six distinct clocks and uncertain intervals | PRD; ADR 0002 | PR #8 `temporal_core` on protected main; `system_clock` system-vs-other-clock identity on the active PR | active-PR | | ||
| | Allen relation algebra/bounded closure | ADR 0002; temporal research | PR #9 `temporal_core` path-consistency on protected main | implemented-main | | ||
| | forward-only transition subgraph | PRD; ADR 0002/0003 | `relation_graph` on protected main; `support_edge` evidential-vs-transition gate on the active PR | active-PR | | ||
| | forward-only transition subgraph | PRD; ADR 0002/0003 | `relation_graph` on protected main; `retrospective_edge` retrospective-versus-translation identity on the active PR | partial | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Traceability maturity downgraded for forward-only transition subgraph TRACEABILITY.md changes the maturity of the "forward-only transition subgraph" row from Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| | event ontology/evidence mentions | PRD; ADR 0003 | `event_core` mention/instance separation on protected main; `persistence_postgres` mention SQL implemented-main refuses mention-as-instance; event-instance SQL (#39 implemented-main) refuses inverted windows; full intelligence stack remaining | partial | | ||
| | time-varying cross-classified multiple membership | PRD; ADR 0003 | `membership_core` network on protected main; `inferred_status` inferred-versus-observed identity on the active PR; multilevel estimators remaining | partial | | ||
| | leakage-safe availability/cutoff snapshots | PRD; ADR 0002/0013 | `corpus_split` on protected main | implemented-main | | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: InvalidEdgePayload doc comment does not cover its wire-name use
error.rs documents
InvalidEdgePayloadas "A recovery slice was empty or length-mismatched." but the same variant is also returned byRetrospectiveKind::from_wire_namefor unrecognized wire names (kind.rs). The doc comment is slightly narrower than the actual usages; not a functional bug, just an incomplete description.Was this helpful? React with 👍 or 👎 to provide feedback.