Repository navigation
feat(relation): refuse citation edges as state transitions #120
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 2 commits
cbd22cc
f2bc284
34c4c11
a7258b0
2cf9ed3
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 = "citation_edge" | ||
| description = "Citation and retrospective edges cannot become reverse state transitions." | ||
| 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,48 @@ | ||
| //! Fail-closed citation-edge errors. | ||
|
|
||
| use std::fmt; | ||
|
|
||
| /// A fail-closed citation-edge error. | ||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| #[non_exhaustive] | ||
| pub enum CitationEdgeError { | ||
| /// A provenance edge was treated as a state transition. | ||
| ProvenanceIsNotTransition, | ||
| /// A kind slice was empty or length-mismatched. | ||
| InvalidEdgePayload, | ||
| } | ||
|
|
||
| impl fmt::Display for CitationEdgeError { | ||
| fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| let message = match self { | ||
| Self::ProvenanceIsNotTransition => { | ||
| "citation or retrospective edges are not state transitions" | ||
| } | ||
| Self::InvalidEdgePayload => "invalid citation-edge payload", | ||
| }; | ||
| formatter.write_str(message) | ||
| } | ||
| } | ||
|
|
||
| impl std::error::Error for CitationEdgeError {} | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::CitationEdgeError; | ||
|
|
||
| #[test] | ||
| fn error_messages_are_stable() { | ||
| for (error, message) in [ | ||
| ( | ||
| CitationEdgeError::ProvenanceIsNotTransition, | ||
| "citation or retrospective edges are not state transitions", | ||
| ), | ||
| ( | ||
| CitationEdgeError::InvalidEdgePayload, | ||
| "invalid citation-edge payload", | ||
| ), | ||
| ] { | ||
| assert_eq!(error.to_string(), message); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| //! Provenance kinds that may point to the past. | ||
|
|
||
| use crate::CitationEdgeError; | ||
|
|
||
| /// Closed vocabulary of provenance edges that are not state transitions. | ||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| pub enum ProvenanceKind { | ||
| /// A citation of earlier evidence. | ||
| Citation, | ||
| /// A translation of an earlier document. | ||
| Translation, | ||
| /// A revision of an earlier document. | ||
| Revision, | ||
| /// A retrospective report about an earlier event. | ||
| RetrospectiveReport, | ||
| } | ||
|
|
||
| /// Refuse to treat a provenance edge as a forward state transition. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Always returns [`CitationEdgeError::ProvenanceIsNotTransition`]. | ||
| pub fn refuse_provenance_as_transition(_kind: ProvenanceKind) -> Result<(), CitationEdgeError> { | ||
| Err(CitationEdgeError::ProvenanceIsNotTransition) | ||
| } | ||
|
|
||
| /// Fraction of recovered provenance kinds that match known truth. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`CitationEdgeError::InvalidEdgePayload`] when either slice is | ||
| /// empty or the lengths differ. | ||
| pub fn edge_kind_recovery_rate( | ||
| truth: &[ProvenanceKind], | ||
| decided: &[ProvenanceKind], | ||
| ) -> Result<f64, CitationEdgeError> { | ||
| if truth.is_empty() || truth.len() != decided.len() { | ||
| return Err(CitationEdgeError::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) | ||
| } | ||
|
Comment on lines
+33
to
+47
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: Recovery-rate function guards divide-by-zero and length mismatch
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::{ProvenanceKind, edge_kind_recovery_rate, refuse_provenance_as_transition}; | ||
| use crate::CitationEdgeError; | ||
|
|
||
| #[test] | ||
| fn local_branches_cover_all_kinds_and_payloads() { | ||
| for kind in [ | ||
| ProvenanceKind::Citation, | ||
| ProvenanceKind::Translation, | ||
| ProvenanceKind::Revision, | ||
| ProvenanceKind::RetrospectiveReport, | ||
| ] { | ||
| assert_eq!( | ||
| refuse_provenance_as_transition(kind), | ||
| Err(CitationEdgeError::ProvenanceIsNotTransition) | ||
| ); | ||
| } | ||
| let truth = [ProvenanceKind::Citation, ProvenanceKind::Revision]; | ||
| let matched = edge_kind_recovery_rate(&truth, &truth).expect("rate"); | ||
| assert!((matched - 1.0).abs() < f64::EPSILON); | ||
| assert_eq!( | ||
| edge_kind_recovery_rate(&[], &[]), | ||
| Err(CitationEdgeError::InvalidEdgePayload) | ||
| ); | ||
| assert_eq!( | ||
| edge_kind_recovery_rate(&truth, &[]), | ||
| Err(CitationEdgeError::InvalidEdgePayload) | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #![forbid(unsafe_code)] | ||
| #![deny(missing_docs)] | ||
| #![allow(clippy::cast_precision_loss)] | ||
| //! Citation and retrospective edges cannot become reverse state transitions. | ||
| //! | ||
| //! Provenance edges may point to earlier event time. They never become | ||
| //! input-process-outcome transitions (ADR 0002/0003). | ||
|
|
||
| mod error; | ||
| mod kind; | ||
|
|
||
| /// Fail-closed citation-edge errors. | ||
| pub use error::CitationEdgeError; | ||
| /// Closed vocabulary of provenance edges that are not state transitions. | ||
| pub use kind::ProvenanceKind; | ||
| /// Fraction of recovered provenance kinds that match known truth. | ||
| pub use kind::edge_kind_recovery_rate; | ||
| /// Refuse to treat a provenance edge as a forward state transition. | ||
| pub use kind::refuse_provenance_as_transition; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| //! Integration contract for the `citation_edge` package identity. | ||
|
|
||
| #[test] | ||
| fn package_identity_is_stable() { | ||
| let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); | ||
| assert_eq!(observed, "citation_edge"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| //! Citation and retrospective edges cannot be promoted to state transitions. | ||
|
|
||
| use citation_edge::{ | ||
| CitationEdgeError, ProvenanceKind, edge_kind_recovery_rate, refuse_provenance_as_transition, | ||
| }; | ||
|
|
||
| #[test] | ||
| fn provenance_kinds_cannot_become_state_transitions() { | ||
| assert_eq!( | ||
| refuse_provenance_as_transition(ProvenanceKind::Citation), | ||
| Err(CitationEdgeError::ProvenanceIsNotTransition) | ||
| ); | ||
| assert_eq!( | ||
| refuse_provenance_as_transition(ProvenanceKind::Translation), | ||
| Err(CitationEdgeError::ProvenanceIsNotTransition) | ||
| ); | ||
| assert_eq!( | ||
| refuse_provenance_as_transition(ProvenanceKind::Revision), | ||
| Err(CitationEdgeError::ProvenanceIsNotTransition) | ||
| ); | ||
| assert_eq!( | ||
| refuse_provenance_as_transition(ProvenanceKind::RetrospectiveReport), | ||
| Err(CitationEdgeError::ProvenanceIsNotTransition) | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn recovered_kinds_match_known_truth_better_than_a_transition_collapse() { | ||
| let truth = [ | ||
| ProvenanceKind::Citation, | ||
| ProvenanceKind::Translation, | ||
| ProvenanceKind::Revision, | ||
| ]; | ||
| let recovered = truth; | ||
| let collapsed = [ | ||
| ProvenanceKind::Citation, | ||
| ProvenanceKind::Citation, | ||
| ProvenanceKind::Citation, | ||
| ]; | ||
| let recovered_rate = edge_kind_recovery_rate(&truth, &recovered).expect("recovered"); | ||
| let collapsed_rate = edge_kind_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!( | ||
| edge_kind_recovery_rate(&[], &[]), | ||
| Err(CitationEdgeError::InvalidEdgePayload) | ||
| ); | ||
| assert_eq!( | ||
| edge_kind_recovery_rate(&[ProvenanceKind::Citation], &[]), | ||
| Err(CitationEdgeError::InvalidEdgePayload) | ||
| ); | ||
| assert_eq!( | ||
| edge_kind_recovery_rate( | ||
| &[ProvenanceKind::Citation, ProvenanceKind::Revision], | ||
| &[ProvenanceKind::Citation] | ||
| ), | ||
| Err(CitationEdgeError::InvalidEdgePayload) | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # Citation edges are not state transitions (doctoring) | ||
|
|
||
| ## Scope | ||
|
|
||
| `citation_edge` keeps citation, translation, revision, and retrospective | ||
| report edges out of the forward state-transition vocabulary. Recovery is | ||
| the computed share of recovered kinds that match known truth. | ||
|
|
||
| This slice does not persist the graph, implement Allen composition, or | ||
| replace `relation_graph`. | ||
|
|
||
| ## Authority | ||
|
|
||
| ### Normative TEPP contract | ||
|
|
||
| - `docs/adr/0002-six-clock-temporal-semantics.md` — forward | ||
| state-transition and input-process-outcome edges never move backward | ||
| in event time; citation and retrospective edges may point to the past | ||
| but never become reverse state transitions. | ||
| - `docs/adr/0003-relational-event-multiple-membership.md` — typed | ||
| relations distinguish transition from provenance. | ||
|
|
||
| ### Supporting literature | ||
|
|
||
| Allen (1983) classifies interval relations; it does **not** authorize | ||
| treating a bibliographic citation as a `causes` or `intervenes_on` | ||
| transition. | ||
|
|
||
| Allen, J. F. (1983). Maintaining knowledge about temporal intervals. | ||
| *Communications of the ACM, 26*(11), 832–843. | ||
| https://doi.org/10.1145/182.358434 |
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: New crate is a documentation/gate slice, not a real transition enforcer
The
citation_edgecrate does not actually integrate withrelation_graphor persist anything.refuse_provenance_as_transitionunconditionally returns an error regardless of input, andedge_kind_recovery_rateis a simple match-fraction computation. This matches the PR's stated "kind gate only" claim boundary, so it is not a bug, but reviewers should note the ADR 0002 maturity was bumped toactive-PRon the strength of a crate that enforces nothing against the existing graph — the invariant "citation edges are not transitions" is only asserted in isolation, never wired into the code path that builds transition edges.Was this helpful? React with 👍 or 👎 to provide feedback.