Authoritative reference for the GNAT analysis rule engine.
Rule files are .hy (Hy/Lisp) files placed in the configured rules_dir.
Each file uses the defrule macro:
(require gnat.analysis.rules.macros *)
(import gnat.analysis.rules.helpers *)
(defrule <rule-name>
:description <string> ;; optional
:phase <string> ;; required: "open", "supported", "refuted", "inconclusive"
:target-status <string> ;; optional: informational only
:priority <integer> ;; optional, default 50
:tags [<string> ...] ;; optional, default []
:when (fn [h ctx] <body>) ;; required: predicate returning truthy/falsy
:then (fn [h ctx] <body>)) ;; required: returns a Decision:phase— hypothesis status the rule applies to (string matchingHypothesisStatus.value):when— predicate function(hypothesis, ctx) -> bool:then— decision function(hypothesis, ctx) -> Decision
| Constructor | Consumes slot? | Mutates? |
|---|---|---|
(set-status <target> :reason <str>) |
Yes | Yes (via orchestrator) |
(annotate <key> <value> :reason <str>) |
No | No |
(no-op :reason <str>) |
Yes | No |
| Function | Signature | Description |
|---|---|---|
supporting-count / supporting_count |
(h) -> int |
Number of supporting evidence items |
refuting-count / refuting_count |
(h) -> int |
Number of refuting evidence items |
evidence-count / evidence_count |
(h) -> int |
Total evidence (supporting + refuting) |
has-refutation? / has_refutation |
(h) -> bool |
True if any refuting evidence |
support-ratio / support_ratio |
(h) -> float |
supporting / (total + 1) |
| Function | Signature | Description |
|---|---|---|
has-confidence? / has_confidence |
(h) -> bool |
True if ConfidenceScore assigned |
stix-confidence / stix_confidence |
(h) -> int |
STIX confidence 0-100, or 0 |
confidence-band / confidence_band |
(h) -> str|None |
HIGH/MEDIUM/LOW or None |
reliability-of / reliability_of |
(h) -> str|None |
Source reliability A-F |
credibility-of / credibility_of |
(h) -> int|None |
Information credibility 1-6 |
reliability-at-least? / reliability_at_least |
(h, level) -> bool |
True if reliability >= level |
credibility-at-least? / credibility_at_least |
(h, level) -> bool |
True if credibility <= level (lower is better) |
| Function | Signature | Description |
|---|---|---|
age-days / age_days |
(h [now]) -> int |
Days since creation |
days-since-update / days_since_update |
(h [now]) -> int |
Days since last update |
stale? / stale |
(h [days=30] [now]) -> bool |
True if not updated in N days |
fresh? / fresh |
(h [days=7] [now]) -> bool |
True if updated within N days |
| Function | Signature | Description |
|---|---|---|
status-of / status_of |
(h) -> HypothesisStatus |
Current status enum |
is-open? / is_open |
(h) -> bool |
Status is OPEN |
is-supported? / is_supported |
(h) -> bool |
Status is SUPPORTED |
is-refuted? / is_refuted |
(h) -> bool |
Status is REFUTED |
is-inconclusive? / is_inconclusive |
(h) -> bool |
Status is INCONCLUSIVE |
| Function | Signature | Description |
|---|---|---|
within-ai-ceiling? / within_ai_ceiling |
(h, ctx) -> bool |
True if not AI-only or confidence <= ceiling |
| Function | Signature | Description |
|---|---|---|
evidence-sources / evidence_sources |
(h, ctx) -> set[str] |
Connector names for all evidence |
trust-levels / trust_levels |
(h, ctx) -> set[str] |
Trust levels across sources |
has-trusted-evidence? / has_trusted_evidence |
(h, ctx) -> bool |
Any trusted_internal source |
all-evidence-trusted? / all_evidence_trusted |
(h, ctx [min]) -> bool |
All evidence meets minimum trust |
evidence-from? / evidence_from |
(h, ctx, name) -> bool |
Evidence from named connector |
unknown-source-count / unknown_source_count |
(h, ctx) -> int |
Evidence from unknown connectors |
ai-only? / ai_only |
(h, ctx) -> bool |
All evidence from AI connectors |
Rules are sorted by priority descending (highest first). For a given hypothesis, the engine walks the sorted list sequentially.
- The first rule returning
set-statusorno-opconsumes the transition slot. No further transition decisions fire. - Rules returning
annotatenever consume the slot. All matching annotation rules fire regardless of position. - A
no-opconsumes the slot without mutating — use it to block lower-priority rules.
A rule only evaluates if hypothesis.status.value == rule.phase. Rules
with phase = None match all statuses.
If a rule's :when or :then raises an exception, the error is logged,
added to result.errors, and evaluation continues to the next rule.
Broken rules never halt hypothesis processing.
Every rule firing writes to the rule_firing_audit table (or in-memory
log) before the orchestrator applies the decision.
| Column | Type | Description |
|---|---|---|
id |
BigInteger | Primary key |
investigation_id |
String | Investigation containing the hypothesis |
hypothesis_id |
String | Evaluated hypothesis |
workspace_id |
Integer | Workspace context |
rule_name |
Text | Rule that fired |
rule_source_file |
Text | Path to .hy file |
rule_git_sha |
String(40) | Git SHA of rule file at firing time |
fired_at |
DateTime(tz) | When the rule fired |
decision |
JSON | Serialized decision object |
applied |
Boolean | True after successful mutation |
applied_at |
DateTime(tz) | When mutation succeeded |
error_message |
Text | Error if mutation failed |
engine_version |
String(32) | Engine version |
Configured via [rules] INI section:
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Feature flag — must be true for evaluation |
rules_dir |
string | rules |
Directory containing .hy files |
ai_confidence_ceiling |
int | 60 |
Max STIX confidence for AI-only evidence |
minimum_evidence_for_support |
int | 3 |
Default evidence threshold |
stale_days_default |
int | 30 |
Default staleness threshold |
fresh_days_default |
int | 7 |
Default freshness threshold |
allow_dirty_rules |
bool | false |
Allow uncommitted rule files to fire |
Environment override: GNAT_ALLOW_DIRTY_RULES=1 forces allow_dirty_rules=true.
Three engines are available, selectable via [rules] engine:
Rules authored as .yaml/.yml files with a structured condition DSL.
No code authoring required — conditions reference helpers by name.
rules:
- name: support-on-strong-evidence
phase: open
priority: 100
when:
all:
- supporting_count: { gte: 3 }
- has_refutation: false
- reliability_at_least: "B"
- within_ai_ceiling: true
then:
set_status:
target: supported
reason: "Strong evidence"Condition operators: eq, neq, gt, gte, lt, lte
Boolean combinators: all (AND), any (OR), not (negate)
Special args: { days: N } for temporal helpers (stale, fresh)
Install: pip install "gnat[rules]" (includes pyyaml)
Rules authored as .pl files using SWI-Prolog syntax. Best for
complex inference chains and backward-chaining reasoning.
rule(my_rule, [phase(open), priority(100)]).
when(my_rule) :- supporting_count(N), N >= 3, \+ has_refutation.
then(my_rule, set_status(supported, 'Strong evidence')).Hypothesis facts are asserted into the Prolog KB before each evaluation
and retracted after. Pre-loaded helper predicates in prolog_helpers.pl
mirror the Python helper library.
Install: pip install "gnat[rules-prolog]" (requires SWI-Prolog system install)
- Rule load failure: Logged, file skipped, other rules still load.
- Rule evaluation exception: Logged, added to
result.errors, next rule evaluated. - Hy not installed: Hy loader returns empty list, logs warning.
- pyyaml not installed: YAML loader returns empty list, logs warning.
- pyswip not installed: Prolog loader returns empty list, logs warning.
- Git unavailable:
rule_file_shareturns None,git_file_is_cleanreturns False (fail closed). - Service mutation failure: Audit row recorded with
error_message,applied=false.