Findings
A finding is one severity-graded validation issue produced during a rule execution: which check raised it, how bad it is, what it says, and any structured detail. Findings are collected in one list per execution, and that list answers a single question: did this execution pass?
Every execution gets its own findings and audit collectors, bound under those names by every
host, so two concurrent executions never share a list. Findings are not part of out: they come
back beside it.
The engine never inspects a finding: it allocates the collector, exposes it, and hands it back.
Severity is the only thing that carries meaning, and only through Passed.
What a finding carries
Section titled “What a finding carries”| Part | Set by | In the rules.svc response |
|---|---|---|
| Rule id | First argument to every raise | rule. Free text: not checked against the loaded rule names |
| Severity | Add’s second argument, or the verb you used | severity: INFO, WARNING, ERROR or CRITICAL |
| Category | Add’s third argument, or the verb’s second | category, omitted when empty. Free-text grouping, such as validation or completeness |
| Message | The human-readable statement | message |
| Fields | Add’s comma-separated fifth argument, or .Field(name) | fields, a list, omitted when empty |
| Details | .Detail(key, value) | details, an object of text values, omitted when empty |
| Timestamp | The engine, when the finding is raised | Not returned |
Where findings go
Section titled “Where findings go”rules.svc returns them in every response. POST /rules/execute answers with a findings list
beside out, empty when nothing was raised, in the order the rules raised them:
{ "out": { "totalsChecked": true, "status": "held", "errors": 1 }, "findings": [ { "rule": "CheckTotals", "severity": "ERROR", "category": "totals", "message": "line items do not sum to the invoice total", "fields": ["invoice.total"], "details": { "expected": "12000.00", "actual": "11750.00" } } ]}The rest of the response is on the rules service page.
kis rules does not print findings. It prints out, and with --audit and --history the
audit trail and the write history. To see a verdict in a local run, carry it in out as well
(below).
Severity
Section titled “Severity”Four grades, named by string everywhere a rule touches them:
| Severity | Fails Passed() |
|---|---|
INFO | no |
WARNING | no |
ERROR | yes |
CRITICAL | yes |
Severity names are resolved case-insensitively, and the short forms rules actually write are accepted:
| Accepted | Resolves to |
|---|---|
info, information, informational | INFO |
warn, warning | WARNING |
error, err | ERROR |
critical, fatal | CRITICAL |
An unrecognised name passed to Add resolves to INFO, so a misspelling silently downgrades an
error to information. Prefer the graded verbs, where the severity is in the function name and
cannot be misspelled.
The calls
Section titled “The calls”Every parameter is a string, so there is nothing a rule cannot express.
| Call | Behaviour |
|---|---|
findings.Add(ruleID, severity, category, message, fields) | Raises a finding from five strings. fields is a comma-separated list of field keys; empty means none |
findings.Info / Warn / Error / Critical (ruleID, category, message) | Raise at a fixed grade. Severity is in the verb, so it cannot be misspelled |
.Detail(key, value) | Attaches one key/value to the finding just raised. Chainable |
.Field(name) | Records another field key on the finding just raised. Chainable |
findings.Count() | How many were raised |
findings.CountOf(severity) | How many carry that severity name. 0 for an unrecognised name |
findings.HasSeverity(severity) | Whether any carries that severity name |
findings.Has(category) | Whether any carries that category |
findings.Passed() | true when nothing at ERROR or above was raised |
findings.All() | The findings in the order raised. A rule can only count them, with .Len() |
findings.JSON() | Not callable. It returns two values; the call loads, then aborts the execution with calling function JSON which returns multiple values |
Passed has only two failing grades. INFO and WARNING findings never fail an execution however
many accumulate, and an empty collector passes.
Raising
Section titled “Raising”The five-string form:
rule ValidateLoanAmount "loan amount must parse as a number" salience 80 { when !strings.MatchRegex("^[0-9.]+$", in.Str("LELoanAmount", "")) then findings.Add("validate_le_loan_amount_numeric", "Error", "invalid_format", "LELoanAmount does not parse as a valid number", "LELoanAmount");}The four graded verbs are shorter and cannot carry a misspelled severity:
findings.Error("check_totals", "totals", "line items do not sum to the invoice total");findings.Warn("check_dates", "completeness", "no issue date found");findings.Info("classify", "routing", "document classified as a closing disclosure");findings.Critical("check_signature", "integrity", "signature block is missing");Detail, and gating a later rule
Section titled “Detail, and gating a later rule”Attach detail by chaining onto the raise, and gate later rules on what has been raised. This pair produces the response shown above:
rule CheckTotals "line items must sum to the invoice total" salience 20 { when math.Abs(in.Double("invoice.total", 0.0) - in.Double("invoice.linesTotal", 0.0)) > 0.01 then findings.Error("CheckTotals", "totals", "line items do not sum to the invoice total") .Detail("expected", to.Fixed(in.Double("invoice.total", 0.0), 2)) .Detail("actual", to.Fixed(in.Double("invoice.linesTotal", 0.0), 2)) .Field("invoice.total"); out.Set("totalsChecked", true);}
rule HoldForReview "anything at error or above holds the invoice" salience 10 { when !findings.Passed() then out.Set("status", "held"); out.Set("errors", findings.CountOf("error"));}{ "invoice": { "total": 12000.0, "linesTotal": 11750.0 } }{ "totalsChecked": true, "status": "held", "errors": 1 }Carrying the verdict in the output as well
Section titled “Carrying the verdict in the output as well”Findings are the validation channel, but a caller that reads only out, and every kis rules run,
still needs the verdict there. Setting both costs one line and makes the result readable either way:
rule ValidateEmail "flag an unparseable customer email" salience 80 { when !strings.MatchRegex("^[^@ ]+@[^@ ]+\\.[a-zA-Z]{2,}$", in.Str("customer_email", "")) then findings.Error("ValidateEmail", "validation", "customer email failed the format check") .Field("customer_email"); out.Set("validation_status", "FAILED");}{ "customer_email": "ana@example" }{ "validation_status": "FAILED" }A later rule can branch on either channel: findings.HasSeverity("error") or
out.Str("validation_status", "") == "FAILED".
audit is the timestamped decision trail, allocated per execution alongside findings. It is a
rule’s write path for diagnostics.
| Call | Behaviour |
|---|---|
audit.Log(ruleID, message) | Appends an entry stamped with the current time |
audit.LogWithData(ruleID, message, data) | Same, carrying a structured payload. Build it with map.Of(...), passed directly |
audit.Count() | Number of entries |
audit.All() | Every entry in append order. A rule can only count them, with .Len() |
audit.ForRule(ruleID) | Entries whose rule id matches exactly. Count with .Len() |
audit.JSON() | Not callable. Two return values abort the execution |
What an entry holds
Section titled “What an entry holds”| Part | Meaning |
|---|---|
| Rule id | The first argument to Log / LogWithData. Free text. The engine’s own entries carry the firing rule’s name |
| Message | The second argument. The engine writes executed |
| Data | The third argument to LogWithData. Absent for entries written by Log |
| Cycle | The engine cycle the entry was made in, from 1 |
| Timestamp | When the call was made. Set for you |
Every rule that fires also appends an automatic entry with the message executed before its then
block runs, so a rule’s own audit.Log lands immediately after its executed marker:
kis rules -r classify.grl --with-bbox -i pages --history --audit page1--- Audit for pages/page1.json --- 1. [DetectInvoice] executed 2. [DetectInvoice] matched the phrase 'Invoice Number'The automatic entry is written by the engine, not by the rule, so Count() is the number of
firings plus the number of explicit Log / LogWithData calls. A rule fires once per execution,
or once per cycle it repeats in when it calls agenda.Repeat().
Where audit entries go
Section titled “Where audit entries go”| Surface | How to get them | What comes back |
|---|---|---|
kis rules | --audit prints them; --audit or --history-audit <dir> also records them in the <prefix>-hist-audit.json sidecar | Rule, message, and the data of a LogWithData entry |
| rules.svc | "options": {"audit": true} on POST /rules/execute | An audit list beside out, each entry’s rule, message and cycle. The data payload is not returned |
Related
Section titled “Related”- Built-in functions: the complete call surface of a rule
- The rule language: why there is no struct literal
- Rule patterns: recording a decision trace
- The rules service: the full
POST /rules/executeresponse - Documents: the page and document facts the audited example runs against
- Errors: what a failed load or a panicking call does to the rest of the execution