Skip to main content

scema_tools/
import.rs

1//! [`ImportObserver`]: a world perceived somewhere else.
2//!
3//! The observer that makes omni's domain-agnosticism *operational* rather than merely
4//! stated. `RepoObserver` can perceive a source tree because it is written in Rust and runs
5//! in this process. It cannot perceive a running Solana bot, a set of Chainlink oracle
6//! feeds, or a DOM — those live behind a different lockfile, a Python package, and a browser
7//! respectively, and pulling any of them in here would make omni a hub of domain
8//! dependencies, which is the exact thing the workspace note forbids.
9//!
10//! The alternative that works is the one the browser extension already proved:
11//!
12//! > **The thing being observed describes itself in `scema-world`'s vocabulary, and omni
13//! > reads that.**
14//!
15//! `plugins/scema-web/src/perceive.js` emits `WorldState` JSON in 300 lines of dependency-
16//! free JavaScript and nothing above perception needed a line changed to gain a browser.
17//! There are now four producers on that contract — `RepoObserver` here, `perceive.js` in the
18//! extension, `scematica_mesh::omni` in the bot workspace, and `alchem_link.omni` in
19//! Python — and only the first of them is written in a language this crate can link.
20//!
21//! ```console
22//! $ mesh-dashboard --world | scema simulate "keep the pipeline honest" --path -
23//! $ alchem-link omni -n base > feeds.json && scema observe feeds.json
24//! ```
25//!
26//! ## The observer field is always rewritten, and that is the load-bearing part
27//!
28//! An imported world is **not** trusted to describe its own provenance — but it does not
29//! have to be. Whatever the producer called itself, [`ImportObserver`] prefixes it with
30//! `imported:`, exactly as `scema-daemon` prefixes a wire-supplied world with `client:`. A
31//! decision record can therefore never claim that a world which arrived as a file was
32//! observed locally, and a reader of that record can see in one field which it was.
33//!
34//! ## What it validates, and what it deliberately does not
35//!
36//! It validates the *shape*: the JSON has to deserialise into a `WorldState`, and a few
37//! internal-consistency rules are checked because violating them makes downstream output
38//! wrong rather than merely odd (a signal id used twice cannot be named unambiguously by
39//! `--ground`; a magnitude outside `[0,1]` would dominate a ranking by arithmetic).
40//!
41//! It does **not** validate the *claims*. A producer that reports a stale feed as `Live`, or
42//! counts a signal it did not count, is lying, and no amount of parsing catches that. The
43//! honest response is not a deeper check — it is the `imported:` prefix, which tells a
44//! reader exactly whose word this is.
45
46use std::io::Read;
47use std::path::Path;
48
49use anyhow::{anyhow, bail, Context, Result};
50use scema_world::WorldState;
51
52use crate::observer::Observer;
53
54/// Bytes read before an import gives up.
55///
56/// A world is a description of an environment, not a dump of it — the repo observer's
57/// output for this workspace is about 60 KB. Sixteen megabytes is roomy enough for anything
58/// legitimate and small enough that a producer stuck in a loop cannot exhaust memory here.
59pub const MAX_IMPORT_BYTES: u64 = 16 * 1024 * 1024;
60
61/// Reads a `WorldState` that something else produced.
62#[derive(Clone, Copy, Debug, Default)]
63pub struct ImportObserver;
64
65impl ImportObserver {
66    pub fn new() -> Self {
67        ImportObserver
68    }
69
70    /// Parse, check, and stamp a world from raw JSON.
71    ///
72    /// Split out from [`Observer::observe`] so the daemon and the MCP server can reuse the
73    /// same validation on a world that arrived over the wire rather than from a file. One
74    /// implementation of "is this a usable world", not three.
75    pub fn from_json(text: &str, source: &str) -> Result<WorldState> {
76        let mut world: WorldState = serde_json::from_str(text).with_context(|| {
77            format!("{source} is not a scema-world WorldState (see scema-world's JSON shape)")
78        })?;
79        check(&world).with_context(|| format!("{source} parsed but is not internally consistent"))?;
80        world.observer = stamp(&world.observer);
81        Ok(world)
82    }
83
84    /// Read from standard input.
85    pub fn from_stdin() -> Result<WorldState> {
86        let mut text = String::new();
87        std::io::stdin()
88            .take(MAX_IMPORT_BYTES)
89            .read_to_string(&mut text)
90            .context("reading a world from stdin")?;
91        if text.trim().is_empty() {
92            bail!(
93                "nothing arrived on stdin. A producer that printed its help text or failed \
94                 silently looks exactly like this — check its exit code."
95            );
96        }
97        ImportObserver::from_json(&text, "stdin")
98    }
99
100    /// Read from a file.
101    pub fn from_file(path: &Path) -> Result<WorldState> {
102        let meta = std::fs::metadata(path)
103            .with_context(|| format!("reading {}", path.display()))?;
104        if meta.len() > MAX_IMPORT_BYTES {
105            bail!(
106                "{} is {} bytes, over the {MAX_IMPORT_BYTES}-byte import cap. A world is a \
107                 description of an environment, not a dump of it.",
108                path.display(),
109                meta.len()
110            );
111        }
112        let text = std::fs::read_to_string(path)
113            .with_context(|| format!("reading {}", path.display()))?;
114        ImportObserver::from_json(&text, &path.display().to_string())
115    }
116}
117
118/// Prefix a producer's own observer name so a record cannot claim a local observation.
119///
120/// Idempotent: importing an already-imported world does not stack prefixes, because a
121/// pipeline that passed a world through twice would otherwise produce
122/// `imported:imported:page` and make the origin harder to read rather than easier.
123fn stamp(observer: &str) -> String {
124    let name = observer.trim();
125    if name.is_empty() {
126        // A producer that named no observer is a producer whose output cannot be attributed.
127        // `unknown` is the honest label; silently inheriting `import` would credit this crate
128        // with an observation it did not make.
129        return "imported:unknown".to_string();
130    }
131    if name.starts_with("imported:") {
132        return name.to_string();
133    }
134    format!("imported:{name}")
135}
136
137/// Internal consistency rules that make downstream output wrong when violated.
138///
139/// Kept short on purpose. Every rule here earns its place by naming a specific way the
140/// ranking, the grounding or the record would be misleading — not by being tidy.
141fn check(w: &WorldState) -> Result<()> {
142    // A duplicated signal id cannot be named unambiguously by `--ground`, and the two
143    // branches built from it would rank as two independent supports for one thing.
144    let mut ids: Vec<&str> = w.signals.iter().map(|s| s.id.as_str()).collect();
145    ids.sort_unstable();
146    let before = ids.len();
147    ids.dedup();
148    if before != ids.len() {
149        bail!("two signals share an id; `--ground` could not name either unambiguously");
150    }
151
152    let mut object_ids: Vec<&str> = w.objects.iter().map(|o| o.id.as_str()).collect();
153    object_ids.sort_unstable();
154    let before = object_ids.len();
155    object_ids.dedup();
156    if before != object_ids.len() {
157        bail!("two objects share an id");
158    }
159
160    for s in &w.signals {
161        if s.id.trim().is_empty() {
162            bail!("a signal has an empty id");
163        }
164        // A magnitude outside [0,1] would dominate a ranking through arithmetic rather than
165        // through importance, and the producer is the only place that can be fixed.
166        if !s.magnitude.is_finite() || s.magnitude < 0.0 || s.magnitude > 1.0 {
167            bail!(
168                "signal `{}` has magnitude {}, outside [0,1] — clamp it in the producer",
169                s.id,
170                s.magnitude
171            );
172        }
173        // A *counted* signal with nothing to cite is the exact laundering this workspace
174        // exists to prevent: it claims `measured: true`, so `scema-sim` will score a real
175        // expected gain from it, and nothing downstream can tell it from a real count.
176        if s.measured && s.evidence.is_empty() {
177            bail!(
178                "signal `{}` claims to be measured but cites no evidence; either cite the \
179                 count or set measured=false",
180                s.id
181            );
182        }
183    }
184
185    for f in &w.facts {
186        if !f.confidence.is_finite() || f.confidence < 0.0 || f.confidence > 1.0 {
187            bail!("fact `{} {} {}` has confidence outside [0,1]", f.subject, f.predicate, f.object);
188        }
189    }
190
191    if let Some(total) = w.extent.total {
192        if w.extent.observed > total {
193            bail!(
194                "extent claims {} observed of {} total; if the denominator is unknown it must \
195                 be null, not smaller than the numerator",
196                w.extent.observed,
197                total
198            );
199        }
200    }
201
202    if w.entity.locator.trim().is_empty() {
203        bail!("the entity has no locator; it is what a decision record cites to find this again");
204    }
205
206    Ok(())
207}
208
209impl Observer for ImportObserver {
210    fn name(&self) -> &str {
211        "import"
212    }
213
214    fn about(&self) -> &str {
215        "a WorldState produced elsewhere: `-` for stdin, or a path to a .json file"
216    }
217
218    fn handles(&self, locator: &str) -> bool {
219        let l = locator.trim();
220        l == "-" || l.eq_ignore_ascii_case("stdin") || l.to_ascii_lowercase().ends_with(".json")
221    }
222
223    fn observe(&self, locator: &str) -> Result<WorldState> {
224        let l = locator.trim();
225        if l == "-" || l.eq_ignore_ascii_case("stdin") {
226            return ImportObserver::from_stdin();
227        }
228        if !self.handles(l) {
229            return Err(anyhow!(
230                "`{l}` is not something this observer handles; it takes `-` or a path ending .json"
231            ));
232        }
233        ImportObserver::from_file(Path::new(l))
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use scema_world::{Domain, Entity, EntityKind, Extent, Polarity, Provenance, Signal};
241    use std::fs;
242
243    fn minimal() -> serde_json::Value {
244        serde_json::json!({
245            "observer": "mesh",
246            "entity": { "kind": "service", "locator": "/bot", "label": "bot" },
247            "domain": "trading",
248            "observed_at": 1_700_000_000i64,
249            "objects": [],
250            "facts": [],
251            "signals": [],
252            "extent": { "observed": 3, "total": 3, "note": "collected" },
253            "blind_spots": []
254        })
255    }
256
257    fn with_signals(signals: serde_json::Value) -> String {
258        let mut v = minimal();
259        v["signals"] = signals;
260        v.to_string()
261    }
262
263    #[test]
264    fn an_imported_world_can_never_claim_it_was_observed_here() {
265        // The whole point of the crate. `scema-daemon` makes the same rewrite for a world
266        // that arrived over the wire, for the same reason.
267        let w = ImportObserver::from_json(&minimal().to_string(), "t").unwrap();
268        assert_eq!(w.observer, "imported:mesh");
269    }
270
271    #[test]
272    fn importing_twice_does_not_stack_prefixes() {
273        // A pipeline that passed a world through two stages would otherwise produce
274        // `imported:imported:mesh`, which makes the origin harder to read, not easier.
275        let mut v = minimal();
276        v["observer"] = serde_json::json!("imported:mesh");
277        let w = ImportObserver::from_json(&v.to_string(), "t").unwrap();
278        assert_eq!(w.observer, "imported:mesh");
279    }
280
281    #[test]
282    fn a_world_with_no_observer_name_is_attributed_to_nobody_rather_than_to_us() {
283        let mut v = minimal();
284        v["observer"] = serde_json::json!("   ");
285        let w = ImportObserver::from_json(&v.to_string(), "t").unwrap();
286        assert_eq!(w.observer, "imported:unknown");
287    }
288
289    #[test]
290    fn a_counted_signal_that_cites_nothing_is_refused() {
291        // The laundering this validation exists for. `measured: true` is a claim that
292        // somebody counted something, and it is the claim `scema-sim` relies on to score a
293        // real expected gain. A producer making it with nothing to cite is producing a
294        // hallucination with a decimal point on it.
295        let text = with_signals(serde_json::json!([{
296            "id": "a", "polarity": "risk", "label": "x", "detail": "",
297            "magnitude": 0.5, "measured": true, "targets": [], "evidence": []
298        }]));
299        let err = ImportObserver::from_json(&text, "t").unwrap_err().to_string();
300        let chain = format!("{:#}", ImportObserver::from_json(&text, "t").unwrap_err());
301        assert!(chain.contains("cites no evidence"), "{err} / {chain}");
302    }
303
304    #[test]
305    fn an_estimated_signal_may_cite_nothing() {
306        // The other half: a producer that admits it guessed is behaving correctly, and
307        // `measured: false` is exactly how it says so.
308        let text = with_signals(serde_json::json!([{
309            "id": "a", "polarity": "risk", "label": "x", "detail": "",
310            "magnitude": 0.5, "measured": false, "targets": [], "evidence": []
311        }]));
312        assert!(ImportObserver::from_json(&text, "t").is_ok());
313    }
314
315    #[test]
316    fn a_magnitude_outside_the_unit_interval_is_refused_with_the_signal_named() {
317        // It would dominate a ranking by arithmetic rather than by importance, and the
318        // producer is the only place that can be fixed.
319        for bad in [1.5, -0.2] {
320            let text = with_signals(serde_json::json!([{
321                "id": "loud", "polarity": "risk", "label": "x", "detail": "",
322                "magnitude": bad, "measured": true, "targets": [], "evidence": ["counted"]
323            }]));
324            let chain = format!("{:#}", ImportObserver::from_json(&text, "t").unwrap_err());
325            assert!(chain.contains("loud"), "{chain}");
326            assert!(chain.contains("outside [0,1]"), "{chain}");
327        }
328    }
329
330    #[test]
331    fn duplicate_signal_ids_are_refused_because_ground_could_not_name_one() {
332        let sig = |id: &str| {
333            serde_json::json!({
334                "id": id, "polarity": "risk", "label": "x", "detail": "",
335                "magnitude": 0.5, "measured": true, "targets": [], "evidence": ["counted"]
336            })
337        };
338        let text = with_signals(serde_json::json!([sig("a"), sig("a")]));
339        let chain = format!("{:#}", ImportObserver::from_json(&text, "t").unwrap_err());
340        assert!(chain.contains("share an id"), "{chain}");
341    }
342
343    #[test]
344    fn an_extent_whose_numerator_exceeds_its_denominator_is_refused() {
345        // `Extent::fraction` would report over 100% observed, which reads as certainty about
346        // a world the producer only partly saw. The `None` denominator exists for exactly
347        // this case and must be used instead.
348        let mut v = minimal();
349        v["extent"] = serde_json::json!({ "observed": 9, "total": 3, "note": "?" });
350        let chain = format!("{:#}", ImportObserver::from_json(&v.to_string(), "t").unwrap_err());
351        assert!(chain.contains("not smaller than the numerator"), "{chain}");
352    }
353
354    #[test]
355    fn an_unknown_denominator_is_accepted_and_is_the_correct_way_to_say_so() {
356        let mut v = minimal();
357        v["extent"] = serde_json::json!({ "observed": 9, "total": null, "note": "capped" });
358        let w = ImportObserver::from_json(&v.to_string(), "t").unwrap();
359        assert_eq!(w.extent.fraction(), None);
360    }
361
362    #[test]
363    fn an_entity_with_no_locator_is_refused() {
364        // The locator is what a decision record cites to find this environment again. A
365        // record naming an empty string is a record nobody can re-check.
366        let mut v = minimal();
367        v["entity"]["locator"] = serde_json::json!("");
368        assert!(ImportObserver::from_json(&v.to_string(), "t").is_err());
369    }
370
371    #[test]
372    fn the_locator_grammar_is_narrow_so_repo_observer_still_wins_a_directory() {
373        // `default_observers` resolves first-match, so this observer has to be *ahead* of
374        // `RepoObserver` and must therefore claim only what it really handles. Claiming a
375        // bare path would break `scema observe .`.
376        let o = ImportObserver;
377        assert!(o.handles("-"));
378        assert!(o.handles("stdin"));
379        assert!(o.handles("mesh.json"));
380        assert!(o.handles("/tmp/World.JSON"));
381        assert!(!o.handles("."));
382        assert!(!o.handles("/some/project"));
383        assert!(!o.handles("crates/scema-tools"));
384    }
385
386    #[test]
387    fn a_file_that_is_not_json_says_what_it_should_have_been() {
388        let dir = std::env::temp_dir().join(format!("scema-import-{}", std::process::id()));
389        fs::create_dir_all(&dir).unwrap();
390        let path = dir.join("bad.json");
391        fs::write(&path, "not json").unwrap();
392        let chain = format!("{:#}", ImportObserver.observe(path.to_str().unwrap()).unwrap_err());
393        assert!(chain.contains("WorldState"), "{chain}");
394        fs::remove_dir_all(&dir).ok();
395    }
396
397    #[test]
398    fn a_real_world_round_trips_through_the_importer_unchanged_but_for_the_stamp() {
399        // The contract producers are written against: everything survives except the
400        // attribution, which is the one thing that must not.
401        let original = WorldState {
402            observer: "mesh".into(),
403            entity: Entity {
404                kind: EntityKind::Service,
405                locator: "/bot".into(),
406                label: "sniper".into(),
407            },
408            domain: Domain::Trading,
409            observed_at: 1_700_000_000,
410            objects: vec![],
411            facts: vec![],
412            signals: vec![Signal {
413                id: "veto:dqstar".into(),
414                polarity: Polarity::Risk,
415                label: "DQ* is suppressing buys".into(),
416                detail: String::new(),
417                magnitude: 0.8,
418                measured: true,
419                targets: vec!["learner.dqstar".into()],
420                evidence: vec!["counted 12 consecutive vetoes".into()],
421            }],
422            extent: Extent::complete(7, "collected"),
423            blind_spots: vec!["scematica-metrics.json: absent".into()],
424        };
425        let text = serde_json::to_string(&original).unwrap();
426        let back = ImportObserver::from_json(&text, "t").unwrap();
427
428        assert_eq!(back.observer, "imported:mesh");
429        assert_eq!(back.entity, original.entity);
430        assert_eq!(back.signals, original.signals);
431        assert_eq!(back.blind_spots, original.blind_spots);
432        assert_eq!(back.extent, original.extent);
433    }
434
435    // ── the wire contract, against what the producers actually emitted ────────
436    //
437    // Three producers emit a `WorldState` without linking `scema-world`: the bot mesh in
438    // Rust behind another lockfile, `alchem-link` in stdlib-only Python, and the browser
439    // extension in dependency-free JavaScript. Each restates this crate's validation on its
440    // own side and fails its own tests. These close the loop from the other direction.
441    //
442    // The two halves catch different things. A producer's self-check catches a bug in that
443    // producer. A fixture catches the case where both sides were changed and only one of
444    // them was right.
445
446    fn fixture(name: &str) -> String {
447        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
448            .join("fixtures")
449            .join(name);
450        std::fs::read_to_string(&path)
451            .unwrap_or_else(|e| panic!("reading {}: {e}", path.display()))
452    }
453
454    /// Every fixture parses, validates, and keeps its producer's own attribution.
455    #[test]
456    fn every_producer_fixture_imports() {
457        for (file, observer) in [
458            ("mesh-world.json", "imported:mesh"),
459            ("alchem-world.json", "imported:alchem-link"),
460            ("page-world.json", "imported:page"),
461        ] {
462            let w = ImportObserver::from_json(&fixture(file), file)
463                .unwrap_or_else(|e| panic!("{file}: {e:#}"));
464            assert_eq!(w.observer, observer, "{file}");
465            assert!(!w.entity.locator.trim().is_empty(), "{file}");
466        }
467    }
468
469    /// A world that lists what it could not read is worth more than one that does not, and
470    /// all three producers are expected to do it. A fixture with no blind spots would mean
471    /// the capture was taken against an environment with nothing hidden in it, which is not
472    /// what any of these three observe.
473    #[test]
474    fn every_producer_reports_what_it_could_not_see() {
475        for file in ["mesh-world.json", "alchem-world.json", "page-world.json"] {
476            let w = ImportObserver::from_json(&fixture(file), file).unwrap();
477            assert!(
478                !w.blind_spots.is_empty(),
479                "{file} reports perfect visibility, which no real observation has"
480            );
481        }
482    }
483
484    /// Every counted signal cites its count, in every producer.
485    ///
486    /// The property `scema-sim` depends on to score a real expected gain. This is also
487    /// enforced by `check()` above, so the assertion is redundant *today* — and it is the
488    /// redundancy that is the point: if somebody relaxes the validator, this fails and says
489    /// which producer's real output stopped being defensible.
490    #[test]
491    fn no_producer_claims_a_measurement_it_cannot_cite() {
492        for file in ["mesh-world.json", "alchem-world.json", "page-world.json"] {
493            let w = ImportObserver::from_json(&fixture(file), file).unwrap();
494            for s in &w.signals {
495                if s.measured {
496                    assert!(!s.evidence.is_empty(), "{file}: `{}` cites nothing", s.id);
497                }
498                assert!((0.0..=1.0).contains(&s.magnitude), "{file}: `{}`", s.id);
499            }
500        }
501    }
502
503    /// Provenance survives the boundary in every arm that matters.
504    ///
505    /// The mesh fixture is captured against a directory holding stale state files and
506    /// missing ones, so it carries `Stale` and `Absent`; the oracle fixture carries `Stale`
507    /// for a feed past its heartbeat. If a producer ever collapsed those into `Live`, an
508    /// agent would act on values that were true an hour ago — the single failure this whole
509    /// arrangement exists to prevent.
510    #[test]
511    fn stale_and_absent_survive_the_wire() {
512        let mesh = ImportObserver::from_json(&fixture("mesh-world.json"), "mesh").unwrap();
513        assert!(
514            mesh.objects.iter().any(|o| matches!(o.provenance, Provenance::Stale { .. })),
515            "the mesh fixture should carry at least one stale unit"
516        );
517        assert!(
518            mesh.objects.iter().any(|o| matches!(o.provenance, Provenance::Absent)),
519            "the mesh fixture should carry at least one unseen unit"
520        );
521
522        let feeds = ImportObserver::from_json(&fixture("alchem-world.json"), "alchem").unwrap();
523        assert!(
524            feeds.objects.iter().any(|o| matches!(o.provenance, Provenance::Stale { .. })),
525            "the oracle fixture should carry a feed past its own heartbeat"
526        );
527        // An absent object must carry no attributes at all. A feed that did not answer has
528        // no price, and an attribute map with a zero in it would say it reported one.
529        for o in feeds.objects.iter().filter(|o| o.provenance == Provenance::Absent) {
530            assert!(o.attrs.is_empty(), "an unread feed must carry no values: {}", o.id);
531        }
532    }
533
534    /// A world from a page does not leak the query string into the record.
535    ///
536    /// `page-world.json` is captured from a URL carrying `?sid=SECRET`. The locator is
537    /// hashed into a decision record that outlives the tab, and query strings routinely
538    /// carry session tokens. The extension's own `test/wire.test.js` pins this against a
539    /// live daemon; this pins it against the bytes that actually crossed.
540    #[test]
541    fn a_perceived_page_carries_no_query_string() {
542        let w = ImportObserver::from_json(&fixture("page-world.json"), "page").unwrap();
543        assert!(!w.entity.locator.contains('?'), "{}", w.entity.locator);
544        assert!(!w.entity.locator.contains("SECRET"), "{}", w.entity.locator);
545    }
546
547    /// Each producer describes a different kind of world, and the domain reflects it.
548    ///
549    /// `Domain` exists so a specialist can decline rather than pretend. The bot mesh is a
550    /// trading world and the Deep Q* evaluator recognises it (and then declines for want of
551    /// a checkpoint, which is a different and more useful answer). An oracle set and a web
552    /// page are `Unknown`, and the evaluator declines outright.
553    #[test]
554    fn the_domain_lets_a_specialist_decline_correctly() {
555        let mesh = ImportObserver::from_json(&fixture("mesh-world.json"), "mesh").unwrap();
556        assert_eq!(mesh.domain, scema_world::Domain::Trading);
557
558        for file in ["alchem-world.json", "page-world.json"] {
559            let w = ImportObserver::from_json(&fixture(file), file).unwrap();
560            assert_eq!(w.domain, scema_world::Domain::Unknown, "{file}");
561        }
562    }
563
564}