Skip to main content

Evaluator

Trait Evaluator 

Source
pub trait Evaluator: Send + Sync {
    // Required methods
    fn compile(&self, predicate: &Value) -> CompileOutcome;
    fn evaluate(
        &self,
        subject: &Subject<'_>,
        compiled: CompiledId,
    ) -> Result<Outcome, EvalError>;
    fn observe(&self, event: &Observed<'_>);
    fn lifecycle(&self, event: &Lifecycle<'_>);
    fn staleness(&self) -> Staleness;

    // Provided method
    fn policy_lifecycle(&self, event: PolicyLifecycle) { ... }
}
Expand description

What judges one or more policies.

Implemented once per language or per stateful mechanism, not once per policy: plan/policy.md’s “one evaluator may serve many policies” is why evaluate takes a PolicyId rather than the evaluator being scoped to a single policy at construction.

No method here is async, and none may perform I/O that assumes a runtime is available to drive it — this crate has no I/O and no async of its own. An evaluator that needs either (a network resolution, a model in the loop, a sidecar process) does so out of process, reached over whatever transport it likes, with the caller enforcing the deadline from outside — see plan/policy.md’s “the deadline is a security parameter”.

§What a stateless predicate may reference

evaluate takes a Subject and nothing else — no store handle, no resolver, no context object that could later grow one. That signature is the enforcement, not a rule an implementor is trusted to follow: a predicate physically cannot reach for a record it was not handed, because nothing reachable from its arguments can fetch one. What it can see, all from Subject itself:

  1. The subject’s own metadata — its account DID, and whatever SubjectAttributes names (its account kind and handle, today).
  2. The incoming write — each Change::after in the diff.
  3. The current version, if it exists — each Change::before, with None meaning there isn’t one.

Nothing else. Not another record, not another account’s repository, not the network. An evaluator’s own accumulated state — built from what it has observed via observe and lifecycle, and read back through &self — is a different thing and is exactly what makes an evaluator stateful rather than stateless; the boundary here is about what evaluate can reach live, at the moment it is asked, not about what an evaluator is allowed to remember.

This is a check on the design, not just a restriction: the epic’s motivating mention / AI opt-out policy needs to resolve DIDs from other people’s servers and cache the answers, which this signature makes impossible for a stateless predicate to do inside evaluate itself. That policy is therefore necessarily stateful — owning its cache as state built from what it has seen, with its own Staleness declaration — exactly where plan/policy.md puts it. Send + Sync: every evaluator is held as Arc<dyn Evaluator> inside a PolicyTree, which a deployment’s write path shares across every repository’s concurrently-judged writes (see didbot-pds::writequeue) — an evaluator that could not cross a thread boundary could never actually be registered into one.

Required Methods§

Source

fn compile(&self, predicate: &Value) -> CompileOutcome

Compile a predicate document into a live artifact this evaluator can judge, or say why it cannot.

Validation is compilation here — there is deliberately no separate validate: anything a caller would learn from validating ahead of time is exactly what a real compile attempt already answers, and a second code path claiming to answer the same question is drift waiting to happen. Called from the loader, never from the write path, so a predicate’s syntax is known good or known bad from the moment it is loaded rather than surfacing at the first write.

§The evaluator owns the artifact

A successful outcome carries a CompiledId this evaluator itself minted; the caller keeps only that id, never the artifact, and round-trips the id back through evaluate, observe and policy_lifecycle. See CompiledId’s doc comment for why: an evaluator may run out of process, possibly in another language, and a caller-owned handle to something living on the other side of that boundary could only ever be an identifier, never the thing itself.

§Idempotent and re-runnable

An out-of-process evaluator restarts with nothing, so a caller must be able to call compile again for a predicate it already compiled once, with no different treatment than the first time — which is also why a caller must keep the source document around rather than discarding it once a CompiledId comes back. Calling compile twice for the same predicate is expected to succeed both times, not treated as a duplicate registration.

§Fallible, and may time out

This method itself has no deadline built in — this crate has no I/O, no async, and no clock, and enforcing one against an in-process call is cooperative rather than real. A caller compiling against a slow or dead out-of-process evaluator enforces its own deadline from outside (a didbot-policy-records-owned timeout, a bounded channel) and treats a timeout exactly like Rejected: this policy’s applicability is known-good, its predicate is not usable, so the caller registers crate::policy::Predicate::Unusable scoped to it — the same fail-closed, scoped treatment EvalError gets at decision time.

Source

fn evaluate( &self, subject: &Subject<'_>, compiled: CompiledId, ) -> Result<Outcome, EvalError>

Given a subject and a compiled artifact, return an outcome, from only what subject carries — see this trait’s “What a stateless predicate may reference” for the boundary the signature enforces. compiled names the artifact compile returned for this policy, never PolicyId — an evaluator serving many compiled policies tells them apart by compiled alone; it never learns PolicyId, which belongs to the caller’s own tree.

Source

fn observe(&self, event: &Observed<'_>)

Every attempt and outcome on the declared observation surface, whether or not evaluation short-circuited. The decision channel short-circuits; this one does not.

Source

fn lifecycle(&self, event: &Lifecycle<'_>)

froze / unfroze / deleted, so per-account state does not outlive its account.

Source

fn staleness(&self) -> Staleness

How stale this evaluator’s view may be before it counts as unhealthy.

Provided Methods§

Source

fn policy_lifecycle(&self, event: PolicyLifecycle)

loaded / superseded / removed, so per-policy state does not outlive the artifact it belongs to — see PolicyLifecycle’s doc comment. Defaulted to a no-op: an evaluator that holds no state per compiled artifact (this crate’s regex evaluator, most builtins) has nothing to clean up, and should not be forced to write an empty override just to say so.

Implementors§