@ai2070/l0
Version:
L0: The Missing Reliability Substrate for AI
443 lines • 14.5 kB
JavaScript
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