Skip to main content

lean_ctx_doc/
state.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! The derived doc state of a page (C0 2.2 `doc-states`), never stored:
4//! fresh, noted (every derived anchor that moved is covered by an open note
5//! that accounted for exactly what is observed now) or stale.
6
7use serde::{Deserialize, Serialize};
8
9/// A page's state as a reader shows it.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum DocState {
13    Fresh,
14    Noted,
15    Stale,
16}
17
18/// Why a page is not fresh.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum StateReason {
22    AnchorChanged,
23    AnchorGone,
24}
25
26/// One moved anchor: its ref and the digest observed now (`None`: gone).
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Drift {
29    #[serde(rename = "ref")]
30    pub reference: String,
31    /// `observed` in a drift entry, `digest` in a note's cover.
32    #[serde(alias = "digest")]
33    pub observed: Option<String>,
34}
35
36/// What a note accounted for: a ref at a digest (`None`: as gone).
37pub type Cover = Drift;
38
39/// One note about the page or one of its sections.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct NoteCovers {
42    pub open: bool,
43    pub covers: Vec<Cover>,
44}
45
46/// The verdict with its reason and the derived refs no open note covers.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct Verdict {
49    pub state: DocState,
50    pub reason: Option<StateReason>,
51    pub uncovered: Vec<String>,
52}
53
54/// The doc state of a page from its `anchor_state` (`fresh`, `to_verify`,
55/// `gone` or absent), the refs of its derived anchors, its `drift` and the
56/// notes about it.
57pub fn of(
58    anchor_state: Option<&str>,
59    derived: &[&str],
60    drift: &[Drift],
61    notes: &[NoteCovers],
62) -> Verdict {
63    let fresh = Verdict {
64        state: DocState::Fresh,
65        reason: None,
66        uncovered: Vec::new(),
67    };
68    if matches!(anchor_state, None | Some("fresh")) {
69        return fresh;
70    }
71    let moved: Vec<&Drift> = drift
72        .iter()
73        .filter(|d| derived.contains(&d.reference.as_str()))
74        .collect();
75    if moved.is_empty() && !drift.is_empty() {
76        // Only cited anchors moved: they never drive the state.
77        return fresh;
78    }
79    let gone = anchor_state == Some("gone") || moved.iter().any(|d| d.observed.is_none());
80    let reason = Some(if gone {
81        StateReason::AnchorGone
82    } else {
83        StateReason::AnchorChanged
84    });
85    let mut uncovered: Vec<String> = moved
86        .iter()
87        .filter(|d| {
88            !notes.iter().filter(|n| n.open).any(|n| {
89                n.covers
90                    .iter()
91                    .any(|c| c.reference == d.reference && c.observed == d.observed)
92            })
93        })
94        .map(|d| d.reference.clone())
95        .collect();
96    uncovered.sort();
97    // A writer that sent no drift detail leaves nothing to cover: stale.
98    let state = if moved.is_empty() || !uncovered.is_empty() {
99        DocState::Stale
100    } else {
101        DocState::Noted
102    };
103    Verdict {
104        state,
105        reason,
106        uncovered,
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    const D7: &str = "7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c";
115    const D5: &str = "5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e";
116    const DERIVED: &[&str] = &["file:a.rs"];
117
118    fn drift(r: &str, d: Option<&str>) -> Drift {
119        Drift {
120            reference: r.into(),
121            observed: d.map(Into::into),
122        }
123    }
124    fn note(open: bool, d: Option<&str>) -> NoteCovers {
125        NoteCovers {
126            open,
127            covers: vec![drift("file:a.rs", d)],
128        }
129    }
130    fn check(
131        name: &str,
132        st: Option<&str>,
133        dr: &[Drift],
134        notes: &[NoteCovers],
135        want: (DocState, Option<StateReason>, &[&str]),
136    ) {
137        let v = of(st, DERIVED, dr, notes);
138        assert_eq!((v.state, v.reason), (want.0, want.1), "{name}");
139        assert_eq!(v.uncovered, want.2, "{name}");
140    }
141
142    // C0 2.2 vectors (hive-v2 vectors/doc_state).
143    #[test]
144    fn doc_state_vectors() {
145        use DocState::{Fresh, Noted, Stale};
146        use StateReason::{AnchorChanged as Changed, AnchorGone as Gone};
147        let moved = [drift("file:a.rs", Some(D7))];
148        check("no_state", None, &[], &[], (Fresh, None, &[]));
149        check("fresh", Some("fresh"), &[], &[], (Fresh, None, &[]));
150        check(
151            "changed_uncovered",
152            Some("to_verify"),
153            &moved,
154            &[],
155            (Stale, Some(Changed), &["file:a.rs"]),
156        );
157        check(
158            "changed_covered",
159            Some("to_verify"),
160            &moved,
161            &[note(true, Some(D7))],
162            (Noted, Some(Changed), &[]),
163        );
164        check(
165            "covered_an_older_move",
166            Some("to_verify"),
167            &moved,
168            &[note(true, Some(D5))],
169            (Stale, Some(Changed), &["file:a.rs"]),
170        );
171        check(
172            "resolved_note_does_not_cover",
173            Some("to_verify"),
174            &moved,
175            &[note(false, Some(D7))],
176            (Stale, Some(Changed), &["file:a.rs"]),
177        );
178        check(
179            "migrated_null_cover_does_not_cover",
180            Some("to_verify"),
181            &moved,
182            &[note(true, None)],
183            (Stale, Some(Changed), &["file:a.rs"]),
184        );
185        check(
186            "gone_covered",
187            Some("gone"),
188            &[drift("file:a.rs", None)],
189            &[note(true, None)],
190            (Noted, Some(Gone), &[]),
191        );
192        check(
193            "cites_drift_only",
194            Some("to_verify"),
195            &[drift("file:docs/b.md", Some(D7))],
196            &[],
197            (Fresh, None, &[]),
198        );
199        check(
200            "state_without_drift",
201            Some("to_verify"),
202            &[],
203            &[],
204            (Stale, Some(Changed), &[]),
205        );
206    }
207}