import type { CodemationLogConfig, CodemationLogRule } from "../../presentation/config/CodemationLogConfig"; export type LogLevel = "silent" | "debug" | "info" | "warn" | "error"; const levelRank: Record, number> = { debug: 0, info: 1, warn: 2, error: 3, }; const globRegexMetaChars = new Set("\\^$+?()[]{}|."); export class LogLevelPolicy { private cachedMin: LogLevel | undefined; private codemationRules: ReadonlyArray<{ matchers: ReadonlyArray; minLevel: LogLevel }> | null = null; resolveMin(): LogLevel { const raw = process.env.CODEMATION_LOG_LEVEL?.toLowerCase(); if (raw === "silent" || raw === "debug" || raw === "info" || raw === "warn" || raw === "error") { return raw; } if (process.env.VITEST === "true") { return "warn"; } return "info"; } private minLevelCached(): LogLevel { if (this.cachedMin === undefined) { this.cachedMin = this.resolveMin(); } return this.cachedMin; } resetForTests(): void { this.cachedMin = undefined; this.codemationRules = null; } applyCodemationLogConfig(config: CodemationLogConfig | undefined): void { this.codemationRules = null; if (!config) { return; } const rawRules = this.normalizeCodemationRules(config); if (rawRules.length === 0) { return; } const compiled: { matchers: ReadonlyArray; minLevel: LogLevel }[] = []; for (const rule of rawRules) { const minLevel = this.parseRuleLevel(rule.level); const patterns = this.normalizeFilterPatterns(rule.filter); if (patterns.length === 0) { throw new Error("codemation.config log rule filter must include at least one pattern"); } const matchers = patterns.map((pattern) => this.compileGlobPattern(pattern)); compiled.push({ matchers, minLevel, }); } this.codemationRules = compiled; } shouldEmit(level: Exclude, namespace: string): boolean { if (this.codemationRules && this.codemationRules.length > 0) { for (const rule of this.codemationRules) { if (rule.matchers.some((regex) => regex.test(namespace))) { return this.levelPassesAgainstMin(level, rule.minLevel); } } } return this.levelPassesAgainstMin(level, this.minLevelCached()); } private normalizeCodemationRules(config: CodemationLogConfig): ReadonlyArray { if ("rules" in config && Array.isArray(config.rules)) { return [...config.rules]; } return [config as CodemationLogRule]; } private normalizeFilterPatterns(filter: string | ReadonlyArray): ReadonlyArray { if (typeof filter === "string") { return [filter]; } return [...filter]; } private compileGlobPattern(pattern: string): RegExp { const trimmed = pattern.trim(); if (trimmed === "*") { return /^.*$/; } let body = ""; for (const ch of trimmed) { if (ch === "*") { body += ".*"; continue; } if (globRegexMetaChars.has(ch)) { body += "\\" + ch; continue; } body += ch; } return new RegExp("^" + body + "$"); } private parseRuleLevel(level: string): LogLevel { const normalized = level.toLowerCase(); if ( normalized === "silent" || normalized === "debug" || normalized === "info" || normalized === "warn" || normalized === "error" ) { return normalized; } throw new Error(`Invalid codemation.config log level: ${level}`); } private levelPassesAgainstMin(level: Exclude, min: LogLevel): boolean { if (min === "silent") { return false; } return levelRank[level] >= levelRank[min]; } }