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