Skip to main content

chronicle/
query.rs

1//! Typed query API: method chains over the chronicle graph.
2
3use std::collections::HashSet;
4use std::ops::RangeInclusive;
5
6use petgraph::Direction;
7use petgraph::stable_graph::NodeIndex;
8use petgraph::visit::EdgeRef;
9
10use crate::graph::Chronicle;
11use crate::model::*;
12
13// ── Edge-filtered neighbor helper ──────────────────────────
14
15impl Chronicle {
16    /// Nodes connected to `nx` via edges matching `pred`, in the given direction.
17    fn neighbors_by_edge<F>(&self, nx: NodeIndex, dir: Direction, pred: F) -> Vec<NodeIndex>
18    where
19        F: Fn(&Relationship) -> bool,
20    {
21        self.graph
22            .edges_directed(nx, dir)
23            .filter(|e| pred(e.weight()))
24            .map(|e| match dir {
25                Direction::Outgoing => e.target(),
26                Direction::Incoming => e.source(),
27            })
28            .collect()
29    }
30}
31
32// ── Entry points on Chronicle ──────────────────────────────
33
34impl Chronicle {
35    /// Look up an actor by id and return a query handle for it.
36    ///
37    /// Returns `None` if the id does not exist or does not refer to an [`Actor`].
38    pub fn actor(&self, id: &str) -> Option<ActorQuery<'_>> {
39        let &nx = self.index.get(id)?;
40        match &self.graph[nx] {
41            Entity::Actor(_) => Some(ActorQuery {
42                chronicle: self,
43                nx,
44            }),
45            _ => None,
46        }
47    }
48
49    /// Look up an event by id and return a query handle for it.
50    ///
51    /// Returns `None` if the id does not exist or does not refer to an [`Event`].
52    pub fn event(&self, id: &str) -> Option<EventQuery<'_>> {
53        let &nx = self.index.get(id)?;
54        match &self.graph[nx] {
55            Entity::Event(_) => Some(EventQuery {
56                chronicle: self,
57                nx,
58            }),
59            _ => None,
60        }
61    }
62
63    /// Look up a place by id and return a query handle for it.
64    ///
65    /// Returns `None` if the id does not exist or does not refer to a [`Place`].
66    pub fn place(&self, id: &str) -> Option<PlaceQuery<'_>> {
67        let &nx = self.index.get(id)?;
68        match &self.graph[nx] {
69            Entity::Place(_) => Some(PlaceQuery {
70                chronicle: self,
71                nx,
72            }),
73            _ => None,
74        }
75    }
76
77    /// Look up a concept by id and return a query handle for it.
78    ///
79    /// Returns `None` if the id does not exist or does not refer to a [`Concept`].
80    pub fn concept(&self, id: &str) -> Option<ConceptQuery<'_>> {
81        let &nx = self.index.get(id)?;
82        match &self.graph[nx] {
83            Entity::Concept(_) => Some(ConceptQuery {
84                chronicle: self,
85                nx,
86            }),
87            _ => None,
88        }
89    }
90
91    /// All accounts whose text contains a `{entity_id}` reference to this entity.
92    pub fn mentions(&self, entity_id: &str) -> Vec<&Account> {
93        let Some(&target_nx) = self.index.get(entity_id) else {
94            return vec![];
95        };
96        let mut seen = HashSet::new();
97        self.neighbors_by_edge(target_nx, Direction::Incoming, |r| {
98            matches!(r, Relationship::Mentions)
99        })
100        .iter()
101        .filter(|&&nx| seen.insert(nx))
102        .filter_map(|&nx| match &self.graph[nx] {
103            Entity::Account(a) => Some(a),
104            _ => None,
105        })
106        .collect()
107    }
108
109    /// All accounts that reference this event (via AccountOf edges).
110    pub fn accounts_of(&self, event_id: &str) -> Vec<&Account> {
111        let Some(&target_nx) = self.index.get(event_id) else {
112            return vec![];
113        };
114        let mut seen = HashSet::new();
115        self.neighbors_by_edge(target_nx, Direction::Incoming, |r| {
116            matches!(r, Relationship::AccountOf)
117        })
118        .iter()
119        .filter(|&&nx| seen.insert(nx))
120        .filter_map(|&nx| match &self.graph[nx] {
121            Entity::Account(a) => Some(a),
122            _ => None,
123        })
124        .collect()
125    }
126
127    /// All accounts authored by a given source.
128    pub fn accounts_by(&self, source_id: &str) -> Vec<&Account> {
129        let Some(&source_nx) = self.index.get(source_id) else {
130            return vec![];
131        };
132        let mut seen = HashSet::new();
133        self.neighbors_by_edge(source_nx, Direction::Incoming, |r| {
134            matches!(r, Relationship::AuthoredBy)
135        })
136        .iter()
137        .filter(|&&nx| seen.insert(nx))
138        .filter_map(|&nx| match &self.graph[nx] {
139            Entity::Account(a) => Some(a),
140            _ => None,
141        })
142        .collect()
143    }
144}
145
146// ── ActorQuery ─────────────────────────────────────────────
147
148/// Query handle for an [`Actor`] node in the chronicle graph.
149///
150/// Provides methods to inspect the actor's data, events, interactions,
151/// and computed status at a point in time.
152pub struct ActorQuery<'a> {
153    chronicle: &'a Chronicle,
154    nx: NodeIndex,
155}
156
157impl<'a> ActorQuery<'a> {
158    /// Returns a reference to the underlying [`Actor`] data.
159    pub fn data(&self) -> &'a Actor {
160        match &self.chronicle.graph[self.nx] {
161            Entity::Actor(a) => a,
162            _ => unreachable!(),
163        }
164    }
165
166    /// All events this actor participated in.
167    pub fn events(&self) -> Vec<&'a Event> {
168        self.chronicle
169            .neighbors_by_edge(self.nx, Direction::Incoming, |r| {
170                matches!(r, Relationship::ParticipatedIn { .. })
171            })
172            .iter()
173            .filter_map(|&nx| match &self.chronicle.graph[nx] {
174                Entity::Event(e) => Some(e),
175                _ => None,
176            })
177            .collect()
178    }
179
180    /// Events filtered by time range.
181    pub fn events_during(&self, range: RangeInclusive<i32>) -> Vec<&'a Event> {
182        self.events()
183            .into_iter()
184            .filter(|e| e.time_span.start <= *range.end() && e.time_span.end >= *range.start())
185            .collect()
186    }
187
188    /// Events filtered by location.
189    pub fn events_at(&self, place_id: &str) -> Vec<&'a Event> {
190        self.events()
191            .into_iter()
192            .filter(|e| e.location.as_deref() == Some(place_id))
193            .collect()
194    }
195
196    /// All other actors this actor has co-participated in events with.
197    pub fn interactions(&self) -> InteractionResult<'a> {
198        let mut actors = Vec::new();
199        for event in self.events() {
200            for p in &event.participants {
201                if p.actor != self.data().id
202                    && let Some(q) = self.chronicle.actor(&p.actor)
203                    && !actors.iter().any(|a: &&Actor| a.id == q.data().id)
204                {
205                    actors.push(q.data());
206                }
207            }
208        }
209        InteractionResult { actors }
210    }
211
212    /// Status at a given point in time, computed from state changes.
213    pub fn status_at(&self, year: i32) -> Status {
214        status_at_impl(self.chronicle, &self.data().id, year)
215    }
216}
217
218// ── InteractionResult ──────────────────────────────────────
219
220/// The result of an actor interaction query, containing the set of
221/// co-participating actors discovered across shared events.
222pub struct InteractionResult<'a> {
223    actors: Vec<&'a Actor>,
224}
225
226impl<'a> InteractionResult<'a> {
227    /// Returns all co-participating actors as a slice.
228    pub fn all(&self) -> &[&'a Actor] {
229        &self.actors
230    }
231
232    /// Returns only actors of type [`ActorType::Character`].
233    pub fn people(&self) -> Vec<&'a Actor> {
234        self.actors
235            .iter()
236            .filter(|a| a.actor_type == ActorType::Character)
237            .copied()
238            .collect()
239    }
240
241    /// Returns only actors of type [`ActorType::Faction`].
242    pub fn factions(&self) -> Vec<&'a Actor> {
243        self.actors
244            .iter()
245            .filter(|a| a.actor_type == ActorType::Faction)
246            .copied()
247            .collect()
248    }
249}
250
251// ── EventQuery ─────────────────────────────────────────────
252
253/// Query handle for an [`Event`] node in the chronicle graph.
254///
255/// Provides methods to inspect participants, location, causal chains,
256/// consequences, and state changes.
257pub struct EventQuery<'a> {
258    chronicle: &'a Chronicle,
259    nx: NodeIndex,
260}
261
262impl<'a> EventQuery<'a> {
263    /// Returns a reference to the underlying [`Event`] data.
264    pub fn data(&self) -> &'a Event {
265        match &self.chronicle.graph[self.nx] {
266            Entity::Event(e) => e,
267            _ => unreachable!(),
268        }
269    }
270
271    /// All participants in this event.
272    pub fn participants(&self) -> Vec<&'a Participant> {
273        self.data().participants.iter().collect()
274    }
275
276    /// Participants filtered by role.
277    pub fn participants_by_role(&self, role: Role) -> Vec<&'a Participant> {
278        self.data()
279            .participants
280            .iter()
281            .filter(|p| p.role == role)
282            .collect()
283    }
284
285    /// The location of this event, if any.
286    pub fn location(&self) -> Option<&'a Place> {
287        let loc_id = self.data().location.as_ref()?;
288        let &nx = self.chronicle.index.get(loc_id.as_str())?;
289        match &self.chronicle.graph[nx] {
290            Entity::Place(p) => Some(p),
291            _ => None,
292        }
293    }
294
295    /// Direct causes of this event.
296    pub fn caused_by(&self) -> Vec<&'a Event> {
297        self.data()
298            .caused_by
299            .iter()
300            .filter_map(|id| {
301                let &nx = self.chronicle.index.get(id.as_str())?;
302                match &self.chronicle.graph[nx] {
303                    Entity::Event(e) => Some(e),
304                    _ => None,
305                }
306            })
307            .collect()
308    }
309
310    /// Full causal chain backward (BFS through caused_by).
311    pub fn causal_chain(&self) -> Vec<&'a Event> {
312        let mut chain = Vec::new();
313        let mut queue = std::collections::VecDeque::new();
314        let mut visited = HashSet::new();
315
316        for cause in &self.data().caused_by {
317            if let Some(&nx) = self.chronicle.index.get(cause.as_str()) {
318                queue.push_back(nx);
319            }
320        }
321
322        while let Some(nx) = queue.pop_front() {
323            if !visited.insert(nx) {
324                continue;
325            }
326            if let Entity::Event(e) = &self.chronicle.graph[nx] {
327                chain.push(e);
328                for cause_id in &e.caused_by {
329                    if let Some(&cause_nx) = self.chronicle.index.get(cause_id.as_str()) {
330                        queue.push_back(cause_nx);
331                    }
332                }
333            }
334        }
335        chain
336    }
337
338    /// Events caused by this one (forward consequences via CausedBy edges).
339    pub fn consequences(&self) -> Vec<&'a Event> {
340        self.chronicle
341            .neighbors_by_edge(self.nx, Direction::Incoming, |r| {
342                matches!(r, Relationship::CausedBy)
343            })
344            .iter()
345            .filter_map(|&nx| match &self.chronicle.graph[nx] {
346                Entity::Event(e) => Some(e),
347                _ => None,
348            })
349            .collect()
350    }
351
352    /// State changes produced by this event.
353    pub fn state_changes(&self) -> &[EventStateChange] {
354        &self.data().state_changes
355    }
356}
357
358// ── PlaceQuery ─────────────────────────────────────────────
359
360/// Query handle for a [`Place`] node in the chronicle graph.
361///
362/// Provides methods to inspect events at this location, actors present
363/// at a point in time, and computed status.
364pub struct PlaceQuery<'a> {
365    chronicle: &'a Chronicle,
366    nx: NodeIndex,
367}
368
369impl<'a> PlaceQuery<'a> {
370    /// Returns a reference to the underlying [`Place`] data.
371    pub fn data(&self) -> &'a Place {
372        match &self.chronicle.graph[self.nx] {
373            Entity::Place(p) => p,
374            _ => unreachable!(),
375        }
376    }
377
378    /// All events at this place.
379    pub fn events(&self) -> Vec<&'a Event> {
380        self.chronicle
381            .neighbors_by_edge(self.nx, Direction::Incoming, |r| {
382                matches!(r, Relationship::OccurredAt)
383            })
384            .iter()
385            .filter_map(|&nx| match &self.chronicle.graph[nx] {
386                Entity::Event(e) => Some(e),
387                _ => None,
388            })
389            .collect()
390    }
391
392    /// Events at this place filtered by time range.
393    pub fn events_during(&self, range: RangeInclusive<i32>) -> Vec<&'a Event> {
394        self.events()
395            .into_iter()
396            .filter(|e| e.time_span.start <= *range.end() && e.time_span.end >= *range.start())
397            .collect()
398    }
399
400    /// All actors who participated in events at this place during a given year.
401    pub fn actors_present_at(&self, year: i32) -> Vec<&'a Actor> {
402        let mut actors = Vec::new();
403        for event in self.events() {
404            if event.time_span.start <= year && event.time_span.end >= year {
405                for p in &event.participants {
406                    if let Some(q) = self.chronicle.actor(&p.actor)
407                        && !actors.iter().any(|a: &&Actor| a.id == q.data().id)
408                    {
409                        actors.push(q.data());
410                    }
411                }
412            }
413        }
414        actors
415    }
416
417    /// Status at a given point in time.
418    pub fn status_at(&self, year: i32) -> Status {
419        status_at_impl(self.chronicle, &self.data().id, year)
420    }
421}
422
423// ── ConceptQuery ───────────────────────────────────────────
424
425/// Query handle for a [`Concept`] node in the chronicle graph.
426///
427/// Provides methods to inspect the concept's data and its origin event.
428pub struct ConceptQuery<'a> {
429    chronicle: &'a Chronicle,
430    nx: NodeIndex,
431}
432
433impl<'a> ConceptQuery<'a> {
434    /// Returns a reference to the underlying [`Concept`] data.
435    pub fn data(&self) -> &'a Concept {
436        match &self.chronicle.graph[self.nx] {
437            Entity::Concept(c) => c,
438            _ => unreachable!(),
439        }
440    }
441
442    /// The event that originated this concept, if any.
443    pub fn origin_event(&self) -> Option<&'a Event> {
444        let origin_id = self.data().origin_event.as_ref()?;
445        let &nx = self.chronicle.index.get(origin_id.as_str())?;
446        match &self.chronicle.graph[nx] {
447            Entity::Event(e) => Some(e),
448            _ => None,
449        }
450    }
451}
452
453// ── Shared helpers ─────────────────────────────────────────
454
455/// Compute status at a point in time by scanning state changes.
456/// Starts from Active and applies state changes up to the queried year in chronological order.
457fn status_at_impl(chronicle: &Chronicle, entity_id: &str, year: i32) -> Status {
458    let mut changes: Vec<(i32, &Status)> = Vec::new();
459
460    for nx in chronicle.graph.node_indices() {
461        if let Entity::Event(event) = &chronicle.graph[nx]
462            && event.time_span.end <= year
463        {
464            for sc in &event.state_changes {
465                if sc.entity == entity_id
466                    && let StateChange::StatusChange(s) = &sc.change
467                {
468                    changes.push((event.time_span.end, s));
469                }
470            }
471        }
472    }
473
474    changes.sort_by_key(|(time, _)| *time);
475    changes
476        .last()
477        .map(|(_, s)| (*s).clone())
478        .unwrap_or(Status::Active)
479}