import { createHash } from 'node:crypto';
/**
 * Version of the normalization rules below. It is stored on every hash record
 * and folded into the cache directory name, so a change here is a clean cache
 * miss rather than a silent comparison of incomparable values.
 *
 * Bump this whenever the lexer, the keyword tables, or the digest material
 * change in any way.
 */
export const ENTITY_HASH_VERSION = 1;
export function computeExactBodyHash(source) {
    return digest(source);
}
/**
 * Keywords reserved in every brace language covered here, and used as
 * identifiers in none of them.
 */
// prettier-ignore
const SHARED_CONTROL_KEYWORDS = [
    'break', 'case', 'catch', 'continue', 'default', 'do', 'else', 'finally',
    'for', 'if', 'return', 'switch', 'throw', 'try', 'while',
];
/**
 * Keyword tables are **per dialect and never merged**.
 *
 * A merged table looks harmless and is not: `record`, `type`, `object`, `get`,
 * `set`, `module`, and `val` are reserved in one language and everyday
 * identifiers in the next. Under a merged table, renaming a local to `record`
 * flips its token shape from the identifier placeholder to a keyword, which
 * silently destroys the structural equality that rename detection depends on.
 *
 * Dialects are keyed by file extension rather than by `LANGUAGE_SPECS.name`,
 * because that name is `generic` for a dozen unrelated brace languages.
 */
// prettier-ignore
const KEYWORDS_BY_DIALECT = {
    typescript: [
        ...SHARED_CONTROL_KEYWORDS, 'abstract', 'as', 'async', 'await', 'class', 'const', 'constructor', 'declare',
        'delete', 'enum', 'export', 'extends', 'from', 'function', 'implements', 'import', 'in', 'instanceof',
        'interface', 'let', 'new', 'of', 'private', 'protected', 'public', 'readonly', 'static', 'super', 'this',
        'typeof', 'var', 'void', 'yield', 'true', 'false', 'null', 'undefined',
    ],
    python: [
        'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
        'False', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'None', 'nonlocal', 'not',
        'or', 'pass', 'raise', 'return', 'True', 'try', 'while', 'with', 'yield', 'self', 'cls',
    ],
    go: [
        ...SHARED_CONTROL_KEYWORDS, 'chan', 'const', 'defer', 'fallthrough', 'func', 'go', 'goto', 'import',
        'interface', 'map', 'package', 'range', 'select', 'struct', 'type', 'var', 'nil', 'true', 'false',
    ],
    rust: [
        ...SHARED_CONTROL_KEYWORDS, 'as', 'async', 'await', 'const', 'crate', 'dyn', 'enum', 'extern', 'fn', 'impl',
        'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'self', 'Self', 'static', 'struct', 'super',
        'trait', 'type', 'unsafe', 'use', 'where', 'true', 'false',
    ],
    java: [
        ...SHARED_CONTROL_KEYWORDS, 'abstract', 'class', 'extends', 'final', 'implements', 'import', 'instanceof',
        'interface', 'native', 'new', 'package', 'private', 'protected', 'public', 'static', 'strictfp', 'super',
        'synchronized', 'this', 'throws', 'transient', 'volatile', 'true', 'false', 'null',
    ],
    csharp: [
        ...SHARED_CONTROL_KEYWORDS, 'abstract', 'as', 'base', 'checked', 'class', 'const', 'delegate', 'enum', 'event',
        'explicit', 'extern', 'fixed', 'foreach', 'goto', 'implicit', 'in', 'interface', 'internal', 'is', 'lock',
        'namespace', 'new', 'operator', 'out', 'override', 'params', 'private', 'protected', 'public', 'readonly',
        'ref', 'sealed', 'sizeof', 'stackalloc', 'static', 'struct', 'this', 'typeof', 'unchecked', 'unsafe', 'using',
        'virtual', 'volatile', 'true', 'false', 'null',
    ],
    kotlin: [
        ...SHARED_CONTROL_KEYWORDS, 'as', 'class', 'companion', 'const', 'data', 'fun', 'import', 'in', 'init',
        'interface', 'internal', 'is', 'object', 'open', 'override', 'package', 'private', 'protected', 'public',
        'sealed', 'super', 'suspend', 'this', 'typealias', 'val', 'var', 'when', 'where', 'true', 'false', 'null',
    ],
    scala: [
        ...SHARED_CONTROL_KEYWORDS, 'abstract', 'class', 'def', 'extends', 'final', 'implicit', 'import', 'lazy',
        'match', 'new', 'object', 'override', 'package', 'private', 'protected', 'sealed', 'super', 'this', 'trait',
        'type', 'val', 'var', 'with', 'yield', 'true', 'false', 'null',
    ],
    swift: [
        ...SHARED_CONTROL_KEYWORDS, 'as', 'associatedtype', 'class', 'deinit', 'enum', 'extension', 'fileprivate',
        'func', 'guard', 'import', 'in', 'init', 'inout', 'internal', 'is', 'let', 'open', 'operator', 'private',
        'protocol', 'public', 'repeat', 'rethrows', 'self', 'static', 'struct', 'subscript', 'typealias', 'var',
        'where', 'true', 'false', 'nil',
    ],
    cpp: [
        ...SHARED_CONTROL_KEYWORDS, 'auto', 'class', 'const', 'constexpr', 'delete', 'enum', 'explicit', 'extern',
        'friend', 'goto', 'inline', 'mutable', 'namespace', 'new', 'operator', 'private', 'protected', 'public',
        'register', 'sizeof', 'static', 'struct', 'template', 'this', 'typedef', 'typename', 'union', 'using',
        'virtual', 'volatile', 'true', 'false', 'nullptr',
    ],
    php: [
        ...SHARED_CONTROL_KEYWORDS, 'abstract', 'as', 'class', 'clone', 'const', 'declare', 'echo', 'elseif', 'endif',
        'extends', 'final', 'foreach', 'function', 'global', 'implements', 'include', 'include_once', 'instanceof',
        'interface', 'namespace', 'new', 'print', 'private', 'protected', 'public', 'require', 'require_once',
        'static', 'trait', 'use', 'var', 'true', 'false', 'null',
    ],
    ruby: [
        'alias', 'and', 'begin', 'break', 'case', 'class', 'def', 'do', 'else', 'elsif', 'end', 'ensure', 'false',
        'for', 'if', 'in', 'module', 'next', 'nil', 'not', 'or', 'redo', 'rescue', 'retry', 'return', 'self', 'super',
        'then', 'true', 'undef', 'unless', 'until', 'when', 'while', 'yield',
    ],
    /** Last resort: only words no covered language treats as an identifier. */
    generic: SHARED_CONTROL_KEYWORDS,
};
function specFor(dialect, keywords) {
    const base = {
        lineComments: ['//'],
        blockComments: [['/*', '*/']],
        quotes: ['"', "'"],
        longQuotes: [],
        keywords: new Set(keywords),
    };
    switch (dialect) {
        case 'python':
            return { ...base, lineComments: ['#'], blockComments: [], longQuotes: ['"""', "'''"] };
        case 'ruby':
            return { ...base, lineComments: ['#'], blockComments: [['=begin', '=end']] };
        case 'php':
            return { ...base, lineComments: ['//', '#'] };
        case 'typescript':
        case 'go':
        case 'kotlin':
        case 'scala':
            return { ...base, quotes: ['"', "'", '`'] };
        default:
            return base;
    }
}
const SPEC_BY_DIALECT = Object.fromEntries(Object.entries(KEYWORDS_BY_DIALECT).map(([dialect, keywords]) => [dialect, specFor(dialect, keywords)]));
// prettier-ignore
const DIALECT_BY_EXTENSION = {
    ts: 'typescript', tsx: 'typescript', mts: 'typescript', cts: 'typescript', js: 'typescript', jsx: 'typescript',
    mjs: 'typescript', cjs: 'typescript', es6: 'typescript', vue: 'typescript',
    py: 'python', pyi: 'python',
    go: 'go',
    rs: 'rust',
    java: 'java',
    cs: 'csharp',
    kt: 'kotlin', kts: 'kotlin',
    scala: 'scala', sc: 'scala', sbt: 'scala',
    swift: 'swift',
    c: 'cpp', h: 'cpp', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp', hh: 'cpp', hxx: 'cpp',
    php: 'php', phtml: 'php',
    rb: 'ruby',
};
/** Lexical dialect for a path. Unknown extensions fall back to the shared set. */
export function lexDialectForPath(relativePath) {
    const dot = relativePath.lastIndexOf('.');
    if (dot === -1)
        return 'generic';
    return DIALECT_BY_EXTENSION[relativePath.slice(dot + 1).toLowerCase()] ?? 'generic';
}
export function lexSpecForDialect(dialect) {
    return SPEC_BY_DIALECT[dialect] ?? SPEC_BY_DIALECT.generic;
}
function isIdentifierStart(char) {
    return /[A-Za-z_$À-]/.test(char);
}
function isIdentifierPart(char) {
    return /[A-Za-z0-9_$À-]/.test(char);
}
function normalizeNumber(raw) {
    // The legacy identity heuristic collapses digit separators. This is useful
    // for candidate matching, but it is not semantic evidence.
    return raw.replace(/_/g, '').toLowerCase();
}
/**
 * Tokenize a source span into shape/text pairs, dropping comments and
 * whitespace entirely.
 *
 * This is a lexer, not a parser: it never needs the span to be a valid program,
 * which matters because an entity body is a fragment. Keywords keep their own
 * identity so control flow survives into the structural stream, while other
 * identifiers collapse to `$` and literals to their class — that is what makes
 * the structural hash rename-tolerant.
 */
export function tokenizeSource(source, dialect) {
    const spec = lexSpecForDialect(dialect);
    const tokens = [];
    const length = source.length;
    let index = 0;
    outer: while (index < length) {
        const char = source[index];
        if (/\s/.test(char)) {
            index++;
            continue;
        }
        for (const marker of spec.lineComments) {
            if (source.startsWith(marker, index)) {
                const end = source.indexOf('\n', index);
                index = end === -1 ? length : end + 1;
                continue outer;
            }
        }
        for (const [open, close] of spec.blockComments) {
            if (source.startsWith(open, index)) {
                const end = source.indexOf(close, index + open.length);
                index = end === -1 ? length : end + close.length;
                continue outer;
            }
        }
        for (const quote of spec.longQuotes) {
            if (source.startsWith(quote, index)) {
                const end = source.indexOf(quote, index + quote.length);
                const stop = end === -1 ? length : end + quote.length;
                tokens.push({ shape: '#s', text: `s:${source.slice(index + quote.length, end === -1 ? length : end)}` });
                index = stop;
                continue outer;
            }
        }
        if (spec.quotes.includes(char)) {
            let cursor = index + 1;
            let value = '';
            while (cursor < length) {
                const current = source[cursor];
                if (current === '\\' && cursor + 1 < length) {
                    value += source[cursor + 1];
                    cursor += 2;
                    continue;
                }
                if (current === char)
                    break;
                value += current;
                cursor++;
            }
            // The legacy identity heuristic records only the decoded value. A CST
            // proof, not this token stream, decides whether a change altered logic.
            tokens.push({ shape: '#s', text: `s:${value}` });
            index = cursor + 1;
            continue;
        }
        if (/[0-9]/.test(char)) {
            let cursor = index;
            while (cursor < length && /[0-9a-fA-FxXoObB_.]/.test(source[cursor]))
                cursor++;
            tokens.push({ shape: '#n', text: `n:${normalizeNumber(source.slice(index, cursor))}` });
            index = cursor;
            continue;
        }
        if (isIdentifierStart(char)) {
            let cursor = index;
            while (cursor < length && isIdentifierPart(source[cursor]))
                cursor++;
            const word = source.slice(index, cursor);
            tokens.push(spec.keywords.has(word) ? { shape: word, text: word } : { shape: '$', text: word });
            index = cursor;
            continue;
        }
        tokens.push({ shape: char, text: char });
        index++;
    }
    return tokens;
}
// Written as escapes, not literal control characters: a raw separator byte in
// the source makes the file read as binary to grep and other line tools.
// Written as escapes, not literal control characters: a raw separator byte
// in the source makes the file read as binary to grep and other line tools.
const FIELD_SEPARATOR = '\u001f';
const RECORD_SEPARATOR = '\u001e';
function digest(material) {
    return createHash('sha256').update(material, 'utf8').digest('hex');
}
/**
 * The declaration is everything up to the first body opener. Hashing its shape
 * alone answers "did the callable contract change?" independently of the body.
 */
function declarationTokens(tokens) {
    const stop = tokens.findIndex(token => token.shape === '{' || token.shape === ':');
    return stop === -1 ? [...tokens] : tokens.slice(0, stop);
}
export function computeEntityHashes(input) {
    // Kept byte-identical to the frozen `code.symbol.bodyHash` contract without
    // importing its higher-level batch module back into the entity layer.
    const bodyHash = digest(input.source);
    if (input.source.length === 0) {
        return { hashVersion: ENTITY_HASH_VERSION, algo: 'none', bodyHash };
    }
    const dialect = lexDialectForPath(input.relativePath);
    const tokens = tokenizeSource(input.source, dialect);
    const normalized = tokens.map(token => `${token.shape}${FIELD_SEPARATOR}${token.text}`).join(RECORD_SEPARATOR);
    const structural = tokens.map(token => token.shape).join(FIELD_SEPARATOR);
    const signature = declarationTokens(tokens)
        .map(token => token.shape)
        .join(FIELD_SEPARATOR);
    return {
        hashVersion: ENTITY_HASH_VERSION,
        algo: input.spanExact ? 'lexer' : 'declaration',
        bodyHash,
        normalizedHash: digest(normalized),
        structuralHash: digest(structural),
        signatureHash: digest(signature),
        tokenCount: tokens.length,
    };
}
/**
 * Whether two hash records may be compared at all.
 *
 * This gate is load-bearing. Without it, a machine where a tree-sitter grammar
 * failed to load (dropping a file to the regex tier, where only the declaration
 * line is available) would compare declaration hashes against body hashes and
 * manufacture lineage that does not exist. When it returns false the matcher
 * must fall through to name-based evidence and record that evidence was
 * unavailable — never guess.
 */
export function hashesComparable(a, b) {
    if (!a || !b)
        return false;
    if (a.hashVersion !== b.hashVersion)
        return false;
    if (a.algo !== b.algo)
        return false;
    return a.algo !== 'none';
}
/** True only when both records are full-span; body evidence requires it. */
export function bodyEvidenceAvailable(a, b) {
    return hashesComparable(a, b) && a?.algo === 'lexer';
}
/** Overlapping shingles of the shape stream, the unit of similarity scoring. */
export function shingleSet(shapes, width = 5) {
    const shingles = new Set();
    if (shapes.length === 0)
        return shingles;
    if (shapes.length <= width) {
        shingles.add(shapes.join(FIELD_SEPARATOR));
        return shingles;
    }
    for (let index = 0; index + width <= shapes.length; index++) {
        shingles.add(shapes.slice(index, index + width).join(FIELD_SEPARATOR));
    }
    return shingles;
}
/** Sørensen–Dice: `2|A∩B| / (|A|+|B|)`. Symmetric, in `[0, 1]`, no sampling. */
export function diceSimilarity(a, b) {
    if (a.size === 0 && b.size === 0)
        return 1;
    if (a.size === 0 || b.size === 0)
        return 0;
    let shared = 0;
    const [small, large] = a.size <= b.size ? [a, b] : [b, a];
    for (const value of small) {
        if (large.has(value))
            shared++;
    }
    return (2 * shared) / (a.size + b.size);
}
/** Similarity over identifier-blind shape shingles — the rename-tolerant signal. */
export function structuralSimilarity(a, b, relativePath, width = 5) {
    const dialect = lexDialectForPath(relativePath);
    const left = shingleSet(tokenizeSource(a, dialect).map(token => token.shape), width);
    const right = shingleSet(tokenizeSource(b, dialect).map(token => token.shape), width);
    return diceSimilarity(left, right);
}
/** Similarity over identifier-aware tokens — the independent tiebreak signal. */
export function normalizedSimilarity(a, b, relativePath, width = 5) {
    const dialect = lexDialectForPath(relativePath);
    const left = shingleSet(tokenizeSource(a, dialect).map(token => `${token.shape}${FIELD_SEPARATOR}${token.text}`), width);
    const right = shingleSet(tokenizeSource(b, dialect).map(token => `${token.shape}${FIELD_SEPARATOR}${token.text}`), width);
    return diceSimilarity(left, right);
}
//# sourceMappingURL=hash.js.map