// engine/spec-changelog/diff.ts — Semantic diff calculation for spec criteria
/** Normalize a criteria text for comparison (trim, collapse whitespace). */
export function normalizeCriteriaText(text) {
    return text
        .replace(/^#+\s+/, '') // remove markdown headers
        .replace(/\*\*/g, '') // remove bold markers
        .replace(/\s+/g, ' ') // collapse whitespace
        .trim()
        .toLowerCase();
}
/**
 * Compute a simple similarity score (0–1) between two strings.
 * Uses character bigram overlap.
 */
export function similarityScore(a, b) {
    if (a.length === 0 && b.length === 0) {
        return 0;
    }
    if (a === b) {
        return 1;
    }
    if (a.length === 0 || b.length === 0) {
        return 0;
    }
    const buildBigrams = (str) => {
        const bigrams = new Set();
        for (let i = 0; i < str.length - 1; i++) {
            bigrams.add(str.slice(i, i + 2));
        }
        return bigrams;
    };
    const bigramsA = buildBigrams(a);
    const bigramsB = buildBigrams(b);
    let intersection = 0;
    for (const bigram of bigramsA) {
        if (bigramsB.has(bigram)) {
            intersection++;
        }
    }
    return (2 * intersection) / (bigramsA.size + bigramsB.size);
}
/**
 * Compute semantic diff between two sets of criteria texts.
 * Returns added, removed, and modified criteria.
 */
export function computeCriteriaDiff(before, after) {
    const normalizedBefore = before.map(normalizeCriteriaText);
    const normalizedAfter = after.map(normalizeCriteriaText);
    const SIMILARITY_THRESHOLD = 0.6;
    const matchedBeforeIndices = new Set();
    const matchedAfterIndices = new Set();
    const modified = [];
    // Find modifications: pairs with high similarity but not identical
    for (let i = 0; i < normalizedBefore.length; i++) {
        const nb = normalizedBefore[i];
        /* v8 ignore next 2 — noUncheckedIndexedAccess guard; array built with .map() so never undefined */
        if (nb === undefined) {
            continue;
        }
        let bestScore = 0;
        let bestJ = -1;
        for (let j = 0; j < normalizedAfter.length; j++) {
            if (matchedAfterIndices.has(j)) {
                continue;
            }
            const na = normalizedAfter[j];
            /* v8 ignore next 2 — noUncheckedIndexedAccess guard; array built with .map() so never undefined */
            if (na === undefined) {
                continue;
            }
            const score = similarityScore(nb, na);
            if (score > bestScore) {
                bestScore = score;
                bestJ = j;
            }
        }
        if (bestScore >= SIMILARITY_THRESHOLD && bestJ !== -1) {
            matchedBeforeIndices.add(i);
            matchedAfterIndices.add(bestJ);
            const beforeText = before[i];
            const afterText = after[bestJ];
            // Only report as modified if texts actually differ
            if (beforeText !== undefined && afterText !== undefined && beforeText !== afterText) {
                modified.push({ before: beforeText, after: afterText });
            }
        }
    }
    // Removed: unmatched before items
    const removed = [];
    for (let i = 0; i < before.length; i++) {
        if (!matchedBeforeIndices.has(i)) {
            const text = before[i];
            if (text !== undefined) {
                removed.push(text);
            }
        }
    }
    // Added: unmatched after items
    const added = [];
    for (let j = 0; j < after.length; j++) {
        if (!matchedAfterIndices.has(j)) {
            const text = after[j];
            if (text !== undefined) {
                added.push(text);
            }
        }
    }
    return { added, removed, modified };
}
/**
 * Returns true if the diff contains any actual changes.
 */
export function hasDiffChanges(diff) {
    return diff.added.length > 0 || diff.removed.length > 0 || diff.modified.length > 0;
}
//# sourceMappingURL=diff.js.map