Documentation Index Fetch the complete documentation index at: https://mintlify.com/Basit-Ali0/Yggdrasil/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Yggdrasil explanations are template-generated, not LLM-generated . Every violation includes:
Policy Excerpt : The exact clause from the regulatory document
Evidence Grid : Matched field values from your data
Condition Summary : Human-readable breakdown of what matched
No AI calls in the enforcement loop means:
✅ Deterministic : Same violation → same explanation
✅ Audit-ready : No hallucinations or inconsistencies
✅ Fast : No API latency for explanation generation
Implementation
Explanations are generated by explainability.ts using string templates:
// From explainability.ts:41-150
export function generateExplanation (
rule : Rule ,
record : NormalizedRecord
) : string {
const recordId = record . account
? ` ${ record . step } _ ${ record . account } `
: `record_ ${ record . step } ` ;
switch ( rule . rule_id ) {
case 'CTR_THRESHOLD' :
return (
`Transaction ${ recordId } was flagged under CTR_THRESHOLD because: \n\n ` +
`- Amount: $ ${ record . amount . toLocaleString () } \n ` +
`- Threshold: $10,000 \n ` +
`- Transaction Type: ${ record . type } \n ` +
`- Account: ${ record . account } \n\n ` +
`Policy Reference: Section 1 - Currency Transaction Reporting \n ` +
`Severity: CRITICAL \n\n ` +
`This transaction exceeds the $10,000 CTR filing threshold and requires ` +
`a Currency Transaction Report to be filed with FinCEN.`
);
// ... more templates
}
}
Explanation Components
1. Policy Excerpt
Every rule includes the exact text from the regulatory document:
{
"policy_excerpt" : "Financial institutions must file a Currency Transaction Report (CTR) for each transaction in currency of more than $10,000." ,
"policy_section" : "31 CFR 1020.310"
}
This is stored in the rules table and displayed in the violation detail.
2. Evidence Grid
The evidence object contains all relevant field values from the matched record:
// From in-memory-backend.ts:574-577
evidence : {
... record ,
... ( match . condition_summary ? { condition_summary: match . condition_summary } : {}),
}
Example evidence :
{
"account" : "C1234567" ,
"amount" : 15000 ,
"transaction_type" : "WIRE" ,
"step" : 42 ,
"recipient" : "R7890123" ,
"condition_summary" : "amount >= 10000 (actual: 15000)"
}
3. Condition Summary
For condition-based rules (GDPR, SOC2), the engine generates a human-readable summary:
// From explainability.ts:12-35
function summarizeConditions ( cond : any , record : NormalizedRecord , depth = 0 ) : string {
if ( ! cond ) return '' ;
const indent = ' ' . repeat ( depth );
if ( 'AND' in cond && Array . isArray ( cond . AND )) {
const parts = cond . AND . map (( c : any ) => summarizeConditions ( c , record , depth + 1 ));
return ` ${ indent } ALL of: \n ${ parts . join ( ' \n ' ) } ` ;
}
if ( 'OR' in cond && Array . isArray ( cond . OR )) {
const parts = cond . OR . map (( c : any ) => summarizeConditions ( c , record , depth + 1 ));
return ` ${ indent } ANY of: \n ${ parts . join ( ' \n ' ) } ` ;
}
if ( 'field' in cond ) {
const actualValue = record [ cond . field ];
if ( cond . operator === 'exists' ) {
return ` ${ indent } - ${ cond . field } is present (value: ${ JSON . stringify ( actualValue ) } )` ;
}
if ( cond . operator === 'not_exists' ) {
return ` ${ indent } - ${ cond . field } is missing or empty (value: ${ JSON . stringify ( actualValue ) } )` ;
}
return ` ${ indent } - ${ cond . field } ${ cond . operator } ${ JSON . stringify ( cond . value ) } (actual: ${ JSON . stringify ( actualValue ) } )` ;
}
return '' ;
}
Example output :
ALL of:
- amount >= 10000 (actual: 15000)
- transaction_type IN ["DEBIT", "WIRE"] (actual: "WIRE")
Single-Transaction Explanations
Prebuilt Rule Templates
For well-known rules (AML, FinCEN), the engine uses hardcoded templates:
CTR Threshold
case 'CTR_THRESHOLD' :
return (
`Transaction ${ recordId } was flagged under CTR_THRESHOLD because: \n\n ` +
`- Amount: $ ${ record . amount . toLocaleString () } \n ` +
`- Threshold: $10,000 \n ` +
`- Transaction Type: ${ record . type } \n ` +
`- Account: ${ record . account } \n\n ` +
`Policy Reference: Section 1 - Currency Transaction Reporting \n ` +
`Severity: CRITICAL \n\n ` +
`This transaction exceeds the $10,000 CTR filing threshold and requires ` +
`a Currency Transaction Report to be filed with FinCEN.`
);
Balance Mismatch
case 'BALANCE_MISMATCH' : {
const expected = record . oldbalanceOrg ! - record . amount ;
const actual = record . newbalanceOrig ! ;
const discrepancy = Math . abs ( expected - actual );
return (
`Transaction ${ recordId } was flagged under BALANCE_MISMATCH because: \n\n ` +
`- Transaction Amount: $ ${ record . amount . toLocaleString () } \n ` +
`- Expected Balance Change: $ ${ expected . toLocaleString () } \n ` +
`- Actual Balance Change: $ ${ actual . toLocaleString () } \n ` +
`- Discrepancy: $ ${ discrepancy . toLocaleString () } \n ` +
`- Account: ${ record . account } \n\n ` +
`Policy Reference: Section 4 - Balance Verification \n ` +
`Severity: MEDIUM \n\n ` +
`The balance change does not match the transaction amount, indicating ` +
`a potential data entry error or system issue.`
);
}
Generic Condition-Based Explanation
For custom PDF-extracted rules, the engine uses the condition summarizer:
default : {
const lines: string [] = [];
lines . push ( `Record ${ recordId } was flagged under ${ rule . rule_id } ( ${ rule . name } ) because: \n ` );
if ( rule . conditions ) {
lines.push( summarizeConditions (rule.conditions, record ));
lines.push( '' );
}
if (rule.policy_excerpt) {
lines.push( `Policy Reference: ${ rule . policy_section || 'N/A' } ` );
lines.push( `Excerpt: " ${ rule . policy_excerpt } "` );
}
lines.push( `Severity: ${ rule . severity } ` );
if (rule.description) {
let descText = rule.description;
try {
const parsed = JSON.parse(rule.description);
if (parsed.text) descText = parsed.text;
} catch {
// Not JSON, use raw string
}
lines.push( ` \n ${ descText } ` );
}
return lines.join( ' \n ' );
}
Example output :
Record 42_C1234567 was flagged under GDPR_CONSENT_MISSING (Consent Not Obtained) because:
ALL of:
- consent_obtained == false (actual: false)
- marketing_sent == true (actual: true)
Policy Reference: Article 6(1)(a)
Excerpt: "Processing shall be lawful only if the data subject has given consent."
Severity: HIGH
Marketing communications were sent without obtaining explicit consent from the data subject.
Windowed Explanations
Windowed rules (aggregations, velocity) generate context-aware explanations:
// From explainability.ts:155-259
export function generateWindowedExplanation (
rule : Rule ,
account : string ,
records : NormalizedRecord [],
extras : Record < string , any > = {}
) : string {
const total = records . reduce (( sum , r ) => sum + r . amount , 0 );
const amounts = records . map (( r ) => `$ ${ r . amount . toLocaleString () } ` ). join ( ', ' );
switch ( rule . rule_id ) {
case 'CTR_AGGREGATION' :
return (
`Account pair ${ account } → ${ extras . recipient ?? 'multiple' } was flagged under CTR_AGGREGATION because: \n\n ` +
`- Aggregate Amount: $ ${ total . toLocaleString () } \n ` +
`- Transaction Count: ${ records . length } \n ` +
`- Time Window: 24 hours \n ` +
`- Individual Amounts: ${ amounts } \n\n ` +
`Policy Reference: Section 1 - CTR Aggregation \n ` +
`Severity: CRITICAL \n\n ` +
`Multiple transactions to the same person within 24 hours exceeded the ` +
`$10,000 aggregate CTR threshold.`
);
// ...
}
}
Structuring Pattern Example
case 'STRUCTURING_PATTERN' :
return (
`Account ${ account } was flagged under STRUCTURING_PATTERN because: \n\n ` +
`- Transaction Count: ${ records . length } \n ` +
`- Individual Amounts: ${ amounts } (all between $8,000-$10,000) \n ` +
`- Total Amount: $ ${ total . toLocaleString () } \n ` +
`- Time Window: 24 hours \n\n ` +
`Policy Reference: Section 2 - Structuring Detection \n ` +
`Severity: CRITICAL \n\n ` +
`This account conducted ${ records . length } transactions just under the $10,000 ` +
`CTR threshold within 24 hours, suggesting intentional structuring ` +
`to avoid reporting requirements.`
);
Output :
Account C1234567 was flagged under STRUCTURING_PATTERN because:
- Transaction Count: 5
- Individual Amounts: $8,500, $9,200, $8,800, $9,500, $9,000 (all between $8,000-$10,000)
- Total Amount: $45,000
- Time Window: 24 hours
Policy Reference: Section 2 - Structuring Detection
Severity: CRITICAL
This account conducted 5 transactions just under the $10,000 CTR threshold within 24 hours, suggesting intentional structuring to avoid reporting requirements.
The description field in the rules table can be:
Plain text : Used directly
JSON with historical context :
{
"text" : "Processing of personal data without lawful basis" ,
"historical_context" : {
"avg_fine" : "€15M" ,
"breach_example" : "Google LLC (2019) - €50M for lack of transparency" ,
"article_reference" : "Article 6(1)"
}
}
The explanation generator parses this and uses the text field.
Why Templates, Not LLMs?
Determinism
Template :
Input: { amount: 15000, type: "WIRE" }
Output: "Amount: $15,000, Type: WIRE"
Run 1: ✅ "Amount: $15,000, Type: WIRE"
Run 2: ✅ "Amount: $15,000, Type: WIRE"
Run 1000: ✅ "Amount: $15,000, Type: WIRE"
LLM :
Input: { amount: 15000, type: "WIRE" }
Run 1: "The transaction amount was $15,000 via wire transfer."
Run 2: "A wire transfer of fifteen thousand dollars was detected."
Run 3: "$15K WIRE transaction flagged."
❌ Not reproducible → Not audit-ready
Speed
Method Latency Template <1ms LLM (Gemini) 200-800ms
For 1,000 violations, templates take 1 second . LLMs would take 5+ minutes .
No Hallucinations
Templates only use:
Rule metadata (stored in DB)
Record fields (from CSV)
Hardcoded policy references
LLMs can hallucinate :
Fake policy section numbers
Incorrect thresholds
Misleading explanations
Cost
Templates are free . LLM calls cost money per violation.
Evidence Drawer
The violation detail page displays:
Policy Excerpt (top)
Evidence Grid (structured data)
Explanation (formatted text)
Example UI structure:
┌─────────────────────────────────────────┐
│ Policy Excerpt │
│ "Financial institutions must file..." │
│ Section: 31 CFR 1020.310 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Evidence │
│ • Account: C1234567 │
│ • Amount: $15,000 │
│ • Type: WIRE │
│ • Timestamp: 2026-02-15 14:32:10 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Explanation │
│ Transaction 42_C1234567 was flagged │
│ under CTR_THRESHOLD because: │
│ │
│ - Amount: $15,000 │
│ - Threshold: $10,000 │
│ - Transaction Type: WIRE │
│ ... │
└─────────────────────────────────────────┘
Extending Templates
To add a new rule template:
Edit explainability.ts
Add a new case to the switch statement:
case 'YOUR_RULE_ID' :
return (
`Record ${ recordId } was flagged under YOUR_RULE_ID because: \n\n ` +
`- Field1: ${ record . field1 } \n ` +
`- Field2: ${ record . field2 } \n\n ` +
`Policy Reference: ${ rule . policy_section } \n ` +
`Severity: ${ rule . severity } \n\n ` +
`Detailed explanation of why this matters.`
);
Fallback : If no template exists, the generic condition summarizer is used.
Next Steps
Confidence Scoring Learn how violations are ranked
Bayesian Feedback How user reviews improve explanations