Evaluation
Generated from the module documentation of crates/canon/src/eval/mod.rs.
Deterministic evaluation of a case under a compiled protocol (design § 8, § 13).
evaluate_with is a pure function of a compiled protocol (Ir), a canon-case/1 case
snapshot, a set of canon-evidence/1 records and the inputs a caller supplies (Supplied):
canon-authority/1 authority decisions (--authority), the evaluation instant (--at) and
canon-decisions/1 explicit decisions (--decisions).
evaluate is the same with nothing supplied. It reads nothing else: no clock, network,
filesystem, credentials or implicit latest. Its result is a canon-decision/1 Decision
giving every declared claim the value true, false or unknown, each declared obligation
open or discharged, each declared action admissible, approval-required or blocked,
and each declared outcome legitimate or blocked (the sections below), or a
Refusal when the inputs do not fit the protocol. render is the decision's one
serialization: canonical JSON, the serialization canon-ir/1 uses.
Three-valued claims
A claim's value is its true_when predicate's value over the evidence set:
- An evidence match reads the records of its kind. One that names a
subjectreads only the records about that artifact: a record about another artifact, even at that artifact's current revision, is not of the match and neither establishes nor contradicts it (CANON-EVIDENCE-003). One that names no subject reads records about any artifact. One that demands independence (independent_on: [D, ...]) reads only the records that name aproducerthe case'sindependencemap lists on every dimensionD: any other record of its kind and subject is not of the match, whatever its result, so it neither establishes nor contradicts it (CANON-INDEPENDENCE-001,independence.rs). "Records of the kind" below are the records the match reads. - An evidence match without a result is
truewhen a record of the kind exists andunknownwhen none does. - An evidence match with a result is
unknownwhen no record of the kind exists,falsewhen records of the kind exist and none has the result,truewhen every record of the kind has that result, andunknownwhen records of the kind disagree (some with the result, some without): conflicting evidence neither establishes the predicate nor by itself contradicts it. allisfalsewhen a member isfalse, otherwisetruewhen every member istrue(an emptyallistrue), otherwiseunknown.anyistruewhen a member istrue, otherwisefalsewhen every member isfalse(an emptyanyisfalse), otherwiseunknown.notswapstrueandfalseand keepsunknown.- A claim reference is three-valued:
{claim: c}and{claim: c, is: true}arec's own value, and{claim: c, is: false}isnotof it, so a claim built oncisunknownexactly when the evidence leavescunknownand is neverfalsefor want of evidence.{claim: c, is: unknown}asks whethercis undecided, and is itself decided:truewhencisunknown,falseotherwise. - An input test
{input: s, in: [...]}istruewhen the case's value of the inputsis one it lists andfalseotherwise. A case gives every input the protocol declares a value (the case check refuses one that does not), so an input test is neverunknown: inputs are configuration fixed when the case opens, not evidence. - An order form,
{first: {kind: K, result: R}},{before: {first: K1, then: K2}},{after: {first: K1, then: {kind: K2, result: R}}}or{since: {last: K1, then: {kind: K2, result: R}}}, reads the evidence history instead: every record of its kinds (about itssubjectwhen it names one) the evidence check accepted, the records the exclusion stages set aside included, ordered byobserved_at.firstis whether the first record ofKhas resultR;beforewhether the first record ofK1was observed strictly before the first ofK2;afterwhether a record ofK2with resultRwas observed strictly after the first record ofK1;sincewhether a record ofK2with resultR, at the subject revision of the latest record ofK1, was observed after it. Each isunknownwhile the order it reads is not established, as with no record or a compared record withoutobserved_at; the predicate's reference inprotocol/1gives every rule (CANON-ORDER-001, CANON-ORDER-002, CANON-ORDER-004). No record is listed inexcluded_evidencebecause of an order form. - The form over the child cases,
{children: {in: [O, ...]}}(story:case-composition), reads everycase_outcomerecord the evidence check accepted, as the order forms read the history, one per child case: itschild_casenames the child and itsresultthe child's outcome,openwhile the child runs. It isunknownwith no record,falsewhen a record's outcome is neither listed noropen(a child that ended outside the set), otherwiseunknownwhile a child isopenandtruewhen every child ended with a listed outcome (CANON-COMPOSE-001). No record is listed inexcluded_evidencebecause of it.
unknown and false are never collapsed, and only true satisfies a positive requirement.
Refusals
Inputs are checked before any claim is evaluated, in this order, and the first problem found is
the refusal: the compiled protocol's depth (a predicate nested more than MAX_IR_DEPTH
levels, itself or along the claims it tests, as predicate-too-deep, and claims that test each
other in a cycle as claim-cycle; see "Depth" below), the case snapshot's format, its identifiers (case id, protocol id, the case
revision when given, then each artifact id and revision in the order written), its protocol against the compiled protocol's,
each artifact it lists (declared by the protocol, listed once), each artifact the protocol
declares (listed by the case), each case input it gives, in the order written (its id and
value identifiers, then declared by the protocol, else undeclared-input, given once, else
duplicate-identifier, and its value declared by the input, else undeclared-input-value,
naming the input and the value), each input the protocol declares (given by the case, else
missing-input, naming it), each producer its independence map lists, in the order written
(an identifier, listed once, else duplicate-identifier, and each dimension listed for it an
identifier, listed once for it, else duplicate-identifier, and declared by the protocol, else
undeclared-dimension, naming the producer and the dimension); then each evidence record in
the order given: its format, its identifiers (id, kind, subject, subject revision, then each
upstream artifact and revision in upstream_revisions, then its producer and its
child_case when given), its
observed_at when given (an instant, a fraction of one to nine digits allowed, else
invalid-instant), its id
(used once in the set), its kind (declared by the protocol) and each upstream artifact
(declared by the protocol, else undeclared-artifact, naming the record and the artifact; named
once, else duplicate-identifier), then, for a case_outcome record, its child case (named,
else missing-child-case) and its outcome (a result, else missing-child-outcome), and for a
record of any other kind no child case (else misplaced-child-case); then, over the whole set,
one case_outcome record per child case, whatever its revision (else duplicate-child-case,
naming the least such child and its records in evidence-id order, citing each). A record's
subject and subject revision are checked here as identifiers only; the revision-binding stage
reads them. Every refusal of a record names it (evidence `e2` format is `canon-evidence/2`, expected `canon-evidence/1`, evidence `e2` kind identifier `k k` is empty or …), and
cites it (Refusal::evidence), here and in the revision-binding stage below, so a caller
that read the records from files can name the file of each (canon evaluate does).
A case snapshot or evidence record that is not a document of its format is refused when it is
read (read_case, read_evidence), before any of this, as malformed-input; a required key
left out is named (missing field `subject`), and an evidence record is named by its id
when it has one (evidence `e2` is not a canon-evidence/1 document: missing field `subject`).
Then the supplied inputs are read, in this order: the authority decisions (--authority), the
evaluation instant (--at) and the explicit decisions. The authority decisions are read by
authority.rs as canon-authority/1, refused as the section "Authority decisions" below
says. The instant is read by freshness.rs and refused as invalid-instant when it is not
one. The explicit decisions are read by decisions.rs as canon-decisions/1, refused as the
section "Explicit decisions" below says.
Then the exclusion stages run, in pipeline order: revision binding, freshness, invalidation.
Each may refuse the inputs it reads. Revision binding checks each record in the order given: a
record whose subject the protocol does not declare is refused as undeclared-artifact, naming
the record and its subject (evidence `e1` is about artifact `x`, which the protocol does not declare), and a record bound to a revision of its subject that is not the case snapshot's
current one is excluded as revision_mismatch. Freshness refuses a max_age it cannot read
(invalid-max-age) and excludes a record older than its kind's max_age at the evaluation
instant as expired. Invalidation refuses a rule whose upstream artifact the case snapshot does
not name, which only an IR a caller builds can hold, as undeclared-artifact, and invalidates,
for the claims each rule names and every claim built on one of them, the records that recorded
a revision of the rule's upstream artifact other than the case snapshot's current one
(invalidation.rs, CANON-INVALIDATION-001).
Then claims are evaluated.
Last, the outcomes section checks the case snapshot's termination: one naming an outcome
the protocol does not declare is refused as undeclared-outcome, naming the outcome; then an
explicit decision taken for an outcome the protocol does not declare is refused as
undeclared-outcome, and one taken for a declared outcome that does not require a decision of
its name as undeclared-decision, each naming the decision and the outcome; and a
termination naming a declared outcome that is blocked as illegitimate-termination, naming
the outcome and its status (CANON-OUTCOME-001).
Depth
read_ir refuses an IR whose arrays and objects nest deeper than MAX_IR_DEPTH
(4096) before parsing it. Reading and claim evaluation recurse along the nesting of the
predicates, so they run on a thread of their own with a stack sized for that bound
(DEEP_STACK): an IR at the bound reads and evaluates from any caller's thread.
Claim evaluation also recurses through each claim test into the predicate of the claim it
tests, so the bound applies to a claim's effective depth: a predicate is one level and each
all, any or not around it one more, and a claim test at level n reaches n plus the
effective depth of the claim it tests. evaluate_with takes any Ir a caller builds and
checks the bound first, before any step recurses (depth.rs), computing each claim's
effective depth once, after the claims it tests, without recursion. It refuses as
predicate-too-deep an obligation, action, outcome or claim whose own predicate nests more
than MAX_IR_DEPTH levels (claim `c` true_when nests predicates deeper than 4096), and
a claim whose effective depth is more than MAX_IR_DEPTH, naming the claims of the chain that
reaches it (claim `c1` true_when nests predicates deeper than 4096 through the claims it tests: c1 -> c2 -> c3). Claims that test each other in a cycle — which canon compile never
produces, but a caller can build — have no effective depth and are refused there as
claim-cycle, naming the claims from the first one reached again, in the order evaluation
reaches them: claims test each other in a cycle: c -> d -> c. Each level of the JSON text is
at least one predicate level, so read_ir's bound holds every predicate of an IR it returns
within the bound; a chain of claims, each within it, can still exceed it, and canon compile
compiles such a protocol, so an IR read_ir returns can still be refused here. Dropping a
caller's IR is the caller's own recursion.
The pipeline
evaluate_with runs the same steps for every evaluation, each in the file that owns its
concept; this module holds only the order:
- check the compiled protocol's depth (
depth.rs), the case snapshot (case.rs) and the evidence set (evidence.rs); - read the supplied inputs: authority decisions (
authority.rs), the evaluation instant (freshness.rs) and explicit decisions (decisions.rs); - run the exclusion stages in order: revision binding (
binding.rs), freshness (freshness.rs), invalidation (invalidation.rs). Each sees only the evidence the stages before it left. Revision binding and freshness return the records they exclude from every claim, with their reason; invalidation returns, for each claim, the records it keeps from that claim alone; - evaluate every claim over the evidence left (
claims.rs), and its order forms and forms over the child cases over every record the evidence check accepted (order.rs,composition.rs), each in its own context: no evidence match in its evaluation reads a record invalidated for it, including the matches of every claim it tests, which are evaluated for that use without the records invalidated for either; the value reported for a tested claim is its own. Each claim'sexcluded_evidencelists, in evidence-id order, every excluded record of a kind the claim reaches: a kind an evidence match in its own predicate names, or one a claim it tests reaches, through any number of claim references. A match that names a subject reaches its kind only for records about that artifact: a record about another artifact does not match it, so it is not listed under a claim that reaches its kind only through such a match. A record excluded from a claim is excluded from every claim built on it. A record invalidated for a claim is listed under it, with the reasoninvalidated, when an evidence match the claim reaches reads it, and under no claim it was not invalidated for. Any other record is listed under a claim, with the reasonnot_independent, when an evidence match the claim reaches would read it by kind and subject and none of those matches accepts its producer (independence.rs): a match that demands independence keeps it out, and no match demanding less reads it. Each record is listed once, with the first of these reasons that applies; - the
obligations,actionsandoutcomessections (obligations.rs,actions.rs,outcomes.rs), then the explanation (crate::explain). Each section evaluates its predicates with the one evaluator claims use (claims::predicate): discharge and applicability predicates over the claim values, action preconditions and outcome requirements over the claim values and the evidence left after step 3 (their order forms and forms over the child cases over every record accepted), each with the case's input values, which every input test reads. The invalidation stage keeps a record from claims only, so these predicates' own evidence matches read it. An outcome that requires an explicit decision is decided by the explicit decisions instead.
Revision binding excludes a record bound to another revision as revision_mismatch, and
freshness a record older than its kind's max_age at the evaluation instant as expired,
each listed as excluded under each claim that reaches its kind through a match that reads it, as
step 4 says. Invalidation keeps a record from the claims an invalidation rule names and every
claim built on one of them, throughout their evaluation, as invalidated, listed under each of
those claims that reaches a match reading it; every other claim, even one reading the same
kind, reads the record and does not list it. The obligations, actions and outcomes
sections are written as the next section says, and every decision carries an explanation
(crate::explain).
Sections
A section is written only when the protocol declares something it reports on: a protocol that
declares no obligation has no obligations key, and likewise for actions and outcomes.
obligationsis an array with one{"id": <obligation>, "status": <status>}entry per declared obligation, in identifier order. An obligation with anapplicable_whenpredicate isnot_applicablewhen it isfalseover the claim values and the case's inputs. Otherwise the status isdischargedwhen the obligation'sdischarged_whenpredicate istrueover the claim values, whatever its applicability;unknownandfalseboth leave itopen(CANON-OBLIGATION-001). An open obligation whose applicability isunknownhas"reasons"naming what is unknown, written as an action's reasons are (CANON-INPUT-001).actionsmaps each declared action to itsstatusand, unless it is admissible, thereasonsthat decide it.blockedwhen the precondition is nottrue: each claim test and evidence match that decides it is a reason,{"claim": <id>, "value": <the claim's value>}or{"evidence": <kind>, "present": <whether a record the match reads applies>}, with"subject": <artifact>added when the match names one (a record about another artifact is not read by it), then each input test that decides it,{"input": <id>, "value": <the case's value>}, then each order form that decides it,{"after": <kind>, "then": <kind>, "result": <result>, "value": <its value>},{"before": <kind>, "then": <kind>, "value": <its value>},{"first": <kind>, "result": <result>, "value": <its value>}or{"since": <kind>, "then": <kind>, "result": <result>, "value": <its value>}, with"subject"added when it names one, then each form over the child cases that decides it,{"children": [<outcome>, ...], "value": <its value>}, or{"requirement": "unsatisfiable"}when none does.blockedtoo when the precondition istrueand the authority decisions deny a capability the action requires, each a reason{"capability": <id>, "decision": "denied"}.approval-requiredwhen the precondition istrue, nothing is denied and some required capability is not decided, each a reason{"capability": <id>, "decision": "none"}.admissiblewhen the precondition istrueand every required capability is granted (CANON-AUTHORITY-001).outcomesmaps each declared outcome to{"status": "legitimate"}when itsrequirespredicate istrueover the claim values and the evidence left, and otherwise to{"status": "blocked", "reasons": [...]}. Its reasons follow the one rule an action precondition's do, and are written the same way: in anallthat isfalse, only itsfalsemembers decide it, with polarity carried throughnot. An outcome that requires an explicit decision (requires: decision: <name>) islegitimateonly when an explicit decision of that name, for that outcome, was taken at the case snapshot'srevision, and its entry then records who decided (design § 37):{"status": "legitimate", "decided_by": {"decision": <name>, "principals": [...]}}, every principal whose decision applied, sorted by Unicode code point; otherwise it is blocked with the one reason{"decision": <name>, "present": false}(CANON-OUTCOME-002). The case snapshot'sterminationis checked against it, as the refusals above say (CANON-OUTCOME-001).explanationis written for every decision: what the decision was computed from (design § 37), and why each claim that is nottrue, each open obligation, each action that is not admissible and each blocked outcome has its status, down to the evidence records that applied or were excluded. Its shape is given bycrate::explain(CANON-EXPLAIN-001).
Authority decisions
--authority names a canon-authority/1 document: a YAML (or JSON) list of decisions, each a
capability and whether it is granted, - {capability: finding.publish, decision: granted} (or
denied). An empty list decides nothing. Canon grants nothing and resolves no identity: the
caller decides and passes the decisions in. Refused, in this order: text that is not such a
list, or an entry with another key, another decision or a missing key, as malformed-input,
naming the entry by its position, counting from 1, and a missing key by name (entry 2: missing field `capability`); then, entry by entry, a capability that is not an identifier
as invalid-identifier, and a capability decided a second time as duplicate-identifier.
Explicit decisions
--decisions names a canon-decisions/1 document: a YAML (or JSON) list of decisions, each
naming the decision, the outcome it was taken for, the principal who took it and the case
revision it was taken at, - {decision: explicitly_inconclusive, outcome: inconclusive, principal: lead-investigator, case_revision: c2}. An empty list decides nothing. Canon decides
nothing and resolves no identity: the caller passes in the decisions that were taken. A
decision applies only while its case revision is the case snapshot's revision: one taken at a
superseded revision, or given for a snapshot without a revision, applies to nothing. Refused,
in this order: text that is not such a list, or an entry with another key or a missing one, as
malformed-input, naming the entry by its position and a missing key by name (entry 1: missing field `case_revision`); then, entry by entry, a decision, outcome, principal or case revision that
is not an identifier as invalid-identifier, and an entry that repeats an earlier one exactly as
duplicate-identifier, naming it; then, in the outcomes section and at any case revision, a
decision for an outcome the protocol does not declare as undeclared-outcome, and one for an
outcome that does not require a decision of its name as undeclared-decision.