Skip to main content

scema_tools/
observer.rs

1//! [`Observer`]: the only interface between the agent and a real environment.
2//!
3//! Everything above this trait reasons over a [`WorldState`] and cannot tell whether it
4//! came from a filesystem, a browser tab or an HTTP API. That is what makes one loop serve
5//! every domain, and it is why the trait is deliberately narrow: an observer *looks*. It
6//! does not act, it does not decide, and it does not summarise on the agent's behalf.
7//!
8//! ## Three obligations on every implementation
9//!
10//! 1. **Report what could not be read.** A directory that raised a permission error belongs
11//!    in [`WorldState::blind_spots`], not in a log the agent never sees. Ignorance the
12//!    observer knows about is the single most useful thing it can pass upward.
13//! 2. **Never round an unread thing to zero.** An object whose attributes could not be
14//!    recovered is [`scema_world::Provenance::Absent`] with no attributes, not an object
15//!    with zeroes.
16//! 3. **Say whether the walk was complete.** [`scema_world::Extent`] with `total: None`
17//!    when a cap or a depth limit was hit. An observer that silently truncates makes the
18//!    agent confident about a fraction of a system.
19//!
20//! A deliberate exclusion is *not* a blind spot. Skipping `target/` and `node_modules/` is
21//! a decision the observer made, not a failure it suffered, and filing it as ignorance
22//! would drown the real unreadable paths in noise.
23
24use anyhow::Result;
25use scema_world::WorldState;
26
27/// Something that can turn a locator into a world state.
28pub trait Observer {
29    /// Stable name; recorded in `WorldState::observer` and hashed into the decision record.
30    fn name(&self) -> &str;
31
32    /// One sentence for `scema observe --list`.
33    fn about(&self) -> &str;
34
35    /// Could this observer handle the locator? Cheap and syntactic — a `true` here is a
36    /// claim about the shape of the string, not a promise the target exists.
37    fn handles(&self, locator: &str) -> bool;
38
39    fn observe(&self, locator: &str) -> Result<WorldState>;
40}
41
42/// Pick the first observer that claims a locator.
43///
44/// First match rather than best match: the registry is small and ordered by the caller, and
45/// a scoring contest between observers would be a second policy layer with no way to
46/// explain itself.
47pub fn resolve<'a>(observers: &'a [&'a dyn Observer], locator: &str) -> Option<&'a dyn Observer> {
48    observers.iter().copied().find(|o| o.handles(locator))
49}