/** * String utilities for config-audit scanners. * Zero external dependencies. */ /** * Count lines in a string. * @param {string} s * @returns {number} */ export function lineCount(s) { if (!s) return 0; return s.split('\n').length; } /** * Truncate a string to maxLen chars with ellipsis. * @param {string} s * @param {number} [maxLen=100] * @returns {string} */ export function truncate(s, maxLen = 100) { if (!s || s.length <= maxLen) return s || ''; return s.slice(0, maxLen - 3) + '...'; } /** * Check if two strings have >threshold% content similarity (word overlap). * @param {string} a * @param {string} b * @param {number} [threshold=0.8] * @returns {boolean} */ export function isSimilar(a, b, threshold = 0.8) { const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2)); const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2)); if (wordsA.size === 0 || wordsB.size === 0) return false; let overlap = 0; for (const w of wordsA) { if (wordsB.has(w)) overlap++; } const similarity = overlap / Math.min(wordsA.size, wordsB.size); return similarity >= threshold; } /** * Levenshtein edit distance between two strings (insertions, deletions, * substitutions; a transposition counts as 2). Used for typo detection on * settings keys. Zero external dependencies, O(a*b) with two rolling rows. * @param {string} a * @param {string} b * @returns {number} */ export function levenshtein(a, b) { if (a === b) return 0; const al = a.length; const bl = b.length; if (al === 0) return bl; if (bl === 0) return al; let prev = new Array(bl + 1); let curr = new Array(bl + 1); for (let j = 0; j <= bl; j++) prev[j] = j; for (let i = 1; i <= al; i++) { curr[0] = i; const ac = a.charCodeAt(i - 1); for (let j = 1; j <= bl; j++) { const cost = ac === b.charCodeAt(j - 1) ? 0 : 1; curr[j] = Math.min( prev[j] + 1, // deletion curr[j - 1] + 1, // insertion prev[j - 1] + cost, // substitution ); } const tmp = prev; prev = curr; curr = tmp; } return prev[bl]; } /** * Extract all key-like patterns from a settings.json or similar config. * @param {object} obj * @param {string} [prefix=''] * @returns {string[]} */ export function extractKeys(obj, prefix = '') { const keys = []; for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; keys.push(fullKey); if (value && typeof value === 'object' && !Array.isArray(value)) { keys.push(...extractKeys(value, fullKey)); } } return keys; } /** * Normalize a file path for comparison (resolve ~, handle trailing slashes). * @param {string} p * @returns {string} */ export function normalizePath(p) { const home = process.env.HOME || process.env.USERPROFILE || ''; let normalized = p.replace(/^~/, home); normalized = normalized.replace(/[/\\]+$/, ''); return normalized; }