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:
- The subject’s own metadata — its account DID, and whatever
SubjectAttributesnames (its account kind and handle, today). - The incoming write — each
Change::afterin the diff. - The current version, if it exists — each
Change::before, withNonemeaning 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§
Sourcefn compile(&self, predicate: &Value) -> CompileOutcome
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.
Sourcefn evaluate(
&self,
subject: &Subject<'_>,
compiled: CompiledId,
) -> Result<Outcome, EvalError>
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.
Sourcefn observe(&self, event: &Observed<'_>)
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.
Provided Methods§
Sourcefn policy_lifecycle(&self, event: PolicyLifecycle)
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.