// tools/create-spec/relevance-scorer.ts — SPEC-778
// Semantic relevance scoring for autopilot file suggestions.
// Filters out generic/unrelated files from spec analysis.
import { readFile } from 'node:fs/promises';
import { glob } from 'glob';
import { join } from 'node:path';
/** Stop words to exclude from keyword extraction. */
const STOP_WORDS = new Set([
    // English stop words
    'the',
    'spec',
    'is',
    'a',
    'of',
    'and',
    'to',
    'in',
    'for',
    'that',
    'with',
    'on',
    'or',
    'by',
    'at',
    'as',
    'be',
    'an',
    'this',
    'from',
    'it',
    'was',
    'are',
    'will',
    'must',
    'should',
    // Structural path/code tokens — too generic for file relevance
    'src',
    'tests',
    'test',
    'engine',
    'tools',
    'types',
    'lib',
    'app',
    'index',
    'utils',
    'helpers',
    // Common code tokens that appear in virtually every file
    'return',
    'const',
    'let',
    'var',
    'function',
    'export',
    'import',
    'class',
    'new',
    'true',
    'false',
    'null',
    'undefined',
    'void',
    'async',
    'await',
    'then',
    'catch',
    'throw',
]);
/** Number of characters to read from each file for content scoring. */
const CONTENT_READ_LIMIT = 4000;
/**
 * Extract meaningful keywords from spec title + description text.
 * Drops stop-words, requires min length 3, also splits CamelCase identifiers.
 */
export function extractKeywords(text) {
    // Split on whitespace and common punctuation (including hyphens, dots, slashes, colons)
    const base = text
        .toLowerCase()
        .split(/[\s\-./:\\|,;!?@#$%^&*()+=<>[\]{}"'`]+/)
        .filter((w) => w.length >= 3 && !STOP_WORDS.has(w));
    // Also extract CamelCase identifiers from the original text and lowercase them
    const camelMatches = text.match(/\b[a-z][a-zA-Z0-9_]*[A-Z][a-zA-Z0-9_]*\b/g) ?? [];
    const camelLower = camelMatches.map((s) => s.toLowerCase());
    // Split CamelCase into individual tokens (e.g. releaseHandler → release, handler)
    const camelParts = camelMatches.flatMap((s) => s
        .replace(/([A-Z])/g, ' $1')
        .toLowerCase()
        .split(/\s+/)
        .filter((w) => w.length >= 3 && !STOP_WORDS.has(w)));
    const all = new Set([...base, ...camelLower, ...camelParts]);
    // Final filter: drop pure stop words and very short tokens
    for (const word of all) {
        if (word.length < 3 || STOP_WORDS.has(word)) {
            all.delete(word);
        }
    }
    return all;
}
/** Regex to test whether a keyword appears as a whole token (word-boundary safe). */
function buildKeywordPattern(keyword) {
    // Escape special regex chars
    const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    return new RegExp(`(?<![a-z0-9])${escaped}(?![a-z0-9])`, 'i');
}
/**
 * Score a single file path for relevance to the keyword set.
 * Uses word-boundary matching to avoid false positives from substrings.
 * Returns pathScore*2 + contentScore. Returns 0 on file-not-found.
 */
export async function scoreFile(filePath, keywords) {
    if (keywords.size === 0) {
        return 0;
    }
    const lowerPath = filePath.toLowerCase();
    // For path segments, split on path separators and check for token equality
    const pathTokens = new Set(lowerPath.split(/[/\\.\-_]+/).filter((t) => t.length >= 3));
    const keyArr = [...keywords];
    const pathScore = keyArr.filter((k) => pathTokens.has(k)).length;
    let contentScore = 0;
    try {
        const raw = await readFile(filePath, 'utf-8');
        const head = raw.slice(0, CONTENT_READ_LIMIT).toLowerCase();
        // Use word-boundary pattern to avoid substring false positives
        contentScore = keyArr.filter((k) => buildKeywordPattern(k).test(head)).length;
    }
    catch {
        // Unreadable file — path score only
    }
    return pathScore * 2 + contentScore;
}
/**
 * Find project files relevant to the keyword set.
 * Scans `src/**\/\*.ts` and `tests/**\/\*.ts` under projectPath,
 * scores each, drops score=0 entries, returns top-`limit` by descending score.
 */
export async function findRelevantFiles(projectPath, keywords, limit = 10) {
    if (keywords.size === 0) {
        return [];
    }
    const patterns = [join(projectPath, 'src/**/*.ts'), join(projectPath, 'tests/**/*.ts')];
    const candidates = [];
    for (const pattern of patterns) {
        const found = await glob(pattern, { ignore: ['**/node_modules/**', '**/dist/**'] });
        candidates.push(...found);
    }
    if (candidates.length === 0) {
        return [];
    }
    const scored = await Promise.all(candidates.map(async (f) => ({ file: f, score: await scoreFile(f, keywords) })));
    return scored
        .filter((s) => s.score > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, limit)
        .map((s) => s.file);
}
//# sourceMappingURL=relevance-scorer.js.map