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 allocates three collectors — Output, Findings, Audit, in that order. Findings
is the second, bound into the run under the name findings and returned as Result.Findings;
Audit is bound as audit and returned as Result.Audit. Both are allocated per call, so two
concurrent executions never share a list. See Result for the
struct that carries them.
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 | Meaning |
|---|---|---|
| Rule id | First argument to every raise | Which check raised it. Free text — not validated against the loaded rule names |
| Severity | Add’s second argument, or the verb you used | The grade. Unset means INFO, which never fails an execution |
| Category | Add’s third argument, or the verb’s second | Free-text grouping, such as validation or completeness |
| Message | The human-readable statement | What went wrong |
| Fields | Add’s comma-separated fifth argument, or .Field(name) | Which field keys the finding concerns |
| Details | .Detail(key, value) | Extra key/value context. Absent until you add some |
| Timestamp | The engine | When the finding was raised |
Findings come back in the execution result, not through out — a caller reads them from the
response’s findings list.
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 | SeverityInfo |
warn, warning | SeverityWarning |
error, err | SeverityError |
critical, fatal | SeverityCritical |
An unrecognised name 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.
Findings
Section titled “Findings”| Signature | Behaviour |
|---|---|
New() *Findings | An empty collector |
Add(ruleID, severity, category, message, fields) | Raises a finding from five strings. fields is a comma-separated list of field keys; empty means none |
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 a raised finding. Chainable |
Field(name) | Records another field key on a raised finding. Chainable |
All() | The findings in insertion order |
Count() | How many were raised |
CountOf(severity) | How many carry that severity name |
HasSeverity(severity) | Whether any carries that severity name |
Has(category) | Whether any carries that category |
Passed() | true when nothing at ERROR or above was raised |
JSON() | Marshals the findings |
Every raise returns the *Finding, which is what makes the detail calls chainable.
Passed has only two failing grades. INFO and WARNING findings never fail an execution however
many accumulate, and an empty collector passes.
What a rule can do
Section titled “What a rule can do”The collector is bound into every execution as findings, and the whole raise-and-query surface is
reachable from rule text. Every parameter is a string, so there is nothing a rule cannot express.
rule ValidateLoanAmount "loan amount must parse as a number" salience 80 { when !strings.MatchRegex("^[0-9,.]+$", 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");Attach detail by chaining, and gate later rules on what has been raised:
findings.Error("check_totals", "totals", "line items do not sum") .Detail("expected", "12000.00") .Detail("actual", "11750.00") .Field("invoice_total");
// in a later rulewhen findings.HasSeverity("error") && !findings.Has("routing")| Call from a rule | Result |
|---|---|
findings.Add(r, sev, cat, msg, fields) | Raises. Five strings, no struct |
findings.Error(r, cat, msg) and the other three verbs | Raise at a fixed grade |
.Detail(k, v) / .Field(n) | Chain onto the returned finding |
findings.Count() / findings.CountOf("error") | Counts |
findings.HasSeverity("error") / findings.Has("totals") | Presence |
findings.Passed() | The pass/fail gate. A real read of real values |
findings.All() | Works, but a rule can only take .Len() of the returned slice |
findings.JSON() | Rejected — two return values. Read the findings from the result instead |
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 the output map 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,}$", customer_email) then findings.Error("ValidateEmail", "validation", "customer email failed the format check") .Field("customer_email"); out.Set("validation_status", "FAILED");}customer_email is a bare fact identifier — a top-level key of the payload the caller supplies. A
later rule can branch on either channel: findings.HasSeverity("error") or
out.GetStr("validation_status") == "FAILED".
Audit is the timestamped decision trail, allocated per execution alongside Findings and bound as
audit. It is the only write path a rule has for diagnostics.
| Signature | Behaviour | Callable from a rule |
|---|---|---|
audit.Log(ruleID, message) | Appends an entry stamped with the current time | yes |
audit.LogWithData(ruleID, message, data) | Same, carrying a structured payload | yes — pass in.NewMapWithValues(...) directly |
audit.All() | Every entry in append order | only .Len() is useful |
audit.Count() | Number of entries | yes |
audit.ForRule(ruleID) | Entries whose rule id matches exactly. Empty when none do | only .Len() is useful |
audit.JSON() | Marshals the entries | no — two return values are rejected outright |
AuditEntry fields
Section titled “AuditEntry fields”| 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 |
| Timestamp | When the call was made. Set for you; not settable through either method |
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 ocr page -r classify.grl -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’s listener, not by the rule, so Count() is the number
of firings plus the number of explicit Log / LogWithData calls. A rule that fires in three cycles
contributes three executed entries.
Both surfaces write the same entries, and both can return them. On a kis run --audit prints them
and the <prefix>-hist-audit.json sidecar records them; over HTTP a body carrying "audit": true
gets them back under a reserved audit key beside the output — see
the rules service for the response shape.
Related
Section titled “Related”- Built-in functions — the complete call surface of a rule, including which of these methods rule text can reach
- The rule language — why there is no struct literal
- Rule patterns — recording a decision trace
- Rules engine API —
Result,Output,Inputand the rest of the engine surface these collectors arrive on - Documents — the page and document facts the audited example runs against
- Errors and limits — what a failed load or a panicking call does to the rest of the execution