import os from 'node:os';
import path from 'node:path';
import { stat } from 'node:fs/promises';
import { z } from 'zod';
import { discoverCodeFiles } from './discover.js';
import { getLanguageSpec, getModuleName, invalidateTsConfigResolutionCache, normalizePath } from './languages.js';
import { parseCodeFile } from './parsers.js';
import { enrichTypeScriptSemantics, semanticCallResolution, semanticInheritanceResolution, } from './typescript-semantics.js';
import { extractKeySection } from './query.js';
import { attachSourceInlineManifest } from './source-inline.js';
import { CODEBASE_INDEX_SCHEMA_VERSION, ContentModeSchema, TypeScriptCheckerModeSchema, } from './types.js';
import { abortIfNeeded, createYielder, isCooperative, mapWithConcurrency, } from '../utils.js';
export const BudgetExceededBehaviorSchema = z.enum(['throw', 'truncate']);
export const BuildCodebaseOptionsSchema = z.object({
    repoRoot: z.string(),
    excludeDir: z.array(z.string()).optional(),
    noGitignore: z.boolean().optional(),
    ignorePatterns: z.array(z.string()).optional(),
    overlayIgnorePatterns: z.array(z.string()).optional(),
    contentMode: z.enum(['none', 'excerpt', 'full']).optional(),
    maxFileSizeBytes: z.number().optional(),
    maxLines: z.number().optional(),
    skipMinified: z.boolean().optional(),
    maxFiles: z.number().int().positive().optional(),
    maxBytes: z.number().int().positive().optional(),
    onBudgetExceeded: BudgetExceededBehaviorSchema.optional(),
    parseConcurrency: z.number().int().positive().optional(),
    typescriptChecker: TypeScriptCheckerModeSchema.optional(),
});
/**
 * Default fan-out for parseCodeFiles. parseCodeFile is async I/O (stat + read)
 * followed by a synchronous CPU parse; a small worker pool hides the I/O
 * latency while bounding the memory held by in-flight file contents.
 * worker_threads are explicitly out of scope: each worker would pay the
 * typescript module load cost (dwarfing small incremental updates) and
 * full-mode payloads would pay structured-clone overhead;
 * buildCodebaseIndexIsolated already provides process-level isolation.
 */
export const DEFAULT_PARSE_CONCURRENCY = Math.min(8, Math.max(2, 
// os.availableParallelism is missing on Node 18.0-18.13; fall back to cpus().
typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length));
export const ParseCodeFilesOptionsSchema = z.object({
    repoRoot: z.string(),
    contentMode: ContentModeSchema.optional(),
    maxFileSizeBytes: z.number().optional(),
    maxLines: z.number().optional(),
    skipMinified: z.boolean().optional(),
    concurrency: z.number().int().positive().optional(),
    typescriptChecker: TypeScriptCheckerModeSchema.optional(),
});
export class CodeIndexBudgetExceededError extends Error {
    name = 'CodeIndexBudgetExceededError';
    code = 'OCC_CODE_INDEX_BUDGET_EXCEEDED';
    budget;
    constructor(budget) {
        const reasons = [
            budget.maxFiles != null && budget.totalFiles > budget.maxFiles
                ? `files ${budget.totalFiles}/${budget.maxFiles}`
                : undefined,
            budget.maxBytes != null && budget.totalBytes > budget.maxBytes
                ? `bytes ${budget.totalBytes}/${budget.maxBytes}`
                : undefined,
        ]
            .filter(Boolean)
            .join(', ');
        super(`Code index budget exceeded (${reasons}).`);
        this.budget = budget;
    }
}
function makeId(prefix, ...parts) {
    return [prefix, ...parts.filter((part) => part != null)].join(':');
}
function legacySymbolId(filePath, symbol) {
    return makeId(symbol.type, filePath, symbol.name, symbol.line, symbol.containerName);
}
function exactSymbolId(filePath, symbol) {
    const span = symbol.span;
    if (!span?.exact || span.startByte == null || span.endByte == null)
        return legacySymbolId(filePath, symbol);
    return makeId('node2', symbol.type, filePath, span.startByte, span.endByte);
}
function mapPush(map, key, value) {
    const existing = map.get(key);
    if (existing)
        existing.push(value);
    else
        map.set(key, [value]);
}
function mergeCapabilities(files) {
    const capabilities = {};
    for (const file of files) {
        const spec = getLanguageSpec(file.path);
        if (!spec)
            continue;
        capabilities[spec.name] = spec.capabilities;
    }
    return capabilities;
}
function mergeParserRefusals(files) {
    const vueTemplate = {
        dynamicComponents: 0,
        unresolvedComponents: 0,
        ambiguousComponents: 0,
        unresolvedIdentifiers: 0,
        ambiguousIdentifiers: 0,
        expressionParseFailures: 0,
        unsupportedScopes: 0,
        unsupportedConstructs: 0,
    };
    let vueObserved = false;
    for (const file of files) {
        for (const counts of [file.provenance?.refusals?.vueTemplate, file.provenance?.refusals?.vueTemplateJoin]) {
            if (!counts)
                continue;
            vueObserved = true;
            for (const key of Object.keys(vueTemplate)) {
                vueTemplate[key] += counts[key];
            }
        }
    }
    return {
        dynamicDispatch: files.reduce((total, file) => total + (file.provenance?.refusals?.dynamicDispatch ?? 0), 0),
        ...(vueObserved ? { vueTemplate } : {}),
    };
}
function applyContentMode(file, contentMode) {
    if (contentMode === 'full') {
        return file;
    }
    if (contentMode === 'excerpt') {
        const excerpt = extractKeySection(file, 4_000);
        return {
            ...file,
            content: undefined,
            lines: undefined,
            excerpt,
        };
    }
    return {
        ...file,
        content: undefined,
        lines: undefined,
        excerpt: undefined,
    };
}
async function withFileSizes(filePaths, signal) {
    const statted = await mapWithConcurrency(filePaths, 32, async (filePath) => {
        try {
            const fileStat = await stat(filePath);
            return fileStat.isFile() ? { path: filePath, size: fileStat.size, mtimeMs: fileStat.mtimeMs } : null;
        }
        catch {
            // Files can disappear between discovery and stat; they are dropped from
            // the parse list here (parseCodeFiles likewise drops files that vanish
            // before parsing).
            return null;
        }
    }, signal);
    return statted.filter((file) => file !== null);
}
/**
 * Filesystem failures parseCodeFiles absorbs per file instead of rejecting
 * the whole batch: the file vanished between listing and parse (ENOENT,
 * ENOTDIR) or became unreadable (EACCES, EPERM).
 */
const RECOVERABLE_PARSE_FS_CODES = new Set(['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM']);
function isRecoverableParseFsError(error) {
    return (error instanceof Error &&
        'code' in error &&
        typeof error.code === 'string' &&
        RECOVERABLE_PARSE_FS_CODES.has(error.code));
}
function enforceBuildBudget(discovered, options) {
    const totalFiles = discovered.length;
    const totalBytes = discovered.reduce((sum, file) => sum + file.size, 0);
    const overFiles = options.maxFiles != null && totalFiles > options.maxFiles;
    const overBytes = options.maxBytes != null && totalBytes > options.maxBytes;
    if (!overFiles && !overBytes) {
        return { files: discovered.map(file => file.path) };
    }
    if ((options.onBudgetExceeded ?? 'throw') === 'throw') {
        throw new CodeIndexBudgetExceededError({
            maxFiles: options.maxFiles,
            maxBytes: options.maxBytes,
            totalFiles,
            totalBytes,
        });
    }
    const retained = [];
    let retainedBytes = 0;
    for (const file of discovered) {
        if (options.maxFiles != null && retained.length >= options.maxFiles)
            continue;
        if (options.maxBytes != null && retainedBytes + file.size > options.maxBytes)
            continue;
        retained.push(file);
        retainedBytes += file.size;
    }
    const reason = overFiles && overBytes ? 'maxFilesAndBytes' : overFiles ? 'maxFiles' : 'maxBytes';
    return {
        files: retained.map(file => file.path),
        truncated: {
            reason,
            keptFiles: retained.length,
            droppedFiles: totalFiles - retained.length,
            totalFiles,
            totalBytes,
            maxFiles: options.maxFiles,
            maxBytes: options.maxBytes,
        },
    };
}
function toCandidates(nodes) {
    if (!nodes || nodes.length === 0)
        return undefined;
    return nodes.map(node => ({
        name: node.name,
        nodeId: node.id,
        type: node.type,
        relativePath: node.relativePath,
        line: node.line,
        endLine: node.endLine,
        containerName: node.containerName,
        span: node.span,
    }));
}
/** Diagnostic source that marks assembly-derived usage-join rows apart from parser rows. */
const VUE_TEMPLATE_JOIN_SOURCE = 'vue-template-join';
function totalVueTemplateRefusalCounts(counts) {
    return counts ? Object.values(counts).reduce((total, count) => total + count, 0) : 0;
}
function ensureVueTemplateJoinRefusals(file) {
    const provenance = file.provenance;
    if (!provenance || provenance.capabilities.templateUses === undefined)
        return undefined;
    provenance.refusals ??= { dynamicDispatch: 0 };
    provenance.refusals.vueTemplateJoin ??= {
        dynamicComponents: 0,
        unresolvedComponents: 0,
        ambiguousComponents: 0,
        unresolvedIdentifiers: 0,
        ambiguousIdentifiers: 0,
        expressionParseFailures: 0,
        unsupportedScopes: 0,
        unsupportedConstructs: 0,
    };
    return provenance.refusals.vueTemplateJoin;
}
/**
 * Assembly-derived join accounting is recomputed on every assembly, so a
 * persisted survivor re-entering `assembleCodebaseIndex` starts from its
 * parser-time truth instead of compounding a previous assembly's counts.
 */
function resetVueUsageJoinAccounting(file) {
    const provenance = file.provenance;
    if (!provenance)
        return;
    if (provenance.refusals?.vueTemplateJoin)
        delete provenance.refusals.vueTemplateJoin;
    const diagnostics = provenance.resolver.diagnostics;
    if (diagnostics?.some(diagnostic => diagnostic.source === VUE_TEMPLATE_JOIN_SOURCE)) {
        provenance.resolver.diagnostics = diagnostics.filter(diagnostic => diagnostic.source !== VUE_TEMPLATE_JOIN_SOURCE);
    }
}
/**
 * Re-derives `templateUses` from the parser refusals plus this assembly's join
 * refusals. `resolver.coverage` only ever degrades: other contributors
 * (checker join misses, parser refusals) also feed it and cannot be re-proved
 * here.
 */
function finalizeVueUsageJoinAccounting(file) {
    const provenance = file.provenance;
    if (!provenance || provenance.capabilities.templateUses === undefined)
        return;
    const total = totalVueTemplateRefusalCounts(provenance.refusals?.vueTemplate) +
        totalVueTemplateRefusalCounts(provenance.refusals?.vueTemplateJoin);
    provenance.capabilities.templateUses = total === 0 ? 'full' : 'partial';
    if (total > 0)
        provenance.resolver.coverage = 'partial';
}
function recordVueUsageJoinRefusal(file, usage, ambiguous) {
    const provenance = file.provenance;
    const refusals = ensureVueTemplateJoinRefusals(file);
    if (!provenance || !refusals)
        return;
    const component = usage.usageKind === 'vue-component';
    if (component) {
        if (ambiguous)
            refusals.ambiguousComponents++;
        else
            refusals.unresolvedComponents++;
    }
    else if (ambiguous)
        refusals.ambiguousIdentifiers++;
    else
        refusals.unresolvedIdentifiers++;
    const reason = component
        ? ambiguous
            ? 'vue-template-component-ambiguous'
            : 'vue-template-component-unresolved'
        : ambiguous
            ? 'vue-template-identifier-ambiguous'
            : 'vue-template-identifier-unresolved';
    const diagnostics = provenance.resolver.diagnostics ?? [];
    const existing = diagnostics.find(diagnostic => diagnostic.source === VUE_TEMPLATE_JOIN_SOURCE && diagnostic.reason === reason);
    if (existing) {
        existing.counts = {
            hits: existing.counts?.hits ?? 0,
            misses: (existing.counts?.misses ?? 0) + 1,
        };
        return;
    }
    if (diagnostics.length >= 100) {
        provenance.resolver.diagnosticsTruncated = true;
        return;
    }
    diagnostics.push({
        message: component
            ? 'A statically bound Vue component reference did not join exactly one indexed declaration.'
            : 'A statically bound Vue template identifier did not join exactly one indexed declaration.',
        severity: 'warning',
        source: VUE_TEMPLATE_JOIN_SOURCE,
        reason,
        counts: { hits: 0, misses: 1 },
    });
    provenance.resolver.diagnostics = diagnostics;
}
const GLOBAL_BUILTIN_CALLS = new Set([
    'Array',
    'Boolean',
    'Date',
    'Error',
    'JSON',
    'Map',
    'Number',
    'Object',
    'Promise',
    'Reflect',
    'RegExp',
    'Set',
    'String',
    'Symbol',
    'console',
    'decodeURIComponent',
    'encodeURIComponent',
    'fetch',
    'isFinite',
    'isNaN',
    'parseFloat',
    'parseInt',
    'require',
    'setInterval',
    'setTimeout',
]);
const MEMBER_BUILTIN_CALLS = new Set([
    'add',
    'append',
    'appendChild',
    'assign',
    'at',
    'catch',
    'charAt',
    'clear',
    'concat',
    'delete',
    'entries',
    'every',
    'filter',
    'find',
    'findIndex',
    'flat',
    'forEach',
    'get',
    'has',
    'includes',
    'indexOf',
    'join',
    'keys',
    'log',
    'map',
    'match',
    'pop',
    'push',
    'reduce',
    'replace',
    'resolve',
    'reverse',
    'set',
    'shift',
    'slice',
    'some',
    'sort',
    'split',
    'startsWith',
    'substring',
    'then',
    'toISOString',
    'toLowerCase',
    'toString',
    'toUpperCase',
    'trim',
    'values',
]);
/**
 * Objects the language or runtime supplies. A member call on one of these can
 * never reach a declaration in this repository, so binding it to a same-named
 * method elsewhere in the index is always wrong — the defect that made
 * `path.join(...)` resolve to an unrelated class method named `join`.
 */
const BUILTIN_RECEIVERS = new Set([
    'Array',
    'Atomics',
    'BigInt',
    'Boolean',
    'Buffer',
    'Date',
    'Error',
    'Function',
    'Intl',
    'JSON',
    'Map',
    'Math',
    'Number',
    'Object',
    'Promise',
    'Proxy',
    'Reflect',
    'RegExp',
    'Set',
    'String',
    'Symbol',
    'WeakMap',
    'WeakSet',
    'console',
    'document',
    'globalThis',
    'localStorage',
    'navigator',
    'process',
    'sessionStorage',
    'window',
]);
/**
 * The identifier a receiver expression is rooted at.
 *
 * `createHash('sha256')` is rooted at `createHash`, `process.stderr` at
 * `process`. Only the root is returned, and only when the qualifier is more
 * than the root itself.
 */
function receiverRoot(qualifier) {
    const match = /^[A-Za-z_$][\w$]*/.exec(qualifier);
    return match && match[0] !== qualifier ? match[0] : undefined;
}
function classifyReceiver(receiverKind, qualifier, binding, rootBinding) {
    if (!qualifier)
        return 'none';
    if (receiverKind === 'self')
        return 'scope-self';
    if (receiverKind === 'super')
        return 'scope-super';
    if (binding) {
        if (binding.namespace)
            return binding.modulePath ? 'local-module' : 'foreign-module';
        return binding.modulePath ? 'value' : 'foreign-module';
    }
    if (BUILTIN_RECEIVERS.has(qualifier))
        return 'builtin';
    // A compound receiver is decided by its root just as firmly as a bare one:
    // `createHash('sha256')` cannot evaluate to anything OCC indexed, so neither
    // can `.update()` on it. Only the two verdicts that resolve against nothing
    // are drawn from the root — a local-module root says nothing about what
    // `ns.sub.fn()` is, so that stays an ordinary value.
    const root = receiverRoot(qualifier);
    if (root) {
        if (rootBinding)
            return rootBinding.modulePath ? 'value' : 'foreign-module';
        if (BUILTIN_RECEIVERS.has(root))
            return 'builtin';
    }
    return 'value';
}
function isBuiltinNoiseCall(call, receiver, directBinding, localSymbols) {
    if (receiver === 'none') {
        return localSymbols.length === 0 && !directBinding && GLOBAL_BUILTIN_CALLS.has(call.calleeName);
    }
    if (receiver === 'builtin')
        return MEMBER_BUILTIN_CALLS.has(call.calleeName);
    return false;
}
/**
 * Parse `files` with bounded concurrency. Both result arrays are returned in
 * input order (per-slot results are compacted after the pool drains).
 * Content-mode reduction happens at parse time, while content is still in
 * memory — excerpt extraction needs the full source text.
 */
export async function parseCodeFiles(files, options, onProgress, signal, yielder) {
    const repoRoot = options.repoRoot;
    // Path mappings are memoized across files, but each parse batch is a new
    // resolution snapshot and must observe config edits made since the prior one.
    invalidateTsConfigResolutionCache(repoRoot);
    const contentMode = options.contentMode ?? 'excerpt';
    const concurrency = options.concurrency ?? DEFAULT_PARSE_CONCURRENCY;
    // Shared monotonic counter — completion order, not slot order.
    let done = 0;
    const slots = await mapWithConcurrency(files, concurrency, async (filePath) => {
        let slot;
        try {
            const parsed = await parseCodeFile(filePath, { repoRoot }, {
                maxFileSizeBytes: options.maxFileSizeBytes,
                maxLines: options.maxLines,
                skipMinified: options.skipMinified,
            });
            if (parsed?.kind === 'parsed') {
                slot = {
                    kind: 'parsed',
                    file: applyContentMode(parsed.file, contentMode),
                    sourceIdentity: parsed.sourceIdentity,
                };
            }
            else if (parsed?.kind === 'skipped') {
                slot = {
                    kind: 'skipped',
                    skipped: {
                        path: filePath,
                        relativePath: normalizePath(path.relative(repoRoot, filePath)),
                        reason: parsed.reason,
                    },
                };
            }
            else {
                slot = { kind: 'none' };
            }
        }
        catch (error) {
            // A file deleted (or made unreadable) between listing and parse must
            // not reject the batch: drop its slot — the file simply vanishes from
            // the inputs and the next manifest diff reconciles it.
            if (!isRecoverableParseFsError(error))
                throw error;
            slot = { kind: 'none' };
        }
        done += 1;
        onProgress?.({ phase: 'parse', total: files.length, completed: done, detail: filePath });
        if (yielder)
            await yielder.tick();
        return slot;
    }, signal);
    const parsedFiles = [];
    const skippedFiles = [];
    const sourceIdentities = new Map();
    for (const slot of slots) {
        if (slot.kind === 'parsed') {
            parsedFiles.push(slot.file);
            if (slot.sourceIdentity)
                sourceIdentities.set(slot.file.relativePath, slot.sourceIdentity);
        }
        else if (slot.kind === 'skipped')
            skippedFiles.push(slot.skipped);
    }
    await enrichTypeScriptSemantics(parsedFiles, {
        mode: options.typescriptChecker ?? 'preferred',
        signal,
    });
    return { parsedFiles, skippedFiles, sourceIdentities };
}
export async function buildCodebaseIndex(options, onProgress, signal) {
    const { index } = await buildCodebaseIndexWithStats(options, onProgress, signal);
    return index;
}
/**
 * Like {@link buildCodebaseIndex}, but also returns the pre-parse stat
 * snapshot of the budgeted file set so callers persisting a freshness
 * manifest don't have to re-stat every file after the build.
 */
export async function buildCodebaseIndexWithStats(options, onProgress, signal) {
    const repoRoot = path.resolve(options.repoRoot);
    const contentMode = options.contentMode ?? 'excerpt';
    const cooperative = isCooperative(options.cooperative);
    const yielder = cooperative ? createYielder(options.cooperative, signal) : undefined;
    abortIfNeeded(signal);
    const discovered = await discoverCodeFiles(repoRoot, {
        excludeDir: options.excludeDir,
        noGitignore: options.noGitignore,
        ignorePatterns: options.ignorePatterns,
        overlayIgnorePatterns: options.overlayIgnorePatterns,
        signal,
    });
    abortIfNeeded(signal);
    const discoveredWithSizes = await withFileSizes(discovered, signal);
    const budgeted = enforceBuildBudget(discoveredWithSizes, options);
    onProgress?.({
        phase: 'discover',
        total: discovered.length,
        completed: discovered.length,
        detail: 'File discovery complete',
    });
    const { parsedFiles, skippedFiles, sourceIdentities } = await parseCodeFiles(budgeted.files, {
        repoRoot,
        contentMode,
        maxFileSizeBytes: options.maxFileSizeBytes,
        maxLines: options.maxLines,
        skipMinified: options.skipMinified,
        concurrency: options.parseConcurrency,
        typescriptChecker: options.typescriptChecker ?? 'preferred',
    }, onProgress, signal, yielder);
    abortIfNeeded(signal);
    const input = {
        repoRoot,
        contentMode,
        parsedFiles,
        skippedFiles,
        truncated: budgeted.truncated,
    };
    const index = yielder
        ? await assembleCodebaseIndexCooperative(input, yielder, onProgress, signal)
        : assembleCodebaseIndex(input, onProgress, signal);
    const budgetedPaths = new Set(budgeted.files);
    const fileStats = discoveredWithSizes
        .filter(file => budgetedPaths.has(file.path))
        .map(file => {
        const relativePath = normalizePath(path.relative(repoRoot, file.path));
        const identity = sourceIdentities.get(relativePath);
        return {
            relativePath,
            size: file.size,
            mtimeMs: file.mtimeMs,
            ...(identity?.size === file.size ? { sha256: identity.sha256 } : {}),
        };
    });
    attachSourceInlineManifest(index, fileStats);
    return { index, fileStats };
}
/**
 * Assemble the node/edge graph from already-parsed files. Synchronous —
 * assembly has no awaits — so it can be reused verbatim by incremental
 * updates that splice fresh parses into a cached file set.
 */
function* assembleSteps(input, onProgress, signal) {
    const { repoRoot, contentMode, parsedFiles, skippedFiles } = input;
    const nodes = [];
    const nodeById = new Map();
    const edges = [];
    const functionsByName = new Map();
    const classNodesByName = new Map();
    const interfaceNodesByName = new Map();
    const filesByPath = new Map();
    const modulesByName = new Map();
    const topLevelSymbolsByFile = new Map();
    const moduleSurfaceByFile = new Map();
    const allPaths = new Set(parsedFiles.map(file => file.path));
    const moduleNodeByPath = new Map();
    const symbolByIdentity = new Map();
    const symbolByLocalIdentity = new Map();
    const memberOwnerByNodeId = new Map();
    const parsedSymbolByNodeId = new Map();
    const methodNodesByClassId = new Map();
    const parentNodesByClassId = new Map();
    const pushNode = (node) => {
        nodes.push(node);
        nodeById.set(node.id, node);
    };
    for (const file of parsedFiles) {
        abortIfNeeded(signal);
        const fileNode = {
            id: makeId('file', file.path),
            type: 'file',
            name: path.basename(file.path),
            path: file.path,
            relativePath: file.relativePath,
            line: 1,
            language: file.language,
            moduleName: file.moduleName,
        };
        const moduleNode = {
            id: makeId('module', file.path),
            type: 'module',
            name: file.moduleName,
            path: file.path,
            relativePath: file.relativePath,
            line: 1,
            language: file.language,
            moduleName: file.moduleName,
        };
        pushNode(fileNode);
        pushNode(moduleNode);
        filesByPath.set(file.path, fileNode);
        moduleNodeByPath.set(file.path, moduleNode);
        mapPush(modulesByName, file.moduleName, moduleNode);
        mapPush(modulesByName, path.basename(file.moduleName), moduleNode);
        edges.push({
            id: makeId('contains', fileNode.id, moduleNode.id),
            type: 'contains',
            from: fileNode.id,
            to: moduleNode.id,
            fromPath: file.path,
            toPath: file.path,
            status: 'resolved',
            resolutionBasis: 'containment',
            resolutionCoverage: 'full',
            targetName: moduleNode.name,
        });
        const symbolIndex = new Map();
        const symbolIds = new Map(file.symbols.map(symbol => [symbol, exactSymbolId(file.path, symbol)]));
        const symbolByLocalIdForFile = new Map(file.symbols.flatMap(symbol => (symbol.localId ? [[symbol.localId, symbol]] : [])));
        for (const symbol of file.symbols) {
            // Containers can be classes or interface-like types (interfaces, traits,
            // protocols). Rebuild the container id exactly as the container node's own
            // id is constructed below so member `contains` edges resolve to a real node.
            const namedContainers = symbol.containerName
                ? file.symbols.filter(candidate => (candidate.type === 'class' || candidate.type === 'interface') && candidate.name === symbol.containerName)
                : [];
            const localContainer = symbol.containerLocalId ? symbolByLocalIdForFile.get(symbol.containerLocalId) : undefined;
            const container = localContainer && (localContainer.type === 'class' || localContainer.type === 'interface')
                ? localContainer
                : namedContainers.length === 1
                    ? namedContainers[0]
                    : undefined;
            const containerId = container
                ? (symbolIds.get(container) ?? legacySymbolId(file.path, container))
                : moduleNode.id;
            const legacyNodeId = legacySymbolId(file.path, symbol);
            const node = {
                id: symbolIds.get(symbol) ?? legacyNodeId,
                legacyNodeId: legacyNodeId === (symbolIds.get(symbol) ?? legacyNodeId) ? undefined : legacyNodeId,
                type: symbol.type,
                name: symbol.name,
                path: file.path,
                relativePath: file.relativePath,
                line: symbol.line,
                endLine: symbol.endLine,
                startColumn: symbol.startColumn,
                endColumn: symbol.endColumn,
                span: symbol.span,
                language: file.language,
                moduleName: file.moduleName,
                containerId,
                containerName: symbol.containerName,
                // Compiler-backed parsers retain the declaration's typed callable
                // shape separately from the legacy parameter-name signature. Prefer
                // that richer contract on graph nodes; other parsers do not set
                // fullSignature and keep their existing representation.
                signature: symbol.fullSignature ?? symbol.signature,
            };
            pushNode(node);
            // Stamp the graph linkage onto the file symbol so index consumers can
            // jump from files[].symbols[] to nodes without a scan. Node ids are
            // deterministic, so re-assembly over cached parses stamps identically.
            symbol.nodeId = node.id;
            symbol.legacyNodeId = node.legacyNodeId;
            parsedSymbolByNodeId.set(node.id, symbol);
            symbolByIdentity.set(legacyNodeId, node);
            if (symbol.localId)
                symbolByLocalIdentity.set(`${file.path}\u0000${symbol.localId}`, node);
            mapPush(symbolIndex, symbol.name, node);
            edges.push({
                id: makeId('contains', containerId, node.id),
                type: 'contains',
                from: containerId,
                to: node.id,
                fromPath: file.path,
                toPath: file.path,
                line: symbol.line,
                span: symbol.span,
                status: 'resolved',
                resolutionBasis: 'containment',
                resolutionCoverage: symbol.span?.exact ? 'full' : 'partial',
                targetName: node.name,
            });
            if (node.type === 'function') {
                mapPush(functionsByName, node.name, node);
                // Kept beside the graph rather than on `CodeNode`: this is resolution
                // bookkeeping, and the node shape is a published contract.
                if (symbol.isMember) {
                    memberOwnerByNodeId.set(node.id, `${file.path}\u0000${symbol.memberOwner ?? ''}`);
                }
                if (node.containerName) {
                    const parentNode = node.containerId ? nodeById.get(node.containerId) : undefined;
                    if (parentNode && (parentNode.type === 'class' || parentNode.type === 'interface')) {
                        mapPush(methodNodesByClassId, parentNode.id, node);
                    }
                }
            }
            else if (node.type === 'class') {
                mapPush(classNodesByName, node.name, node);
            }
            else if (node.type === 'interface') {
                mapPush(interfaceNodesByName, node.name, node);
            }
        }
        topLevelSymbolsByFile.set(file.path, symbolIndex);
        const moduleSurface = new Map();
        const moduleScopeIds = new Set((file.scopes ?? []).filter(scope => scope.kind === 'module').map(scope => scope.id));
        const isModuleDeclaration = (node) => {
            const parsed = parsedSymbolByNodeId.get(node.id);
            if (!parsed)
                return false;
            if (parsed.containerName || parsed.isMember || memberOwnerByNodeId.has(node.id))
                return false;
            // Legacy ParsedFile producers have no scope table. Their explicit export
            // flags remain the compatibility proof; parser-contract V2 records must
            // identify the module scope before a declaration enters the public API.
            return parsed.scopeId === undefined ? file.scopes === undefined : moduleScopeIds.has(parsed.scopeId);
        };
        if (file.exports) {
            for (const parsedExport of file.exports) {
                if (parsedExport.typeOnly ||
                    !parsedExport.localName ||
                    parsedExport.kind === 're-export' ||
                    parsedExport.kind === 'star' ||
                    parsedExport.kind === 'namespace') {
                    continue;
                }
                for (const node of symbolIndex.get(parsedExport.localName) ?? []) {
                    if (isModuleDeclaration(node)) {
                        mapPush(moduleSurface, parsedExport.exportedName, node);
                    }
                }
            }
            for (const symbol of file.symbols)
                symbol.exported = false;
            for (const nodes of moduleSurface.values()) {
                for (const node of nodes) {
                    const parsed = parsedSymbolByNodeId.get(node.id);
                    if (parsed)
                        parsed.exported = true;
                }
            }
        }
        else {
            // Compatibility for legacy ParsedFile producers that predate an explicit
            // export table.
            for (const symbol of file.symbols) {
                if (!symbol.exported || symbol.containerName || symbol.isMember || !symbol.nodeId)
                    continue;
                if (file.scopes && !moduleScopeIds.has(symbol.scopeId ?? ''))
                    continue;
                const node = nodeById.get(symbol.nodeId);
                if (node)
                    mapPush(moduleSurface, symbol.name, node);
            }
        }
        moduleSurfaceByFile.set(file.path, moduleSurface);
        yield;
    }
    const reExportsByFile = new Map();
    const starReExportsByFile = new Map();
    for (const file of parsedFiles) {
        abortIfNeeded(signal);
        for (const parsedExport of file.exports ?? []) {
            if (parsedExport.typeOnly)
                continue;
            if (!parsedExport.resolvedPath || !allPaths.has(parsedExport.resolvedPath))
                continue;
            if (parsedExport.kind === 'star') {
                mapPush(starReExportsByFile, file.path, parsedExport.resolvedPath);
                continue;
            }
            if (parsedExport.kind !== 're-export')
                continue;
            let fileMap = reExportsByFile.get(file.path);
            if (!fileMap) {
                fileMap = new Map();
                reExportsByFile.set(file.path, fileMap);
            }
            fileMap.set(parsedExport.exportedName, {
                path: parsedExport.resolvedPath,
                sourceName: parsedExport.sourceName ?? parsedExport.exportedName,
            });
        }
        for (const fileImport of file.imports) {
            if (!fileImport.isReExport || !fileImport.resolvedPath || !allPaths.has(fileImport.resolvedPath))
                continue;
            let fileMap = reExportsByFile.get(file.path);
            if (!fileMap) {
                fileMap = new Map();
                reExportsByFile.set(file.path, fileMap);
            }
            for (const binding of fileImport.bindings) {
                if (binding.typeOnly)
                    continue;
                if (binding.isNamespace) {
                    mapPush(starReExportsByFile, file.path, fileImport.resolvedPath);
                }
                else {
                    // For aliased re-exports (`export { foo as baz }`) the source module
                    // exports the original name, so resolution must look it up by
                    // importedName, not the outward alias.
                    fileMap.set(binding.localName, {
                        path: fileImport.resolvedPath,
                        sourceName: binding.importedName ?? binding.localName,
                    });
                }
            }
        }
        yield;
    }
    for (let fi = 0; fi < parsedFiles.length; fi++) {
        abortIfNeeded(signal);
        const file = parsedFiles[fi];
        const fileNode = filesByPath.get(file.path);
        if (!fileNode)
            continue;
        onProgress?.({ phase: 'index', total: parsedFiles.length, completed: fi + 1, detail: file.relativePath });
        const bindings = new Map();
        const parentByScope = new Map((file.scopes ?? []).map(scope => [scope.id, scope.parentId]));
        const compilerFamily = file.language === 'typescript' || file.language === 'javascript' || file.language === 'vue';
        const scopeDistance = (fromScope, declarationScope) => {
            if (!fromScope || !declarationScope)
                return undefined;
            let cursor = fromScope;
            let distance = 0;
            const seen = new Set();
            while (cursor && !seen.has(cursor)) {
                if (cursor === declarationScope)
                    return distance;
                seen.add(cursor);
                cursor = parentByScope.get(cursor);
                distance += 1;
            }
            return undefined;
        };
        for (const fileImport of file.imports) {
            const resolvedPath = fileImport.resolvedPath && allPaths.has(fileImport.resolvedPath) ? fileImport.resolvedPath : undefined;
            const importKind = resolvedPath ? 'local' : fileImport.kind === 'external' ? 'external' : 'unresolved';
            let targetNode = resolvedPath ? moduleNodeByPath.get(resolvedPath) : undefined;
            if (!targetNode && importKind === 'external') {
                targetNode = {
                    id: makeId('module-external', fileImport.specifier),
                    type: 'module',
                    name: fileImport.specifier,
                    path: fileImport.specifier,
                    relativePath: fileImport.specifier,
                    line: fileImport.line,
                    language: 'external',
                    moduleName: fileImport.specifier,
                    external: true,
                };
                const existingNode = nodeById.get(targetNode.id);
                if (!existingNode) {
                    pushNode(targetNode);
                    mapPush(modulesByName, targetNode.name, targetNode);
                }
                else {
                    targetNode = existingNode;
                }
            }
            edges.push({
                id: makeId('imports', fileNode.id, targetNode?.id, fileImport.line, fileImport.specifier),
                type: 'imports',
                from: fileNode.id,
                to: targetNode?.id,
                fromPath: file.path,
                toPath: resolvedPath ?? targetNode?.path,
                line: fileImport.line,
                span: fileImport.span,
                status: importKind === 'unresolved' ? 'unresolved' : 'resolved',
                resolutionBasis: resolvedPath ? 'module-resolution' : importKind === 'external' ? 'module-resolution' : 'none',
                resolutionCoverage: resolvedPath || importKind === 'external' ? 'full' : 'unavailable',
                targetName: targetNode?.name ?? fileImport.specifier,
                specifier: fileImport.specifier,
                importKind,
            });
            for (const binding of fileImport.bindings) {
                bindings.set(binding.localName, {
                    importedName: binding.importedName,
                    modulePath: binding.typeOnly ? undefined : resolvedPath,
                    namespace: binding.isNamespace,
                    typeOnly: binding.typeOnly,
                    specifier: fileImport.specifier,
                });
            }
        }
        for (const inheritance of file.inheritances) {
            const classSymbols = file.symbols.filter(symbol => (symbol.type === 'class' || symbol.type === 'interface') &&
                symbol.name === inheritance.className &&
                symbol.line === inheritance.classLine &&
                (!inheritance.scopeId || symbol.scopeId === inheritance.scopeId));
            const classSymbol = classSymbols.length === 1 ? classSymbols[0] : undefined;
            const classNode = classSymbol?.localId
                ? symbolByLocalIdentity.get(`${file.path}\u0000${classSymbol.localId}`)
                : classSymbol
                    ? symbolByIdentity.get(legacySymbolId(file.path, classSymbol))
                    : undefined;
            if (!classNode)
                continue;
            const edgeType = inheritance.kind === 'implements' ? 'implements' : 'inherits';
            const candidatePool = edgeType === 'implements'
                ? interfaceNodesByName
                : classNode.type === 'interface'
                    ? interfaceNodesByName
                    : classNodesByName;
            const localCandidates = candidatePool.get(inheritance.baseName) ?? [];
            const exactLocal = localCandidates.filter(candidate => candidate.path === file.path);
            const accessibleLocal = exactLocal.filter(candidate => {
                const parsed = parsedSymbolByNodeId.get(candidate.id);
                return scopeDistance(inheritance.scopeId, parsed?.scopeId) !== undefined;
            });
            const semanticInheritance = semanticInheritanceResolution(inheritance);
            const semanticTarget = semanticInheritance?.target;
            const semanticNode = semanticTarget
                ? ((semanticTarget.localId
                    ? symbolByLocalIdentity.get(`${semanticTarget.path}\u0000${semanticTarget.localId}`)
                    : undefined) ??
                    symbolByIdentity.get(makeId(semanticTarget.type, semanticTarget.path, semanticTarget.name, semanticTarget.line, semanticTarget.containerName)))
                : undefined;
            const semanticCandidates = (semanticInheritance?.candidates ?? [])
                .map(target => (target.localId ? symbolByLocalIdentity.get(`${target.path}\u0000${target.localId}`) : undefined) ??
                symbolByIdentity.get(makeId(target.type, target.path, target.name, target.line, target.containerName)))
                .filter((node) => !!node);
            // A checker result is authoritative, including a refusal. Scope ancestry
            // alone cannot rule out a parameter/local binding that shadows the base
            // name, so syntax-only matches remain candidates rather than proof.
            const candidates = semanticNode
                ? [semanticNode]
                : semanticInheritance?.status === 'ambiguous'
                    ? semanticCandidates
                    : semanticInheritance
                        ? []
                        : accessibleLocal.length > 0
                            ? accessibleLocal
                            : exactLocal.length > 0
                                ? exactLocal
                                : localCandidates;
            const trusted = !!semanticNode;
            const resolved = trusted && candidates.length === 1 ? candidates[0] : undefined;
            const trustedResolved = trusted ? resolved : undefined;
            if (edgeType === 'inherits' && trustedResolved) {
                mapPush(parentNodesByClassId, classNode.id, trustedResolved);
            }
            edges.push({
                id: makeId(edgeType, classNode.id, inheritance.baseName, inheritance.line),
                type: edgeType,
                from: classNode.id,
                to: trustedResolved?.id,
                fromPath: file.path,
                toPath: trustedResolved?.path,
                line: inheritance.line,
                span: inheritance.span,
                status: trustedResolved ? 'resolved' : semanticInheritance?.status === 'ambiguous' ? 'ambiguous' : 'unresolved',
                resolutionBasis: semanticInheritance
                    ? 'typescript-checker'
                    : trustedResolved
                        ? 'inheritance'
                        : candidates.length > 0
                            ? 'heuristic-name'
                            : 'none',
                resolutionCoverage: trustedResolved ? 'full' : 'partial',
                targetName: inheritance.baseName,
                candidates: trustedResolved ? undefined : toCandidates(candidates),
            });
        }
        const visitedClassMethods = new Map();
        const resolveClassMethods = (classNode, methodName, includeParents) => {
            if (!classNode)
                return [];
            const visitKey = `${classNode.id}:${methodName}:${includeParents}`;
            const cached = visitedClassMethods.get(visitKey);
            if (cached)
                return cached;
            const ownMethods = (methodNodesByClassId.get(classNode.id) ?? []).filter(node => node.name === methodName);
            if (!includeParents || ownMethods.length > 0) {
                visitedClassMethods.set(visitKey, ownMethods);
                return ownMethods;
            }
            const parents = parentNodesByClassId.get(classNode.id) ?? [];
            const inherited = [];
            for (const parentNode of parents) {
                inherited.push(...resolveClassMethods(parentNode, methodName, true));
            }
            const unique = inherited.filter((node, index, array) => array.findIndex(candidate => candidate.id === node.id) === index);
            visitedClassMethods.set(visitKey, unique);
            return unique;
        };
        /** Symbols a module positively exports under `name`, following barrels. */
        const moduleSymbols = (modulePath, name, seen = new Set()) => {
            const visitKey = `${modulePath}\u0000${name}`;
            if (seen.has(visitKey))
                return [];
            seen.add(visitKey);
            const direct = moduleSurfaceByFile.get(modulePath)?.get(name) ?? [];
            if (direct.length > 0)
                return direct;
            const reExport = reExportsByFile.get(modulePath)?.get(name);
            if (reExport)
                return moduleSymbols(reExport.path, reExport.sourceName, seen);
            if (name === 'default')
                return [];
            const throughStars = (starReExportsByFile.get(modulePath) ?? []).flatMap(sourcePath => moduleSymbols(sourcePath, name, seen));
            return throughStars.filter((node, index, array) => array.findIndex(candidate => candidate.id === node.id) === index);
        };
        resetVueUsageJoinAccounting(file);
        for (const usage of file.usages ?? []) {
            const sourceNode = symbolByLocalIdentity.get(`${file.path}\u0000${usage.sourceLocalId}`);
            if (!sourceNode) {
                recordVueUsageJoinRefusal(file, usage, false);
                continue;
            }
            let candidates = [];
            if (usage.targetLocalId) {
                const target = symbolByLocalIdentity.get(`${file.path}\u0000${usage.targetLocalId}`);
                if (target)
                    candidates = [target];
            }
            else {
                const binding = bindings.get(usage.targetName);
                if (binding?.modulePath && !binding.namespace && !binding.typeOnly) {
                    candidates = moduleSymbols(binding.modulePath, binding.importedName ?? usage.targetName);
                }
            }
            candidates = candidates.filter((node, index, array) => array.findIndex(candidate => candidate.id === node.id) === index);
            if (candidates.length !== 1) {
                recordVueUsageJoinRefusal(file, usage, candidates.length > 1);
                continue;
            }
            const target = candidates[0];
            edges.push({
                id: makeId('uses', sourceNode.id, usage.usageKind, usage.span.startByte, usage.span.endByte, target.id),
                type: 'uses',
                from: sourceNode.id,
                to: target.id,
                fromPath: file.path,
                toPath: target.path,
                line: usage.line,
                span: usage.span,
                status: 'resolved',
                resolutionBasis: usage.resolutionBasis,
                resolutionCoverage: 'full',
                targetName: usage.targetName,
                detail: usage.referenceName,
                usageKind: usage.usageKind,
            });
        }
        finalizeVueUsageJoinAccounting(file);
        const inexactCallOccurrences = new Map();
        for (const call of file.calls) {
            const callerId = makeId('function', file.path, call.callerName, call.callerLine, call.callerContainerName);
            const callerNode = (call.callerLocalId ? symbolByLocalIdentity.get(`${file.path}\u0000${call.callerLocalId}`) : undefined) ??
                symbolByIdentity.get(callerId);
            if (!callerNode)
                continue;
            let localSymbols = (topLevelSymbolsByFile.get(file.path)?.get(call.calleeName) ?? []).filter(node => !node.containerName && !memberOwnerByNodeId.has(node.id));
            const hasScopeEvidence = !!call.scopeId && !!file.scopes;
            if (hasScopeEvidence) {
                const reachable = localSymbols
                    .map(node => ({ node, distance: scopeDistance(call.scopeId, parsedSymbolByNodeId.get(node.id)?.scopeId) }))
                    .filter((entry) => entry.distance !== undefined);
                const nearest = reachable.reduce((minimum, entry) => Math.min(minimum, entry.distance), Number.POSITIVE_INFINITY);
                localSymbols = reachable.filter(entry => entry.distance === nearest).map(entry => entry.node);
            }
            const qualifierBinding = call.qualifier ? bindings.get(call.qualifier) : undefined;
            // Only an unqualified call can be the imported name itself: `a.b()` names
            // a member of `a`, never an import that happens to be called `b`.
            const directBinding = call.qualifier ? undefined : bindings.get(call.calleeName);
            const qualifierRoot = call.qualifier ? receiverRoot(call.qualifier) : undefined;
            const receiver = classifyReceiver(call.receiverKind, call.qualifier, qualifierBinding, qualifierRoot ? bindings.get(qualifierRoot) : undefined);
            const callerContainer = callerNode.containerId ? nodeById.get(callerNode.containerId) : undefined;
            const callerClassNode = callerContainer && (callerContainer.type === 'class' || callerContainer.type === 'interface')
                ? callerContainer
                : undefined;
            const receiverDomainCompatible = (candidate) => {
                if (!compilerFamily)
                    return true;
                const callerSymbol = parsedSymbolByNodeId.get(callerNode.id);
                const candidateSymbol = parsedSymbolByNodeId.get(candidate.id);
                return !!callerSymbol?.isStatic === !!candidateSymbol?.isStatic;
            };
            let candidates = [];
            let resolutionBasis = 'none';
            let trusted = false;
            const semanticCall = semanticCallResolution(call);
            const semanticTarget = semanticCall?.target;
            const semanticNode = semanticTarget
                ? ((semanticTarget.localId
                    ? symbolByLocalIdentity.get(`${semanticTarget.path}\u0000${semanticTarget.localId}`)
                    : undefined) ??
                    symbolByIdentity.get(makeId(semanticTarget.type, semanticTarget.path, semanticTarget.name, semanticTarget.line, semanticTarget.containerName)))
                : undefined;
            // Checker evidence is evaluated before the compatibility noise filter:
            // application methods named `get`, `map`, or `find` are not built-ins
            // when the checker points at an indexed declaration.
            if (!semanticCall && isBuiltinNoiseCall(call, receiver, directBinding, localSymbols))
                continue;
            if (semanticNode) {
                candidates = [semanticNode];
                resolutionBasis = 'typescript-checker';
                trusted = true;
            }
            else if (semanticCall?.status === 'ambiguous') {
                candidates = (semanticCall.candidates ?? [])
                    .map(target => (target.localId ? symbolByLocalIdentity.get(`${target.path}\u0000${target.localId}`) : undefined) ??
                    symbolByIdentity.get(makeId(target.type, target.path, target.name, target.line, target.containerName)))
                    .filter((node) => !!node);
                resolutionBasis = 'typescript-checker';
            }
            else if (semanticCall &&
                !(semanticCall.status === 'unresolved' && (receiver === 'scope-self' || receiver === 'scope-super'))) {
                // A checker-backed refusal is authoritative for name/import/value
                // lookup. Parser-proved `this`/`super` ownership is an independent,
                // narrower proof: TypeScript deliberately types object-literal `this`
                // as `any` when noImplicitThis is disabled, so the checker can have no
                // symbol even though exact sibling ownership is known.
                if (receiver === 'local-module' && qualifierBinding?.modulePath) {
                    candidates = moduleSymbols(qualifierBinding.modulePath, call.calleeName);
                }
                else if (receiver === 'value') {
                    const methods = (functionsByName.get(call.calleeName) ?? []).filter(node => node.containerName || memberOwnerByNodeId.has(node.id));
                    const sameFile = methods.filter(node => node.path === file.path);
                    candidates = sameFile.length > 0 ? sameFile : methods;
                }
                else if (receiver === 'none') {
                    if (directBinding?.modulePath) {
                        candidates = moduleSymbols(directBinding.modulePath, directBinding.importedName ?? call.calleeName);
                    }
                    else if (!directBinding) {
                        candidates = localSymbols.length > 0 ? localSymbols : (functionsByName.get(call.calleeName) ?? []);
                    }
                }
                else {
                    candidates = [];
                }
                resolutionBasis = 'typescript-checker';
            }
            else if (receiver === 'scope-self') {
                candidates = resolveClassMethods(callerClassNode, call.calleeName, true).filter(receiverDomainCompatible);
                resolutionBasis = 'receiver-self';
                // `this.other()` inside an object literal or a class expression has no
                // class *declaration* to resolve against, so the lookup above comes back
                // empty and the edge would be dropped. What `this` names there is the
                // enclosing body, so its own members are the admissible candidates —
                // and only those. Matching on "same file and not in a class" instead
                // would let one literal's `this.helper()` bind to a different literal's
                // `helper`, which is the bare-name defect wearing a smaller hat.
                if (candidates.length === 0 && !callerClassNode) {
                    const owner = memberOwnerByNodeId.get(callerNode.id);
                    candidates = owner
                        ? (functionsByName.get(call.calleeName) ?? []).filter(node => memberOwnerByNodeId.get(node.id) === owner)
                        : [];
                    candidates = candidates.filter(receiverDomainCompatible);
                }
                trusted = candidates.length === 1;
            }
            else if (receiver === 'scope-super') {
                resolutionBasis = 'receiver-super';
                const parents = callerClassNode ? (parentNodesByClassId.get(callerClassNode.id) ?? []) : [];
                for (const parentNode of parents) {
                    candidates.push(...resolveClassMethods(parentNode, call.calleeName, true).filter(receiverDomainCompatible));
                }
                trusted = candidates.length === 1;
            }
            else if (receiver === 'local-module' && qualifierBinding?.modulePath) {
                candidates = moduleSymbols(qualifierBinding.modulePath, call.calleeName);
                resolutionBasis = 'explicit-import';
                trusted = false;
            }
            else if (receiver === 'foreign-module' || receiver === 'builtin') {
                // The receiver is an external module or a runtime object, so nothing
                // OCC indexed can be the callee. An unresolved edge is the honest
                // answer; a bare-name match here is the E1 defect.
                candidates = [];
                resolutionBasis = 'none';
            }
            else if (receiver === 'value') {
                // `receiver.method()`: whatever the receiver holds, the callee is a
                // member, so only members are admissible — never a free function that
                // merely shares the name.
                //
                // Membership is not the same question as having a `containerName`. The
                // members of an object literal — how a factory usually exposes an API,
                // and the prevailing style in this codebase — have no enclosing class
                // to name, so testing the container alone drops every call into them.
                const methods = (functionsByName.get(call.calleeName) ?? []).filter(node => node.containerName || memberOwnerByNodeId.has(node.id));
                const sameFile = methods.filter(node => node.path === file.path);
                candidates = sameFile.length > 0 ? sameFile : methods;
                resolutionBasis = candidates.length > 0 ? 'heuristic-name' : 'none';
            }
            else {
                if (directBinding?.modulePath) {
                    candidates = moduleSymbols(directBinding.modulePath, directBinding.importedName ?? call.calleeName);
                    resolutionBasis = 'explicit-import';
                    trusted = false;
                }
                // An explicit import that names no exported declaration must not fall
                // through to an unrelated local/global name.
                if (!directBinding && candidates.length === 0) {
                    candidates = localSymbols;
                    resolutionBasis = candidates.length > 0 ? (hasScopeEvidence ? 'lexical-scope' : 'heuristic-name') : 'none';
                    // Scope ancestry proves visibility, but these parsers do not yet
                    // enumerate every parameter/local/block binding that could shadow
                    // the name. Keep the candidate without certifying the edge.
                    trusted = false;
                }
                // A bare call names a free function. A member — of a class or of an
                // object literal — cannot be reached without a receiver, so it is not a
                // candidate. `containerName` alone does not exclude a literal's
                // members, which is exactly what the member-owner map is for.
                if (!directBinding && candidates.length === 0) {
                    // Keep repository-wide name matches only as actionable candidates.
                    // They may include members because the point is discovery, not a
                    // claim that a bare call can reach them.
                    candidates = functionsByName.get(call.calleeName) ?? [];
                    resolutionBasis = candidates.length > 0 ? 'heuristic-name' : 'none';
                }
            }
            const resolved = trusted && candidates.length === 1 ? candidates[0] : undefined;
            const provenAmbiguity = semanticCall?.status === 'ambiguous' ||
                ((receiver === 'scope-self' || receiver === 'scope-super') && candidates.length > 1);
            const legacyCallId = makeId('calls', callerNode.id, call.calleeName, call.line);
            const occurrence = inexactCallOccurrences.get(legacyCallId) ?? 0;
            inexactCallOccurrences.set(legacyCallId, occurrence + 1);
            const callEdgeId = occurrence === 0
                ? legacyCallId
                : call.span?.exact && call.span.startByte != null && call.span.endByte != null
                    ? makeId(legacyCallId, 'bytes', call.span.startByte, call.span.endByte, occurrence)
                    : makeId(legacyCallId, occurrence);
            edges.push({
                id: callEdgeId,
                type: 'calls',
                from: callerNode.id,
                to: resolved?.id,
                fromPath: file.path,
                toPath: resolved?.path,
                line: call.line,
                span: call.span,
                status: resolved ? 'resolved' : provenAmbiguity ? 'ambiguous' : 'unresolved',
                resolutionBasis,
                resolutionCoverage: resolved ? 'full' : 'partial',
                targetName: call.calleeName,
                detail: call.qualifier,
                candidates: resolved ? undefined : toCandidates(candidates),
            });
        }
        yield;
    }
    return {
        schemaVersion: CODEBASE_INDEX_SCHEMA_VERSION,
        repoRoot,
        contentMode,
        files: parsedFiles.map(file => ({
            ...file,
            relativePath: normalizePath(path.relative(repoRoot, file.path)),
            moduleName: getModuleName(repoRoot, file.path),
        })),
        nodes,
        edges,
        capabilities: Object.fromEntries(Object.entries(mergeCapabilities(parsedFiles)).map(([language, capabilities]) => [
            language,
            { ...capabilities, content: contentMode === 'full' },
        ])),
        parserRefusals: mergeParserRefusals(parsedFiles),
        skippedFiles,
        truncated: input.truncated,
    };
}
export function assembleCodebaseIndex(input, onProgress, signal) {
    const steps = assembleSteps(input, onProgress, signal);
    let result = steps.next();
    while (!result.done) {
        result = steps.next();
    }
    return result.value;
}
export async function assembleCodebaseIndexCooperative(input, yielder, onProgress, signal) {
    const steps = assembleSteps(input, onProgress, signal);
    let result = steps.next();
    while (!result.done) {
        await yielder.tick();
        result = steps.next();
    }
    return result.value;
}
//# sourceMappingURL=build.js.map