All files / Scripts/rules RulesService.ts

98.46% Statements 64/65
85.71% Branches 24/28
100% Functions 11/11
98.46% Lines 64/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177      3x 3x         3x 3x             53x 32x     21x       21x 21x 21x   20x 20x 20x   20x 20x       1x 1x       21x             53x   53x     52x             52x     52x 205x   205x 1490x       205x         51x       51x 51x   51x 204x 204x 1482x 1482x 27x   43x   39x   39x           39x 4x     39x     4x 4x                 51x   51x 39x 39x 39x 39x 39x   39x 37x                 39x 39x     51x   51x 51x   51x             2x   2x                           21x 21x         3x
// Rules service - the main rules engine for processing headers
 
import { HeaderModel } from "../HeaderModel";
import { headerValidationRules } from "./engine/HeaderValidationRules";
import { getRules, ruleStore } from "./loaders/GetRules";
import { AnalysisResult, RuleViolation, ViolationGroup } from "./types/AnalysisTypes";
import { HeaderSection, IValidationRule } from "./types/interfaces";
 
class RulesService {
    private rulesLoaded = false;
    private loadingPromise: Promise<void> | null = null;
 
    /**
     * Load rules once at application startup
     * Can be called multiple times safely - subsequent calls return the same promise
     */
    private async loadRules(): Promise<void> {
        if (this.rulesLoaded) {
            return Promise.resolve();
        }
 
        Iif (this.loadingPromise) {
            return this.loadingPromise;
        }
 
        this.loadingPromise = (async () => {
            try {
                await getRules(
                    () => {
                        console.log("🔍 RulesService: Rules loaded successfully");
                        console.log("🔍 RulesService: SimpleRules:", ruleStore.simpleRuleSet?.length || 0);
                        console.log("🔍 RulesService: AndRules:", ruleStore.andRuleSet?.length || 0);
 
                        headerValidationRules.setRules(ruleStore.simpleRuleSet, ruleStore.andRuleSet);
                        this.rulesLoaded = true;
                    }
                );
            } catch (error) {
                console.error("🔍 RulesService: Failed to load rules:", error);
                throw error;
            }
        })();
 
        return this.loadingPromise;
    }
 
    /**
     * Analyze headers for rule violations - main entry point
     */
    public async analyzeHeaders(headerModel: HeaderModel): Promise<AnalysisResult> {
        try {
            // Load rules once (safe to call multiple times)
            await this.loadRules();
 
            // Create sections array for header processing
            const headerSections = [
                headerModel.summary.rows,
                headerModel.forefrontAntiSpamReport.rows,
                headerModel.antiSpamReport.rows,
                headerModel.otherHeaders.rows
            ];
 
            console.log("🔍 RulesService: Processing", headerSections.length, "sections");
 
            // Clear flags and run simple rules on each section
            headerSections.forEach(section => {
                Eif (Array.isArray(section)) {
                    // Clear existing flags
                    section.forEach((headerSection: HeaderSection) => {
                        delete headerSection.rulesFlagged;
                    });
 
                    // Run simple rules on this section
                    headerValidationRules.flagAllRowsWithViolations(section, headerSections);
                }
            });
 
            // Run complex rule validation (operates on all sections)
            headerValidationRules.findComplexViolations(headerSections);
 
            // Extract violations and build groups directly during evaluation
            // Use a Map to ensure each rule only creates one violation
            const violationMap = new Map<IValidationRule, RuleViolation>();
            const groupMap = new Map<string, ViolationGroup>();
 
            headerSections.forEach(section => {
                Eif (Array.isArray(section)) {
                    section.forEach((headerSection: HeaderSection) => {
                        const rulesFlagged = headerSection.rulesFlagged;
                        if (rulesFlagged && rulesFlagged.length > 0) {
                            rulesFlagged.forEach((rule: IValidationRule) => {
                                // Check if we've already created a violation for this rule
                                if (!violationMap.has(rule)) {
                                    // First time seeing this rule - create the violation
                                    const parentAndRule = rule.parentAndRule;
 
                                    const violation: RuleViolation = {
                                        rule: rule,
                                        affectedSections: [headerSection],
                                        highlightPattern: rule.errorPattern
                                    };
 
                                    if (parentAndRule?.message) {
                                        violation.parentMessage = parentAndRule.message;
                                    }
 
                                    violationMap.set(rule, violation);
                                } else {
                                    // We've seen this rule - add this section to affected sections
                                    const existing = violationMap.get(rule)!;
                                    existing.affectedSections.push(headerSection);
                                }
                            });
                        }
                    });
                }
            });
 
            // Convert violation map to array and build groups
            const violations = Array.from(violationMap.values());
 
            violations.forEach((violation) => {
                const rule = violation.rule;
                const isAndRule = !!rule.parentAndRule;
                const displayName = isAndRule ? rule.parentAndRule!.message : rule.errorMessage;
                const severity = isAndRule ? rule.parentAndRule!.severity : rule.severity;
                const groupKey = displayName;
 
                if (!groupMap.has(groupKey)) {
                    groupMap.set(groupKey, {
                        groupId: `group-${groupKey.replace(/\s+/g, "-").toLowerCase()}`,
                        displayName,
                        severity,
                        isAndRule,
                        violations: []
                    });
                }
 
                const group = groupMap.get(groupKey)!;
                group.violations.push(violation);
            });
 
            const violationGroups = Array.from(groupMap.values());
 
            console.log("Rule violations found:", violations.length);
            console.log("Violation groups found:", violationGroups.length);
 
            return {
                success: true,
                enrichedHeaders: headerModel,
                violations,
                violationGroups
            };
        } catch (error) {
            console.error("Rules analysis failed:", error);
 
            return {
                success: false,
                error: error instanceof Error ? error.message : "Unknown analysis error",
                enrichedHeaders: headerModel,
                violations: [],
                violationGroups: []
            };
        }
    }
 
    /**
     * Reset service state for testing
     */
    public resetForTesting(): void {
        this.rulesLoaded = false;
        this.loadingPromise = null;
    }
}
 
// Export singleton instance
export const rulesService = new RulesService();