Skip to main content

datagrout_panels/
lib.rs

1//! The DataGrout Smart Panel model.
2//!
3//! A Smart Panel is not a JSON blob — it is a set of Prolog facts living in a
4//! logic cell's `_panels` namespace:
5//!
6//! ```prolog
7//! panel(revenue_chart, bar_chart, my_app).
8//! panel_prop(revenue_chart, title, 'Revenue by Month').
9//! panel_source(revenue_chart, my_app, 'monthly_revenue(Month, Amt)').
10//! ```
11//!
12//! `panel/3` carries the id, the kind and the owning namespace; `panel_prop/3`
13//! is one fact per config key; `panel_source/3` names a namespace and a
14//! **Prolog goal** whose solutions are the panel's rows — one row per solution,
15//! `Month` and `Amt` as the columns. A panel with fixed rows instead carries
16//! `panel_data/2`.
17//!
18//! Which makes a panel definition queryable, composable, and versioned like any
19//! other knowledge in the cell — and means a panel is *derived*, not stored: its
20//! `panel_source` goal is re-run against the rulebase every time it is read.
21//!
22//! # Why "smart"
23//!
24//! The goal can call *rules*, not just match stored facts, so a panel over
25//! `at_risk(Deal)` shows whatever satisfies that rule at read time: change the
26//! rule and every panel built on it changes, with no panel edited and no cache
27//! to invalidate. Because the definitions are facts, an agent can publish a
28//! dashboard as an outcome of its reasoning, and `logic.query` can audit what
29//! exists. Form fields carry dependency edges, triggers and emits, and a
30//! field's goal may invoke a tool and replace the field's own value — a small
31//! dataflow graph that runs *in the cell*: on submit, DataGrout binds the
32//! fields into the panel's goal (or into the `+` inputs of a rule published
33//! with `reactor.expose`) and evaluates it under the cell's sandbox. A host
34//! chooses where to submit; it does not implement the cascade.
35//!
36//! # This crate is the model, not a renderer
37//!
38//! It parses facts into a [`Panel`] tree and stops. Rendering lives in separate
39//! crates so that one panel definition can drive several very different
40//! surfaces:
41//!
42//! | crate | surface |
43//! |---|---|
44//! | `datagrout-panels-egui` | native immediate-mode GUI |
45//! | `datagrout-panels-mcp` | MCP Apps (SEP-1865) `ui://` resources |
46//!
47//! Keeping the model renderer-free is what makes that possible, and it means a
48//! consumer that only transpiles never links a GUI toolkit.
49//!
50//! # Where panels come from
51//!
52//! This crate does not create panels and does not fetch them. A Smart Panel is
53//! created on DataGrout by calling the gateway's `smart_panel.publish` tool
54//! with an id, a kind, an owning namespace, and whatever props, rows or backing
55//! query it needs; a dashboard is published as `kind: "dashboard"` with each
56//! child naming it in `props.parent`, and a form as `kind: "form"` with a
57//! `fields` array.
58//!
59//! The resulting facts live in the `_panels` namespace of a logic cell, and a
60//! cell is scoped to one account **and one hub server** — so the panels a
61//! caller can read are those published through the server it connected to.
62//! Reading them back is a single `smart_panel.list` call, and its response is
63//! what [`Panel::all_from_list`] takes. Bring your own MCP client.
64//!
65//! # Two ways in
66//!
67//! * [`Panel::all_from_list`] — feed it a `smart_panel.list` response. The
68//!   intended path: one call, props included, children resolved.
69//! * [`Panel::all_from_facts`] — feed it raw `logic.query` rows per goal (see
70//!   [`goals`]) when you need full rows or field metadata.
71//!
72//! Nothing here fetches. See [`facts`] for the fetch-layer facts a caller has
73//! to know — undefined predicates, result paging, duplicate registrations.
74//!
75//! A hand-written list response parses like a real one, which is how to see a
76//! renderer work before you have panels of your own:
77//!
78//! ```
79//! use datagrout_panels::Panel;
80//! use serde_json::json;
81//!
82//! let panels = Panel::all_from_list(&json!({
83//!     "panels": [{
84//!         "id": "revenue", "kind": "bar_chart", "namespace": "demo",
85//!         "props": { "title": "Revenue by Month", "columns": ["Month", "Amount"] },
86//!         "data_preview": [["Jan", 12500], ["Feb", 18300], ["Mar", 21100]]
87//!     }]
88//! }));
89//!
90//! assert_eq!(panels[0].title(), "Revenue by Month");
91//! assert_eq!(panels[0].columns(), ["Month", "Amount"]);
92//! assert_eq!(panels[0].rows.len(), 3);
93//! ```
94
95#![forbid(unsafe_code)]
96
97pub mod facts;
98pub mod model;
99
100pub use facts::{goals, normalize_rows, parse_curly_term, PanelFacts};
101pub use model::{
102    prop_bool, prop_f64, prop_list, prop_str, Field, FieldEmit, FieldTrigger, Panel, PanelKind,
103    PanelSource, Props, TriggerEvent, TriggerType,
104};
105
106/// The system namespace every Smart Panel is published into.
107pub const PANELS_NAMESPACE: &str = "_panels";