Skip to main content

rustev_backend_rules/
lib.rs

1//! Rustev's deterministic rules backend (spec 005).
2//!
3//! A [`RulesBackend`] answers `classify`, `proposition` and `rubric`
4//! requests with authored logits computed by a bounded, identified rules
5//! program ([`RulesProgram`], `rustev.rules/1`). Its descriptor is derived
6//! from the program, and the program's digest is the artifact identity a
7//! plan binds, so a changed rule, coefficient, lookup value, option mapping,
8//! limit or cost can never answer for a plan compiled against the old
9//! program.
10//!
11//! Setup is explicit (spec 005, 3.11): the host reads the program bytes,
12//! parses them with [`RulesProgram::parse`](rustev_contract::Document::parse),
13//! builds the backend with [`RulesBackend::new`], compiles plans against
14//! [`descriptor`](DecisionBackend::descriptor), checks them with
15//! [`RulesBackend::check_plan`] and registers the backend with a runtime.
16//! Inference performs no I/O and reads no clock.
17//!
18//! Outputs are authored heuristics over the projected inputs: not
19//! calibrated probabilities, not evidence of semantic understanding, and not
20//! independent evidence about the claims the inputs describe.
21#![forbid(unsafe_code)]
22
23mod check;
24mod eval;
25pub mod program;
26
27use rustev_contract::definition::{Determinism, Operation};
28use rustev_contract::descriptor::{
29    BackendDescriptor, InputExcess, InputLimit, OperationSupport, OutputKind,
30};
31use rustev_contract::ids::ArtifactId;
32use rustev_contract::run::{Charge, CostBound, CostModel};
33use rustev_contract::schema;
34use rustev_core::seams::{
35    AdapterFailure, AttemptCall, AttemptReport, BoxFuture, CancelAck, CancelSignal,
36    DecisionBackend, RemoteEnd,
37};
38
39pub use check::{PlanMismatch, PlanMismatchKind};
40pub use eval::Evaluated;
41pub use program::{ProgramError, ProgramErrorKind, RulesProgram};
42
43use program::{CTask, ProgramErrorKind as K};
44
45/// A validated rules program, ready to answer requests.
46#[derive(Debug, Clone)]
47pub struct RulesBackend {
48    program: RulesProgram,
49    tasks: Vec<CTask>,
50    descriptor: BackendDescriptor,
51}
52
53impl RulesBackend {
54    /// Validate `program` and derive the descriptor and artifact identity
55    /// (spec 005, 3.3, 3.9 and 3.10). The program is the only input: there
56    /// is no other behavior-affecting configuration.
57    pub fn new(program: RulesProgram) -> Result<Self, ProgramError> {
58        let tasks = program::check(&program)?;
59        let artifact = program.artifact_id().map_err(|e| ProgramError {
60            at: String::new(),
61            kind: K::Canonical(e),
62        })?;
63        let descriptor = derive_descriptor(&program, &tasks, artifact);
64        Ok(Self {
65            program,
66            tasks,
67            descriptor,
68        })
69    }
70
71    /// Parse and validate program bytes supplied by the host.
72    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProgramError> {
73        use rustev_contract::Document;
74        let program = RulesProgram::parse(bytes).map_err(|e| ProgramError {
75            at: String::new(),
76            kind: K::Document(e),
77        })?;
78        Self::new(program)
79    }
80
81    /// The program, including each task's authored interpretation (3.8).
82    pub fn program(&self) -> &RulesProgram {
83        &self.program
84    }
85
86    /// The artifact identity: the program's digest.
87    pub fn artifact(&self) -> &ArtifactId {
88        &self.descriptor.artifact
89    }
90
91    /// Answer one canonical projection synchronously, observing `cancel` at
92    /// the points of spec 005 3.13.1. Usable without any runtime.
93    pub fn evaluate(&self, projection: &[u8], cancel: &CancelSignal) -> Evaluated {
94        self.evaluate_observed(projection, &mut |_| cancel.is_raised())
95    }
96
97    pub(crate) fn evaluate_observed(
98        &self,
99        projection: &[u8],
100        cancelled: &mut dyn FnMut(eval::Point) -> bool,
101    ) -> Evaluated {
102        eval::evaluate(
103            &self.tasks,
104            self.program.limits.max_projection_bytes,
105            projection,
106            cancelled,
107        )
108    }
109
110    fn charge(&self) -> Charge {
111        Charge::Observed {
112            units: self.program.cost.units_per_call,
113        }
114    }
115}
116
117/// The descriptor, derived from the program alone (spec 005, 3.9).
118fn derive_descriptor(
119    program: &RulesProgram,
120    tasks: &[CTask],
121    artifact: ArtifactId,
122) -> BackendDescriptor {
123    let operations = [
124        Operation::Classify,
125        Operation::Proposition,
126        Operation::Rubric,
127    ]
128    .into_iter()
129    .filter_map(|op| {
130        tasks
131            .iter()
132            .filter(|t| t.operation == op)
133            .map(|t| t.options.len() as u64)
134            .max()
135            .map(|max_options| OperationSupport {
136                isolation: None,
137                screen: None,
138                operation: op,
139                output: OutputKind::Logits,
140                max_options,
141            })
142    })
143    .collect();
144    BackendDescriptor {
145        schema: schema::BACKEND.into(),
146        backend_id: program.backend_id.clone(),
147        artifact,
148        operations,
149        input_limit: InputLimit {
150            max_bytes: program.limits.max_projection_bytes,
151            on_excess: InputExcess::Refuse,
152        },
153        determinism: Determinism::Bitwise,
154    }
155}
156
157impl DecisionBackend for RulesBackend {
158    fn descriptor(&self) -> &BackendDescriptor {
159        &self.descriptor
160    }
161
162    /// Every call is bounded by the program's `units_per_call` (3.12).
163    fn cost_model(&self) -> CostModel {
164        CostModel::Bounded
165    }
166
167    fn cost_bound(&self, _projection: &[u8]) -> CostBound {
168        CostBound::Bounded {
169            max_units: self.program.cost.units_per_call,
170        }
171    }
172
173    fn infer<'a>(&'a self, call: AttemptCall<'a>) -> BoxFuture<'a, AttemptReport> {
174        Box::pin(async move {
175            let charge = self.charge();
176            match self.evaluate(call.projection, &call.cancel) {
177                Evaluated::Output(output) => AttemptReport {
178                    result: Ok(output),
179                    charge,
180                    cancel: CancelAck::NotRequested,
181                    remote: RemoteEnd::Finished,
182                },
183                Evaluated::Failed(detail) => AttemptReport {
184                    result: Err((AdapterFailure::Permanent, detail)),
185                    charge,
186                    cancel: CancelAck::NotRequested,
187                    remote: RemoteEnd::Finished,
188                },
189                // A local evaluation that observed the signal has stopped;
190                // there is no remote work (3.13.2).
191                Evaluated::Cancelled(point) => AttemptReport {
192                    result: Err((AdapterFailure::Cancelled, format!("cancelled {point}"))),
193                    charge,
194                    cancel: CancelAck::Stopped,
195                    remote: RemoteEnd::Finished,
196                },
197            }
198        })
199    }
200}
201
202#[cfg(test)]
203mod tests;