Skip to main content

nibli_session/
lib.rs

1//! The shared session core — the ONE compile/assert/query chain every runtime
2//! surface wraps with only boundary conversion.
3//!
4//! Before this crate, the compile chain (`nibli_kr::parse_checked` →
5//! `nibli_semantics::compile_from_ast` → `nibli_reason::transform_compute_nodes`)
6//! plus the compute-predicate registry and the assert/query wrappers were
7//! hand-mirrored across nibli-engine (native), nibli-pipeline (WASM component),
8//! nibli-wasm (wasm-bindgen), nibli-ui (Dioxus), and nibli-verify's battery —
9//! the pipeline's copy literally commented "the mirror of nibli-engine's
10//! compile_text, so native and WASM agree". [`CoreSession`] is that agreement
11//! BY CONSTRUCTION.
12//!
13//! What stays surface-side, deliberately:
14//! - the ERROR BOUNDARY (this crate speaks canonical [`NibliError`]; the
15//!   pipeline converts to its WIT twin, nibli-wasm flattens to `String`);
16//! - verdict/proof SERIALIZATION (WIT records, JSON, rendered text);
17//! - the LINT policy (`nibli_kr::lint` — stdout `[Note:]` echoes vs UI note
18//!   data vs none) and env reads (`NIBLI_QUIET`/`NIBLI_STRICT` are wasip2/host
19//!   concerns; the browser has no process env);
20//! - STORE write-through (nibli-engine's durable registry mints its own ids
21//!   and reaches the KB through [`CoreSession::kb`]);
22//! - compute-dispatch WIRING (the pipeline bridges to its WIT host import,
23//!   the engine offers an opt-in TCP client, the browser leaves external
24//!   compute unregistered) — the [`CoreSession::set_compute_dispatch`]
25//!   passthrough is the seam.
26//!
27//! nibli-formalize's gates intentionally do NOT use this crate: they stop
28//! before compute-marking (the translator never needs `ComputeNode`s), keep
29//! the AST for the render round-trip gate, and carry their own `GateError`
30//! taxonomy (see nibli-formalize/src/gates.rs).
31
32use std::collections::HashSet;
33
34use nibli_types::error::NibliError;
35use nibli_types::logic::{
36    AggregateOp, FactSummary, LogicBuffer, LogicalTerm, ProofTrace, QueryResult, WitnessBinding,
37};
38
39/// Compile KR text WITHOUT compute-marking: parse + semantic compile only.
40/// For consumers that deliberately stop before `transform_compute_nodes`
41/// (display paths; nibli-formalize's gates mirror this shape independently).
42pub fn compile_unmarked(text: &str) -> Result<LogicBuffer, NibliError> {
43    let ast = nibli_kr::parse_checked(text)?;
44    nibli_semantics::compile_from_ast(ast)
45}
46
47/// THE compile chain: parse + semantic compile + compute-marking against the
48/// given predicate set. Free-fn form for per-call-set users (nibli-ui builds
49/// its set fresh each query); [`CoreSession::compile_text`] is the
50/// session-owned form.
51pub fn compile_text(
52    text: &str,
53    compute_predicates: &HashSet<String>,
54) -> Result<LogicBuffer, NibliError> {
55    let mut buf = compile_unmarked(text)?;
56    nibli_reason::transform_compute_nodes(&mut buf, compute_predicates);
57    Ok(buf)
58}
59
60/// The shared session: a [`nibli_reason::KnowledgeBase`] + the compute-predicate
61/// registry, with the compile/assert/query verbs every surface previously
62/// hand-mirrored. No env reads, no linting, no persistence — those are
63/// per-surface boundary policy (see the module doc).
64pub struct CoreSession {
65    kb: nibli_reason::KnowledgeBase,
66    compute_predicates: HashSet<String>,
67}
68
69impl Default for CoreSession {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl CoreSession {
76    /// A fresh in-memory session seeded with the built-in arithmetic compute
77    /// predicates (`nibli_reason::default_compute_predicates`).
78    pub fn new() -> Self {
79        Self::with_kb(nibli_reason::KnowledgeBase::new())
80    }
81
82    /// Wrap an already-constructed KB (e.g. one built with a persistent
83    /// write-through fact store via `KnowledgeBase::with_store`).
84    pub fn with_kb(kb: nibli_reason::KnowledgeBase) -> Self {
85        CoreSession {
86            kb,
87            compute_predicates: nibli_reason::default_compute_predicates(),
88        }
89    }
90
91    /// The underlying KB, for surface-specific extras (cancel flags, predicate
92    /// tracing, contradiction scans, store replay via `assert_fact_with_id`).
93    pub fn kb(&self) -> &nibli_reason::KnowledgeBase {
94        &self.kb
95    }
96
97    /// The current compute-predicate set (the marking input).
98    pub fn compute_predicates(&self) -> &HashSet<String> {
99        &self.compute_predicates
100    }
101
102    /// Register a predicate name for external compute dispatch.
103    pub fn register_compute_predicate(&mut self, name: String) {
104        self.compute_predicates.insert(name);
105    }
106
107    /// Register this session's external compute dispatch (per-instance; see
108    /// `nibli_reason::KnowledgeBase::set_compute_dispatch` for the trust
109    /// boundary). Without it, external predicates error; built-in arithmetic
110    /// still resolves in-engine.
111    pub fn set_compute_dispatch(
112        &self,
113        eval: fn(&str, &[LogicalTerm]) -> Result<bool, String>,
114        batch_eval: fn(&[nibli_reason::ComputeRequest]) -> Vec<Result<bool, String>>,
115    ) {
116        self.kb.set_compute_dispatch(eval, batch_eval);
117    }
118
119    /// Engine stdout diagnostics (`[Rule]`/`[Skolem]`/`[Constraint]`).
120    /// Default OFF — a silent library; surfaces opt in.
121    pub fn set_verbose(&self, verbose: bool) {
122        self.kb.set_verbose(verbose);
123    }
124
125    /// STRICT MODE (default off — permissive warn-and-insert).
126    pub fn set_strict(&self, strict: bool) {
127        self.kb.set_strict(strict);
128    }
129
130    /// EXISTENTIAL-IMPORT MODE (default ON — the v0.1 xorlo behavior). OFF gives
131    /// the clean-core `some` = plain ∃ profile (no presupposition witnesses).
132    pub fn set_existential_import(&self, on: bool) {
133        self.kb.set_existential_import(on);
134    }
135
136    /// STRATUM-ORDERED MATERIALISATION (default ON). OFF sends every
137    /// negation-as-failure check back through backward chaining.
138    pub fn set_materialization(&self, on: bool) {
139        self.kb.set_materialization(on);
140    }
141
142    /// What the last query's saturation covered: `(complete, [(relation, why not)])`.
143    /// Empty until a query has run and after any KB mutation.
144    pub fn materialization_report(&self) -> (Vec<String>, Vec<(String, String)>) {
145        self.kb.materialization_report()
146    }
147
148    /// THE compile chain against this session's compute-predicate set.
149    pub fn compile_text(&self, text: &str) -> Result<LogicBuffer, NibliError> {
150        compile_text(text, &self.compute_predicates)
151    }
152
153    /// Compile KR text and assert it, splitting a multi-statement input into
154    /// one INDEPENDENT fact per root (`split_roots` — connectives compile to a
155    /// single root and stay one compound fact). The full input text is each
156    /// root's label. Returns one `(id, compiled-sub-buffer)` pair per root so
157    /// a persisting caller can store the FACT itself and replay it
158    /// recompile-free; callers that only need ids map the pairs down.
159    pub fn assert_text(&self, text: &str) -> Result<Vec<(u64, LogicBuffer)>, NibliError> {
160        let buf = self.compile_text(text)?;
161        let mut out = Vec::new();
162        for sub in buf.split_roots() {
163            let id = self.kb.assert_fact(sub.clone(), text.to_string())?;
164            out.push((id, sub));
165        }
166        Ok(out)
167    }
168
169    /// Assert a fact directly by relation name and arguments, bypassing text
170    /// parsing, under an optional caller-chosen id (store replay). The label
171    /// is `":assert {relation}"`. Event-decomposes to the SAME shape a surface
172    /// assertion produces, so the injected fact is matched by surface text
173    /// queries (not just raw-FOL / same-shape direct queries). Identity stays
174    /// flat; arity follows the injected-arity policy (fail-closed) — see
175    /// `nibli_semantics::compile_injected_fact`.
176    pub fn assert_fact_direct(
177        &self,
178        relation: &str,
179        args: &[LogicalTerm],
180        id: Option<u64>,
181    ) -> Result<u64, NibliError> {
182        let label = format!(":assert {}", relation);
183        let buf = nibli_semantics::compile_injected_fact(relation, args)?;
184        match id {
185            Some(i) => {
186                // The assert is the reasoning stage (buffer already past
187                // nibli-semantics); nibli-reason's `assert_fact_with_id`
188                // returns a String, so wrap as Reasoning.
189                self.kb
190                    .assert_fact_with_id(buf, label, i)
191                    .map_err(NibliError::Reasoning)?;
192                Ok(i)
193            }
194            None => self.kb.assert_fact(buf, label),
195        }
196    }
197
198    /// Compile a KR query and run the entailment check.
199    pub fn query_text(&self, text: &str) -> Result<QueryResult, NibliError> {
200        let buf = self.compile_text(text)?;
201        self.kb.query_entailment(buf)
202    }
203
204    /// Compile a KR query, run the entailment check, and return the typed
205    /// result with the canonical wire [`ProofTrace`].
206    pub fn query_text_with_proof(
207        &self,
208        text: &str,
209    ) -> Result<(QueryResult, ProofTrace), NibliError> {
210        let buf = self.compile_text(text)?;
211        self.kb.query_entailment_with_proof(buf)
212    }
213
214    /// Compile a KR query and extract all satisfying witness binding sets.
215    pub fn query_find_text(&self, text: &str) -> Result<Vec<Vec<WitnessBinding>>, NibliError> {
216        let buf = self.compile_text(text)?;
217        self.kb.query_find(buf)
218    }
219
220    /// Count the distinct witness binding sets satisfying a KR query.
221    pub fn count_witnesses_text(&self, text: &str) -> Result<usize, NibliError> {
222        let buf = self.compile_text(text)?;
223        self.kb.count_witnesses(buf)
224    }
225
226    /// Aggregate the numeric values bound to `variable` across all witness
227    /// binding sets of a KR query. `Ok(None)` when no numeric witnesses exist.
228    pub fn aggregate_text(
229        &self,
230        text: &str,
231        variable: &str,
232        op: AggregateOp,
233    ) -> Result<Option<f64>, NibliError> {
234        let buf = self.compile_text(text)?;
235        self.kb.aggregate(buf, variable, op)
236    }
237
238    /// Retract a fact by id and rebuild derived state (KB only — durable
239    /// tombstones are the persisting surface's concern).
240    pub fn retract_fact(&self, id: u64) -> Result<(), NibliError> {
241        self.kb.retract_fact(id)
242    }
243
244    /// Reset the KB, clearing all facts and rules.
245    pub fn reset(&self) -> Result<(), NibliError> {
246        self.kb.reset()
247    }
248
249    /// List all active (non-retracted) facts with their ids and labels.
250    pub fn list_facts(&self) -> Result<Vec<FactSummary>, NibliError> {
251        self.kb.list_facts()
252    }
253}