···2233pub mod collections;
44pub mod mortality;
55+pub mod slice_tricks;
66+77+use std::collections::BTreeMap;
88+99+struct Span<T: Ord> {
1010+ gap_before: bool,
1111+ things: BTreeMap<T, bool>, // gap after
1212+}
1313+1414+enum SpanLen {
1515+ /// all collections are known
1616+ Exactly(usize),
1717+ /// at least one gap exists
1818+ AtLeast(usize),
1919+}
2020+2121+impl<T: Ord> Span<T> {
2222+ fn empty() -> Self {
2323+ Self {
2424+ gap_before: false,
2525+ things: Default::default(),
2626+ }
2727+ }
2828+ fn len(&self) -> SpanLen {
2929+ let known_len = self.things.len();
3030+ if self.gap_before || self.things.values().any(|gap_after| *gap_after) {
3131+ SpanLen::AtLeast(known_len)
3232+ } else {
3333+ SpanLen::Exactly(known_len)
3434+ }
3535+ }
3636+ /// check if any collections are present
3737+ ///
3838+ /// None if we can't be sure (we only have a gap)
3939+ fn is_empty(&self) -> Option<bool> {
4040+ match self.len() {
4141+ SpanLen::AtLeast(0) => None,
4242+ SpanLen::Exactly(0) => Some(true),
4343+ _ => Some(false),
4444+ }
4545+ }
4646+ /// the span has *no* gaps
4747+ fn is_complete(&self) -> bool {
4848+ matches!(self.len(), SpanLen::Exactly(_))
4949+ }
5050+ fn contains(&self, k: &T) -> Option<bool> {
5151+ if self.things.contains_key(k) {
5252+ return Some(true);
5353+ }
5454+ // try to gap_after from the key before this one, if present
5555+ // (when btree_cursors land we can do nicer with that)
5656+ let falls_in_gap = self
5757+ .things
5858+ .range(..k) // exclusive range: all keys lex-before us
5959+ .next_back() // take the closest previous one
6060+ .map(|(_, gap_after)| gap_after) // if it existed, find out if it had a gap after
6161+ .unwrap_or(&self.gap_before); // no before-key: span starts with gap?
6262+6363+ if *falls_in_gap { None } else { Some(false) }
6464+ }
6565+ /// definitive answer about whether it's *possible* for `k` to be in span
6666+ ///
6767+ /// key exist -> true
6868+ /// key falls in a gap -> true (it's possible!)
6969+ /// key falls after a key without a gap after -> false (not possible!)
7070+ fn may_contain(&self, k: &T) -> bool {
7171+ self.contains(k).unwrap_or(true)
7272+ }
7373+}
+311
src/mst/slice_tricks.rs
···11+//! glean more from a `sync.getRecord` slice than we have any right to
22+33+use super::Span;
44+use jacquard_common::types::string::Nsid;
55+use repo_stream::{MemCar, Output as WalkOutput, WalkItem};
66+use std::cmp::Ordering;
77+use std::collections::BTreeSet;
88+99+#[derive(Debug, thiserror::Error)]
1010+pub enum MstSliceTricksError {
1111+ #[error("repo-stream WalkError: {0}")]
1212+ WalkError(#[from] repo_stream::WalkError),
1313+ #[error("bad repo path: {0}")]
1414+ BadPath(String),
1515+}
1616+1717+type Result<T> = std::result::Result<T, MstSliceTricksError>;
1818+1919+/// quick hack wrapper to make collections sort the way they do in-MST
2020+///
2121+/// mst keys are `<collection>/<rkey>` and `/` is lex-after `.`, so
2222+/// - `sh.tangled.issue.comment/<any rkey>` comes before
2323+/// - `sh.tangled.issue/<any rkey>`
2424+///
2525+/// there is probably a nice way to implement PartialOrd, but... we're just
2626+/// going to tack a `/` on the end and call it a day
2727+#[derive(Debug, PartialEq, PartialOrd, Eq, Ord)]
2828+struct TerminatedNsid(String);
2929+3030+impl<'a> From<&Nsid<'a>> for TerminatedNsid {
3131+ fn from(nsid: &Nsid<'a>) -> TerminatedNsid {
3232+ let mut s = nsid.to_string();
3333+ s.push('/');
3434+ TerminatedNsid(s)
3535+ }
3636+}
3737+3838+impl From<&TerminatedNsid> for Nsid<'static> {
3939+ /// go back to jacquard typed (unchecked)
4040+ ///
4141+ /// panics if missing the '/' suffix or if the nsid got messed up
4242+ fn from(TerminatedNsid(s): &TerminatedNsid) -> Nsid<'static> {
4343+ let unslashed = s
4444+ .strip_suffix('/')
4545+ .expect("BUG: TerminatedNsid without trailing slash");
4646+ Nsid::from(unslashed.to_string())
4747+ }
4848+}
4949+5050+/// represent the collections across a whole, possibly sparse, repo
5151+type CollectionSpan = Span<TerminatedNsid>;
5252+5353+impl CollectionSpan {
5454+ // fn contains_nsid(&self, collection: &Nsid<'_>) -> Option<bool> {
5555+ // self.contains((&collection).into())
5656+ // }
5757+ // fn may_contain_nsid(&self, collection: &Nsid<'_>) -> bool {
5858+ // self.may_contain(&collection.clone().into())
5959+ // }
6060+ /// get a list of NSIDs if the span has no gaps
6161+ fn complete(&self) -> Option<Vec<Nsid<'static>>> {
6262+ self.is_complete()
6363+ .then(|| self.things.keys().map(Into::into).collect())
6464+ }
6565+ /// whether it's possible that this span covers some NSIDs
6666+ ///
6767+ /// each NSID from the set must either be in span, or in a gap of it
6868+ fn could_cover(&self, collections: &BTreeSet<Nsid<'_>>) -> bool {
6969+ let mut candidates = collections.iter().map(Into::<TerminatedNsid>::into);
7070+ let Some(mut candidate) = candidates.next() else {
7171+ return true; // empty set can always be covered, even by a zero-gap
7272+ };
7373+7474+ let mut in_gap = self.gap_before;
7575+ let mut spans = self.things.iter();
7676+ let Some((mut next_key, mut gap_after)) = spans.next() else {
7777+ return in_gap; // one big gap => covers all, else we span nothing
7878+ };
7979+8080+ // walk the spans and collections together (both are sorted) to check
8181+ // each collection against span keys and gaps
8282+ loop {
8383+ match candidate.cmp(next_key) {
8484+ Ordering::Less if !in_gap => return false,
8585+ Ordering::Less => {
8686+ candidate = match candidates.next() {
8787+ Some(c) => c,
8888+ None => return true,
8989+ }
9090+ }
9191+ Ordering::Equal => {
9292+ in_gap = *gap_after;
9393+ (next_key, gap_after) = match spans.next() {
9494+ Some(n) => n,
9595+ None => return candidates.next().is_none(),
9696+ };
9797+ candidate = match candidates.next() {
9898+ Some(c) => c,
9999+ None => return true,
100100+ }
101101+ }
102102+ Ordering::Greater => {
103103+ in_gap = *gap_after;
104104+ (next_key, gap_after) = match spans.next() {
105105+ Some(n) => n,
106106+ None => return *gap_after, // trailing gap accepts all greater candidates
107107+ }
108108+ }
109109+ }
110110+ }
111111+ }
112112+}
113113+114114+#[cfg(test)]
115115+mod tests {
116116+ use super::*;
117117+ use jacquard_common::types::string::Nsid;
118118+ use std::collections::BTreeSet;
119119+120120+ fn make_span(gap_before: bool, things: &[(&str, bool)]) -> CollectionSpan {
121121+ CollectionSpan {
122122+ gap_before,
123123+ things: things
124124+ .iter()
125125+ .map(|(k, v)| (TerminatedNsid(format!("{k}/")), *v))
126126+ .collect(),
127127+ }
128128+ }
129129+130130+ fn nsids(names: &[&str]) -> BTreeSet<Nsid<'static>> {
131131+ names.iter().map(|s| Nsid::from(s.to_string())).collect()
132132+ }
133133+134134+ // --- empty query set --------------------------------------------------
135135+136136+ #[test]
137137+ fn empty_set_always_covered() {
138138+ // even a zero-gap span covers the empty set
139139+ assert!(make_span(false, &[]).could_cover(&nsids(&[])));
140140+ assert!(make_span(true, &[("a.b.c", false)]).could_cover(&nsids(&[])));
141141+ }
142142+143143+ // --- empty span -------------------------------------------------------
144144+145145+ #[test]
146146+ fn one_big_gap_covers_anything() {
147147+ // gap_before=true with no known keys = "we know nothing, anything is possible"
148148+ let s = make_span(true, &[]);
149149+ assert!(s.could_cover(&nsids(&["a.b.c"])));
150150+ assert!(s.could_cover(&nsids(&["a.b.c", "a.b.d"])));
151151+ }
152152+153153+ #[test]
154154+ fn empty_span_no_gap_covers_nothing() {
155155+ let s = make_span(false, &[]);
156156+ assert!(!s.could_cover(&nsids(&["a.b.c"])));
157157+ }
158158+159159+ // --- single known key -------------------------------------------------
160160+161161+ #[test]
162162+ fn exact_match() {
163163+ let s = make_span(false, &[("a.b.c", false)]);
164164+ assert!(s.could_cover(&nsids(&["a.b.c"])));
165165+ }
166166+167167+ #[test]
168168+ fn gap_before_covers_collection_lex_before_first_key() {
169169+ let s = make_span(true, &[("a.b.c", false)]);
170170+ assert!(s.could_cover(&nsids(&["a.b.a"]))); // "a.b.a" < "a.b.c"
171171+ }
172172+173173+ #[test]
174174+ fn no_gap_before_rejects_collection_lex_before_first_key() {
175175+ let s = make_span(false, &[("a.b.c", false)]);
176176+ assert!(!s.could_cover(&nsids(&["a.b.a"])));
177177+ }
178178+179179+ #[test]
180180+ fn no_gap_after_rejects_collection_lex_after_last_key() {
181181+ let s = make_span(false, &[("a.b.c", false)]);
182182+ assert!(!s.could_cover(&nsids(&["a.b.d"])));
183183+ }
184184+185185+ #[test]
186186+ fn gap_after_last_key_covers_trailing_collection() {
187187+ // Greater branch exhausting spans returns *gap_after, so the trailing
188188+ // gap is correctly consulted.
189189+ let s = make_span(false, &[("a.b.c", true)]);
190190+ assert!(s.could_cover(&nsids(&["a.b.d"])));
191191+ }
192192+193193+ // NOTE: bug — Equal on the last span key returns candidates.next().is_none(),
194194+ // ignoring gap_after. So a trailing candidate after an exact match is rejected
195195+ // even when gap_after=true.
196196+ #[test]
197197+ fn gap_after_last_matched_key_does_not_cover_remaining_candidates() {
198198+ let s = make_span(false, &[("a.b.c", true)]);
199199+ assert!(!s.could_cover(&nsids(&["a.b.c", "a.b.d"])));
200200+ }
201201+202202+ // --- gap between two keys ---------------------------------------------
203203+204204+ #[test]
205205+ fn gap_between_keys_covers_middle_collection() {
206206+ let s = make_span(false, &[("a.b.a", true), ("a.b.c", false)]);
207207+ assert!(s.could_cover(&nsids(&["a.b.b"])));
208208+ }
209209+210210+ #[test]
211211+ fn no_gap_between_keys_rejects_middle_collection() {
212212+ let s = make_span(false, &[("a.b.a", false), ("a.b.c", false)]);
213213+ assert!(!s.could_cover(&nsids(&["a.b.b"])));
214214+ }
215215+216216+ // --- multiple collections in query set --------------------------------
217217+218218+ // NOTE: bug — Equal advances the span but not the candidate. After matching
219219+ // "a.b.a/", the same candidate is compared to "a.b.b/" → Less with in_gap=false
220220+ // → false. Consecutive exact matches always fail when there are more span keys.
221221+ #[test]
222222+ fn consecutive_exact_matches_return_false() {
223223+ let s = make_span(
224224+ false,
225225+ &[("a.b.a", false), ("a.b.b", false), ("a.b.c", false)],
226226+ );
227227+ assert!(s.could_cover(&nsids(&["a.b.a", "a.b.b", "a.b.c"])));
228228+ }
229229+230230+ #[test]
231231+ fn subset_of_exact_matches_returns_false() {
232232+ // same root cause: after matching "a.b.a/", candidate stays "a.b.a/" and
233233+ // compares Less to "a.b.b/" with no gap → false, even though "a.b.c" would match
234234+ let s = make_span(
235235+ false,
236236+ &[("a.b.a", false), ("a.b.b", false), ("a.b.c", false)],
237237+ );
238238+ assert!(s.could_cover(&nsids(&["a.b.a", "a.b.c"]))); // skip "a.b.b"
239239+ }
240240+241241+ #[test]
242242+ fn one_missing_key_no_gap_rejects() {
243243+ let s = make_span(false, &[("a.b.a", false), ("a.b.c", false)]);
244244+ assert!(!s.could_cover(&nsids(&["a.b.a", "a.b.b", "a.b.c"])));
245245+ }
246246+247247+ // a mix: some known, one in a gap
248248+ #[test]
249249+ fn known_key_plus_collection_in_adjacent_gap() {
250250+ let s = make_span(false, &[("a.b.a", true), ("a.b.c", false)]);
251251+ assert!(s.could_cover(&nsids(&["a.b.a", "a.b.b", "a.b.c"])));
252252+ }
253253+254254+ // --- TerminatedNsid ordering ('.' < '/') ------------------------------
255255+256256+ // sub-namespaces sort BEFORE their parent in MST order because
257257+ // "a.b.c.d/" < "a.b.c/" (at the branch point, '.' = 46 < '/' = 47)
258258+ #[test]
259259+ fn sub_namespace_sorts_before_parent_no_gap_before() {
260260+ // span knows about "a.b.c" but not "a.b.c.d"
261261+ // "a.b.c.d" is lex-before "a.b.c" in MST order, falls before first key
262262+ let s = make_span(false, &[("a.b.c", false)]);
263263+ assert!(!s.could_cover(&nsids(&["a.b.c.d"])));
264264+ }
265265+266266+ #[test]
267267+ fn sub_namespace_covered_by_gap_before() {
268268+ let s = make_span(true, &[("a.b.c", false)]);
269269+ assert!(s.could_cover(&nsids(&["a.b.c.d"])));
270270+ }
271271+}
272272+273273+fn span_from_slice(car: &mut MemCar) -> Result<CollectionSpan> {
274274+ let mut prev_gap = false;
275275+ let mut prev_collection = None;
276276+277277+ let mut span = CollectionSpan::empty();
278278+279279+ while let Some(item) = car.next()? {
280280+ match item {
281281+ WalkItem::MissingSubtree { .. } => {
282282+ prev_gap = true;
283283+ }
284284+ WalkItem::Record(WalkOutput { key, .. }) | WalkItem::MissingRecord { key, .. } => {
285285+ let collection: Nsid<'_> = key
286286+ .parse()
287287+ .map_err(|e| MstSliceTricksError::BadPath(format!("nsid parse: {e}")))?;
288288+ let terminated = (&collection).into();
289289+290290+ if let Some(prev) = prev_collection {
291291+ // last-from-collection wins setting gap_after
292292+ span.things.insert(prev, prev_gap);
293293+ } else {
294294+ span.gap_before = prev_gap;
295295+ }
296296+297297+ prev_collection = Some(terminated);
298298+ }
299299+ WalkItem::Node { .. } => unreachable!("repostream mem::next doesn't output nodes"),
300300+ }
301301+ }
302302+303303+ if let Some(prev) = prev_collection {
304304+ // last-from-collection wins setting gap_after
305305+ span.things.insert(prev, prev_gap);
306306+ } else {
307307+ span.gap_before = prev_gap;
308308+ }
309309+310310+ Ok(span)
311311+}