UNPKG

@ai2070/l0

Version:

L0: The Missing Reliability Substrate for AI

443 lines 14.5 kB
export function compareStrings(a, b, options = {}) { const { caseSensitive = true, normalizeWhitespace = true, algorithm = "levenshtein", } = options; let str1 = a; let str2 = b; if (!caseSensitive) { str1 = str1.toLowerCase(); str2 = str2.toLowerCase(); } if (normalizeWhitespace) { str1 = str1.replace(/\s+/g, " ").trim(); str2 = str2.replace(/\s+/g, " ").trim(); } if (str1 === str2) return 1.0; switch (algorithm) { case "levenshtein": return levenshteinSimilarity(str1, str2); case "jaro-winkler": return jaroWinklerSimilarity(str1, str2); case "cosine": return cosineSimilarity(str1, str2); default: return levenshteinSimilarity(str1, str2); } } export function levenshteinSimilarity(a, b) { if (a === b) return 1.0; if (a.length === 0 || b.length === 0) return 0.0; const distance = levenshteinDistance(a, b); const maxLength = Math.max(a.length, b.length); return 1 - distance / maxLength; } export function levenshteinDistance(a, b) { const matrix = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); } } } return matrix[b.length][a.length]; } export function jaroWinklerSimilarity(a, b) { if (a === b) return 1.0; if (a.length === 0 || b.length === 0) return 0.0; const jaroSim = jaroSimilarity(a, b); const prefixLength = commonPrefixLength(a, b, 4); const prefixScale = 0.1; return jaroSim + prefixLength * prefixScale * (1 - jaroSim); } function jaroSimilarity(a, b) { const matchWindow = Math.floor(Math.max(a.length, b.length) / 2) - 1; const aMatches = new Array(a.length).fill(false); const bMatches = new Array(b.length).fill(false); let matches = 0; let transpositions = 0; for (let i = 0; i < a.length; i++) { const start = Math.max(0, i - matchWindow); const end = Math.min(i + matchWindow + 1, b.length); for (let j = start; j < end; j++) { if (bMatches[j] || a[i] !== b[j]) continue; aMatches[i] = true; bMatches[j] = true; matches++; break; } } if (matches === 0) return 0.0; let k = 0; for (let i = 0; i < a.length; i++) { if (!aMatches[i]) continue; while (!bMatches[k]) k++; if (a[i] !== b[k]) transpositions++; k++; } return ((matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3); } function commonPrefixLength(a, b, maxLength) { let length = 0; const max = Math.min(a.length, b.length, maxLength); for (let i = 0; i < max; i++) { if (a[i] === b[i]) { length++; } else { break; } } return length; } export function cosineSimilarity(a, b) { const vectorA = stringToVector(a); const vectorB = stringToVector(b); const dotProduct = Object.keys(vectorA).reduce((sum, key) => { return sum + (vectorA[key] || 0) * (vectorB[key] || 0); }, 0); const magnitudeA = Math.sqrt(Object.values(vectorA).reduce((sum, val) => sum + val * val, 0)); const magnitudeB = Math.sqrt(Object.values(vectorB).reduce((sum, val) => sum + val * val, 0)); if (magnitudeA === 0 || magnitudeB === 0) return 0; return dotProduct / (magnitudeA * magnitudeB); } function stringToVector(str) { const words = str.toLowerCase().split(/\s+/); const vector = {}; for (const word of words) { vector[word] = (vector[word] || 0) + 1; } return vector; } export function compareNumbers(a, b, tolerance = 0.001) { return Math.abs(a - b) <= tolerance; } export function compareArrays(a, b, options, path = "") { const differences = []; if (options.ignoreArrayOrder) { const aSet = new Set(a.map((item) => JSON.stringify(item))); const bSet = new Set(b.map((item) => JSON.stringify(item))); for (const item of aSet) { if (!bSet.has(item)) { differences.push({ path: `${path}[]`, expected: JSON.parse(item), actual: undefined, type: "missing", severity: options.style === "strict" ? "error" : "warning", message: `Item missing in actual array`, }); } } for (const item of bSet) { if (!aSet.has(item)) { differences.push({ path: `${path}[]`, expected: undefined, actual: JSON.parse(item), type: "extra", severity: options.ignoreExtraFields ? "info" : "warning", message: `Extra item in actual array`, }); } } } else { const maxLength = Math.max(a.length, b.length); for (let i = 0; i < maxLength; i++) { const itemPath = `${path}[${i}]`; if (i >= a.length) { differences.push({ path: itemPath, expected: undefined, actual: b[i], type: "extra", severity: options.ignoreExtraFields ? "info" : "warning", message: `Extra item at index ${i}`, }); } else if (i >= b.length) { differences.push({ path: itemPath, expected: a[i], actual: undefined, type: "missing", severity: "error", message: `Missing item at index ${i}`, }); } else { const itemDiffs = compareValues(a[i], b[i], options, itemPath); differences.push(...itemDiffs); } } } return differences; } export function compareObjects(expected, actual, options, path = "") { const differences = []; const expectedKeys = Object.keys(expected); const actualKeys = Object.keys(actual); const allKeys = new Set([...expectedKeys, ...actualKeys]); for (const key of allKeys) { const fieldPath = path ? `${path}.${key}` : key; const hasExpected = key in expected; const hasActual = key in actual; if (options.customComparisons?.[fieldPath]) { const customResult = options.customComparisons[fieldPath](expected[key], actual[key]); if (typeof customResult === "boolean" && !customResult) { differences.push({ path: fieldPath, expected: expected[key], actual: actual[key], type: "different", severity: "error", message: `Custom comparison failed for ${fieldPath}`, }); } else if (typeof customResult === "number" && customResult < 0.8) { differences.push({ path: fieldPath, expected: expected[key], actual: actual[key], type: "different", severity: "warning", message: `Custom comparison score too low: ${customResult.toFixed(2)}`, similarity: customResult, }); } continue; } if (!hasExpected && hasActual) { if (!options.ignoreExtraFields) { differences.push({ path: fieldPath, expected: undefined, actual: actual[key], type: "extra", severity: options.style === "strict" ? "error" : "info", message: `Extra field: ${key}`, }); } } else if (hasExpected && !hasActual) { differences.push({ path: fieldPath, expected: expected[key], actual: undefined, type: "missing", severity: "error", message: `Missing field: ${key}`, }); } else { const valueDiffs = compareValues(expected[key], actual[key], options, fieldPath); differences.push(...valueDiffs); } } return differences; } export function compareValues(expected, actual, options, path = "") { if (expected === actual) { return []; } const expectedType = getType(expected); const actualType = getType(actual); if (expectedType !== actualType) { return [ { path, expected, actual, type: "type-mismatch", severity: "error", message: `Type mismatch: expected ${expectedType}, got ${actualType}`, }, ]; } switch (expectedType) { case "null": case "undefined": return expected === actual ? [] : [ { path, expected, actual, type: "different", severity: "error", message: `Expected ${expected}, got ${actual}`, }, ]; case "number": if (compareNumbers(expected, actual, options.numericTolerance)) { return []; } return [ { path, expected, actual, type: "different", severity: "error", message: `Numbers differ: ${expected} vs ${actual}`, }, ]; case "string": if (expected === actual) return []; const similarity = compareStrings(expected, actual, { caseSensitive: true, normalizeWhitespace: true, algorithm: "levenshtein", }); if (options.style === "lenient" && similarity >= 0.8) { return [ { path, expected, actual, type: "different", severity: "warning", message: `Strings differ but similar (${(similarity * 100).toFixed(0)}%)`, similarity, }, ]; } return [ { path, expected, actual, type: "different", severity: "error", message: `Strings differ`, similarity, }, ]; case "boolean": return [ { path, expected, actual, type: "different", severity: "error", message: `Boolean mismatch: ${expected} vs ${actual}`, }, ]; case "array": return compareArrays(expected, actual, options, path); case "object": return compareObjects(expected, actual, options, path); default: return [ { path, expected, actual, type: "different", severity: "error", message: `Values differ`, }, ]; } } export function getType(value) { if (value === null) return "null"; if (value === undefined) return "undefined"; if (Array.isArray(value)) return "array"; return typeof value; } export function deepEqual(a, b) { if (a === b) return true; if (a === null || b === null) return false; if (a === undefined || b === undefined) return false; const typeA = typeof a; const typeB = typeof b; if (typeA !== typeB) return false; if (typeA !== "object") return false; const isArrayA = Array.isArray(a); const isArrayB = Array.isArray(b); if (isArrayA !== isArrayB) return false; if (isArrayA) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!deepEqual(a[i], b[i])) return false; } return true; } const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!(key in b)) return false; if (!deepEqual(a[key], b[key])) return false; } return true; } export function calculateSimilarityScore(differences, totalFields) { if (totalFields === 0) return 1.0; const weights = { error: 1.0, warning: 0.5, info: 0.1, }; const totalPenalty = differences.reduce((sum, diff) => { return sum + weights[diff.severity]; }, 0); const maxPenalty = totalFields; return Math.max(0, 1 - totalPenalty / maxPenalty); } export function countFields(value) { const type = getType(value); if (type === "object") { return Object.keys(value).reduce((sum, key) => { return sum + 1 + countFields(value[key]); }, 0); } if (type === "array") { return value.reduce((sum, item) => { return sum + countFields(item); }, 0); } return 1; } //# sourceMappingURL=comparison.js.map