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