/**
 * `ScopeExtractor` — the central, source-agnostic driver that turns a
 * language provider's `CaptureMatch[]` into a `ParsedFile`
 * (RFC §5.3 + §3.2 Phase 1; Ring 2 PKG #919).
 *
 * Exactly one entry point: `extract(matches, filePath, provider) → ParsedFile`.
 * Runs a five-pass pipeline over the matches. Each pass is internal; the
 * public contract is the output `ParsedFile`.
 *
 * ## Design principles
 *
 *   - **Source-agnostic.** Consumes `CaptureMatch[]` from providers;
 *     doesn't know whether they came from tree-sitter queries or COBOL's
 *     regex tagger. No `Tree` / `SyntaxNode` types leak into this file.
 *   - **One AST walk per language.** Providers do the AST walk inside
 *     their `emitScopeCaptures` hook; this driver does zero further
 *     traversal — it consumes captures only.
 *   - **Pure-ish.** The extractor itself is pure (same matches →
 *     same ParsedFile) when providers are pure. No side effects, no I/O.
 *   - **Centralized invariant enforcement.** Structural invariants on the
 *     scope tree (non-module has parent; parent contains child; siblings
 *     don't overlap) are enforced by `buildScopeTree` from Ring 2 SHARED
 *     (#912). Malformed inputs throw `ScopeTreeInvariantError`.
 *
 * ## The five passes
 *
 *   1. **Build scope tree.** Walk `@scope.*` matches. For each, consult
 *      `provider.resolveScopeKind` (default: suffix of the capture name).
 *      Derive parent by lexical-range containment. Hand the resulting
 *      `Scope[]` to `buildScopeTree` for validation.
 *   2. **Attach declarations + local bindings.** Walk `@declaration.*`
 *      matches. For each, build a `SymbolDefinition` and attach it to
 *      `provider.bindingScopeFor` (default: innermost containing scope)
 *      as `ownedDefs` + a local `BindingRef { origin: 'local' }`.
 *   3. **Collect raw imports.** Walk `@import.*` matches. Call
 *      `provider.interpretImport` per match; attach the returned
 *      `ParsedImport` to the ParsedFile (not to any `Scope` — finalize
 *      reconstructs the owning scope via `provider.importOwningScope`
 *      during Phase 2).
 *   4. **Collect type bindings.** Walk `@type-binding.*` matches. Call
 *      `provider.interpretTypeBinding` per match. Attach the resulting
 *      `TypeRef` to the innermost containing scope's `typeBindings`
 *      (or override via `provider.bindingScopeFor` if set).
 *   5. **Collect reference sites.** Walk `@reference.*` matches. Emit
 *      one `ReferenceSite` per match. Classify call form via
 *      `provider.classifyCallForm` (default: the capture's sub-tag if
 *      present; else `'free'`).
 *
 * ## What gets attached where
 *
 *   - `Scope.bindings`     — **local bindings only** at this stage (Pass 2).
 *                            Finalize (#915) merges imports/wildcards on top.
 *   - `Scope.ownedDefs`    — declarations structurally owned by this scope.
 *   - `Scope.typeBindings` — local type facts (parameter annotations, `self`).
 *   - `Scope.imports`      — empty here. Populated by the finalize algorithm
 *                            when it resolves `ParsedImport.targetRaw`.
 *   - `ParsedFile.parsedImports` — every raw import in this file.
 *   - `ParsedFile.localDefs`     — flattened union of `Scope.ownedDefs`.
 *   - `ParsedFile.referenceSites` — pre-resolution usage facts.
 */
import { buildPositionIndex, buildScopeTree, canParentScope, makeScopeId } from '../../_shared/index.js';
import { extractTemplateArguments } from './utils/template-arguments.js';
// ─── Public entry point ─────────────────────────────────────────────────────
/**
 * Drive the five extraction passes and return a `ParsedFile`.
 *
 * Throws `ScopeTreeInvariantError` (from #912) when the provider emits
 * captures that violate structural scope invariants (e.g., overlapping
 * sibling scopes). When no `@scope.module` capture is present, a
 * synthetic Module scope is created spanning all captures, and orphan
 * non-Module scopes are re-parented under it. This enables indexing of
 * files where tree-sitter produces an ERROR root (e.g., complex .phtml
 * templates with mixed PHP/HTML/JS).
 */
export function extract(matches, filePath, provider) {
    // Partition matches by topic up front — one linear pass over the input.
    const partitioned = partitionByTopic(matches);
    // ── Pass 1: build the scope tree ─────────────────────────────────────
    const scopeDrafts = pass1BuildScopes(partitioned.scope, filePath, provider);
    const moduleScope = ensureModuleScope(scopeDrafts, filePath, matches);
    // Re-parent orphan drafts (parent === null, non-Module) under the
    // Module scope. Replaces drafts with new ones carrying the correct
    // parent — runs before content passes so bindings/ownedDefs are empty.
    for (let i = 0; i < scopeDrafts.length; i++) {
        const d = scopeDrafts[i];
        if (d.parent === null && d.kind !== 'Module') {
            scopeDrafts[i] = makeDraft(d.id, moduleScope.id, d.kind, d.range, d.filePath);
        }
    }
    const scopes = scopeDrafts.map(draftToScope);
    // buildScopeTree validates invariants (throws on violation) and exposes
    // the lookup contract consumed by Passes 2-5.
    //
    // **Snapshot semantics.** Both `scopeTree` and `positionIndex` are built
    // from the post-Pass-1 `scopes` — parent/range/kind are accurate, but
    // `bindings`, `ownedDefs`, and `typeBindings` are all empty here. Later
    // passes write into the *drafts*, not into these snapshots; any hook
    // that reads `scope.bindings` etc. via the `scopeTree` argument sees a
    // structural view only. This is by design — hooks use scopeTree for
    // "what's the parent chain?" queries, not for content queries.
    const scopeTree = buildScopeTree(scopes);
    const positionIndex = buildPositionIndex(scopes);
    // ── Pass 2: attach declarations + local bindings ────────────────────
    const localDefs = [];
    pass2AttachDeclarations(partitioned.declaration, scopeDrafts, positionIndex, localDefs, filePath, provider, scopeTree);
    // ── Pass 3: collect raw imports ─────────────────────────────────────
    const parsedImports = [];
    pass3CollectImports(partitioned.import_, parsedImports, provider);
    // ── Pass 4: collect type bindings ───────────────────────────────────
    pass4CollectTypeBindings(partitioned.typeBinding, scopeDrafts, positionIndex, filePath, provider, scopeTree);
    // ── Pass 5: collect reference sites ─────────────────────────────────
    const referenceSites = [];
    pass5CollectReferences(partitioned.reference, positionIndex, filePath, referenceSites, provider, scopeTree);
    // Freeze Scope drafts into final shape and return.
    const frozenScopes = scopeDrafts.map(draftToScope);
    return Object.freeze({
        filePath,
        moduleScope: moduleScope.id,
        scopes: Object.freeze(frozenScopes),
        parsedImports: Object.freeze(parsedImports.slice()),
        localDefs: Object.freeze(localDefs.slice()),
        referenceSites: Object.freeze(referenceSites.slice()),
    });
}
/**
 * Bucket each match by the topic of its anchor capture. The anchor is the
 * capture whose name is prefixed with the match's topic (`@scope.*`,
 * `@declaration.*`, `@import.*`, `@type-binding.*`, `@reference.*`).
 *
 * A match may contain additional captures (e.g., `@import.source`,
 * `@declaration.class.name`) that are used by the provider hooks to
 * decode details. Those live inside the `CaptureMatch` and are surfaced
 * to hooks verbatim — the extractor itself only routes by anchor.
 */
function partitionByTopic(matches) {
    const scope = [];
    const declaration = [];
    const import_ = [];
    const typeBinding = [];
    const reference = [];
    for (const match of matches) {
        const topic = topicOf(match);
        switch (topic) {
            case 'scope':
                scope.push(match);
                break;
            case 'declaration':
                declaration.push(match);
                break;
            case 'import':
                import_.push(match);
                break;
            case 'type-binding':
                typeBinding.push(match);
                break;
            case 'reference':
                reference.push(match);
                break;
            case 'unknown':
                // Unrecognized anchor — silently skip. Providers may emit extra
                // captures (e.g., `@comment`) that the extractor has no topic for.
                break;
        }
    }
    return { scope, declaration, import_, typeBinding, reference };
}
function topicOf(match) {
    // The anchor is the capture whose name uses one of the known topic
    // prefixes. For multi-capture matches, ALL captures share the topic;
    // we pick the first matching key for efficiency.
    for (const name of Object.keys(match)) {
        if (name.startsWith('@scope.'))
            return 'scope';
        if (name.startsWith('@declaration.'))
            return 'declaration';
        if (name.startsWith('@import.'))
            return 'import';
        if (name.startsWith('@type-binding.'))
            return 'type-binding';
        if (name.startsWith('@reference.'))
            return 'reference';
    }
    return 'unknown';
}
function ensureModuleScope(scopeDrafts, filePath, allMatches) {
    const moduleScope = scopeDrafts.find((s) => s.kind === 'Module');
    if (moduleScope !== undefined)
        return moduleScope;
    // Synthesize a Module scope spanning all captures in the file.
    // Computed from ALL captures (scope, declaration, reference, etc.)
    // so the range covers top-level references that appear after the
    // last inner scope — not just inner Function/Class scopes.
    let endLine = 0;
    let endCol = 0;
    for (const match of allMatches) {
        for (const capture of Object.values(match)) {
            if (capture.range.endLine > endLine ||
                (capture.range.endLine === endLine && capture.range.endCol > endCol)) {
                endLine = capture.range.endLine;
                endCol = capture.range.endCol;
            }
        }
    }
    const range = { startLine: 0, startCol: 0, endLine, endCol };
    const synthetic = makeDraft(makeScopeId({ filePath, range, kind: 'Module' }), null, 'Module', range, filePath);
    scopeDrafts.push(synthetic);
    return synthetic;
}
function draftToScope(draft) {
    const frozenBindings = new Map();
    for (const [name, refs] of draft.bindings) {
        frozenBindings.set(name, Object.freeze(refs.slice()));
    }
    return {
        id: draft.id,
        parent: draft.parent,
        kind: draft.kind,
        range: draft.range,
        filePath: draft.filePath,
        bindings: frozenBindings,
        ownedDefs: Object.freeze(draft.ownedDefs.slice()),
        imports: Object.freeze(draft.imports.slice()),
        typeBindings: new Map(draft.typeBindings),
    };
}
// ─── Pass 1: build scope tree ──────────────────────────────────────────────
/**
 * Convert `@scope.*` matches into `ScopeDraft[]`. Parent relationships
 * are derived from range containment (outermost scope containing `range`
 * becomes the parent).
 */
function pass1BuildScopes(matches, filePath, provider) {
    const candidates = [];
    for (const match of matches) {
        const anchor = anchorCaptureFor(match, '@scope.');
        if (anchor === undefined)
            continue;
        const kind = resolveKindForScopeMatch(match, anchor, provider);
        if (kind === null)
            continue;
        const id = makeScopeId({ filePath, range: anchor.range, kind });
        candidates.push({ match, range: anchor.range, kind, id });
    }
    // Sort by (startLine, startCol) ASC, (endLine, endCol) DESC so outer
    // scopes appear before their children for parent-resolution. When two
    // candidates have exactly equal ranges (e.g. a `compilation_unit` and
    // the only top-level scope in the file — see `canParentScope`), Module
    // sorts first so it lands on the stack ahead of the candidate that will
    // claim it as parent.
    candidates.sort((a, b) => {
        if (a.range.startLine !== b.range.startLine)
            return a.range.startLine - b.range.startLine;
        if (a.range.startCol !== b.range.startCol)
            return a.range.startCol - b.range.startCol;
        if (a.range.endLine !== b.range.endLine)
            return b.range.endLine - a.range.endLine;
        if (a.range.endCol !== b.range.endCol)
            return b.range.endCol - a.range.endCol;
        if (a.kind === b.kind)
            return 0;
        if (a.kind === 'Module')
            return -1;
        if (b.kind === 'Module')
            return 1;
        return 0;
    });
    const drafts = [];
    const stack = []; // enclosing real scopes, outermost at [0]
    for (const cand of candidates) {
        // Pop the stack until the top can parent this candidate (strict
        // containment, plus the equal-range Module carve-out).
        while (stack.length > 0 &&
            !canParentScope(stack[stack.length - 1].range, cand.range, stack[stack.length - 1].kind, cand.kind)) {
            stack.pop();
        }
        const parent = stack.length > 0 ? stack[stack.length - 1].id : null;
        drafts.push(makeDraft(cand.id, parent, cand.kind, cand.range, filePath));
        stack.push(cand);
    }
    return drafts;
}
function resolveKindForScopeMatch(match, anchor, provider) {
    // Provider override takes precedence.
    const override = provider.resolveScopeKind?.(match);
    if (override !== undefined && override !== null)
        return override;
    // Default: derive from capture name suffix (`@scope.function` → 'Function').
    const suffix = anchor.name.slice('@scope.'.length);
    switch (suffix.toLowerCase()) {
        case 'module':
            return 'Module';
        case 'namespace':
            return 'Namespace';
        case 'class':
            return 'Class';
        case 'function':
            return 'Function';
        case 'block':
            return 'Block';
        case 'expression':
            return 'Expression';
        default:
            return null;
    }
}
function makeDraft(id, parent, kind, range, filePath) {
    return {
        id,
        parent,
        kind,
        range,
        filePath,
        bindings: new Map(),
        ownedDefs: [],
        imports: [],
        typeBindings: new Map(),
    };
}
// ─── Pass 2: attach declarations + local bindings ──────────────────────────
function pass2AttachDeclarations(matches, drafts, positionIndex, localDefs, filePath, provider, scopeTree) {
    const draftById = new Map();
    for (const d of drafts)
        draftById.set(d.id, d);
    for (const match of matches) {
        const anchor = anchorCaptureFor(match, '@declaration.');
        if (anchor === undefined)
            continue;
        const def = buildDefFromDeclarationMatch(match, anchor, filePath);
        if (def === undefined)
            continue;
        // Find the innermost scope that contains the declaration's anchor range.
        const innermostId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
        if (innermostId === undefined)
            continue;
        const innermost = draftById.get(innermostId);
        if (innermost === undefined)
            continue;
        // Ownership: attach the def to the innermost scope's `ownedDefs` — that
        // is the structural owner. `def.ownerId` is NOT populated here — the
        // extractor has no clean path to the parent's own DefId mid-extraction
        // (the parent declaration may not yet have been processed, or may live
        // in a different scope entirely). Providers that need `ownerId` should
        // set it directly from the declaration hook (e.g., derive from the
        // `@declaration.owner` capture or the parent scope id); otherwise
        // `finalize` populates method/field `ownerId` via `MethodDispatchIndex`
        // (#914) in a follow-up pass that sees every def already in place.
        innermost.ownedDefs.push(def);
        localDefs.push(def);
        // Binding visibility: default to innermost; allow hoisting via
        // `provider.bindingScopeFor`. `draftToScope(innermost)` here is a
        // **structural** snapshot — parent/range/kind only. Hooks MUST NOT
        // rely on `scope.bindings`, `ownedDefs`, or `typeBindings` being
        // populated during Pass 2: those fields are written across passes,
        // so reading them mid-extraction yields a partial view. The
        // `scopeTree` argument is similarly snapshot-before-mutation.
        //
        // Auto-hoist for scope-creating declarations: when the declaration's
        // anchor range is the same node that produced `innermost` (e.g. a
        // `function_definition` is both `@scope.function` and the
        // `@declaration.function` anchor), the name is visible OUTSIDE the
        // body, not inside. Hoisting to the parent scope is what every
        // mainstream language wants for function/class declarations. Hooks
        // can override by returning a non-null scope id.
        const autoHostedId = innermost.parent !== null && rangesEqual(anchor.range, innermost.range)
            ? innermost.parent
            : innermost.id;
        const bindingScopeId = provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? autoHostedId;
        const bindingHost = draftById.get(bindingScopeId) ?? innermost;
        const nameKey = deriveDeclarationName(match, def);
        if (nameKey === undefined)
            continue;
        const existing = bindingHost.bindings.get(nameKey) ?? [];
        existing.push({ def, origin: 'local' });
        bindingHost.bindings.set(nameKey, existing);
    }
}
function buildDefFromDeclarationMatch(match, anchor, filePath) {
    // Anchor name pattern: `@declaration.<kind>` where <kind> maps to NodeLabel.
    const kindStr = anchor.name.slice('@declaration.'.length);
    const type = normalizeNodeLabel(kindStr);
    if (type === undefined)
        return undefined;
    const nameCap = match['@declaration.name'] ?? match[`@declaration.${kindStr}.name`] ?? match[anchor.name];
    if (nameCap === undefined)
        return undefined;
    const qualifiedCap = match['@declaration.qualified_name'];
    const qualifiedName = qualifiedCap?.text;
    const templateArguments = extractTemplateArguments(match['@declaration.template-arguments']?.text ?? '') ??
        extractTemplateArguments(qualifiedName ?? nameCap.text);
    // Optional arity metadata — producers (e.g. Python emit-captures)
    // synthesize these on function/method declarations. Their absence is
    // the normal case for other producers; readers treat undefined as
    // "unknown" per `SymbolDefinition` contract.
    const parameterCount = parseIntCapture(match['@declaration.parameter-count']);
    const requiredParameterCount = parseIntCapture(match['@declaration.required-parameter-count']);
    const parameterTypes = parseJsonStringArrayCapture(match['@declaration.parameter-types']);
    const parameterTypeClasses = parseJsonParameterTypeClassesCapture(match['@declaration.parameter-type-classes']);
    const declaredType = match['@declaration.field-type']?.text;
    const returnType = match['@declaration.return-type']?.text;
    const templateConstraints = parseJsonCapture(match['@declaration.template-constraints']);
    const isExplicit = parseBooleanCapture(match['@declaration.is-explicit']);
    return {
        nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
        filePath,
        type,
        ...(qualifiedName !== undefined ? { qualifiedName } : { qualifiedName: nameCap.text }),
        ...(parameterCount !== undefined ? { parameterCount } : {}),
        ...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
        ...(parameterTypes !== undefined ? { parameterTypes } : {}),
        ...(parameterTypeClasses !== undefined ? { parameterTypeClasses } : {}),
        ...(declaredType !== undefined ? { declaredType } : {}),
        ...(returnType !== undefined ? { returnType } : {}),
        ...(templateArguments !== undefined ? { templateArguments } : {}),
        ...(templateConstraints !== undefined ? { templateConstraints } : {}),
        ...(isExplicit === true ? { isExplicit: true } : {}),
    };
}
/** Parse an opaque JSON payload synthesized by per-language captures
 *  (e.g. C++ `@declaration.template-constraints`). Producer owns the
 *  shape; shared code threads it through as `unknown` per the
 *  `SymbolDefinition.templateConstraints` contract. */
function parseJsonCapture(cap) {
    if (cap === undefined)
        return undefined;
    try {
        return JSON.parse(cap.text);
    }
    catch {
        return undefined;
    }
}
function parseIntCapture(cap) {
    if (cap === undefined)
        return undefined;
    const n = Number.parseInt(cap.text, 10);
    return Number.isFinite(n) ? n : undefined;
}
function parseBooleanCapture(cap) {
    if (cap === undefined)
        return undefined;
    if (cap.text === 'true')
        return true;
    if (cap.text === 'false')
        return false;
    return undefined;
}
function parseJsonParameterTypeClassesCapture(cap) {
    if (cap === undefined)
        return undefined;
    try {
        const parsed = JSON.parse(cap.text);
        if (!Array.isArray(parsed))
            return undefined;
        const out = [];
        for (const item of parsed) {
            if (item === null || typeof item !== 'object')
                return undefined;
            const o = item;
            if (typeof o.base !== 'string')
                return undefined;
            if (o.cv !== 'none' &&
                o.cv !== 'const' &&
                o.cv !== 'volatile' &&
                o.cv !== 'const volatile' &&
                o.cv !== 'unknown') {
                return undefined;
            }
            if (o.indirection !== 'value' &&
                o.indirection !== 'lvalue-ref' &&
                o.indirection !== 'rvalue-ref' &&
                o.indirection !== 'pointer' &&
                o.indirection !== 'unknown') {
                return undefined;
            }
            if (typeof o.pointerDepth !== 'number' || !Number.isFinite(o.pointerDepth)) {
                return undefined;
            }
            out.push({
                base: o.base,
                cv: o.cv,
                indirection: o.indirection,
                pointerDepth: o.pointerDepth,
            });
        }
        return out;
    }
    catch {
        return undefined;
    }
}
function parseJsonStringArrayCapture(cap) {
    if (cap === undefined)
        return undefined;
    try {
        const parsed = JSON.parse(cap.text);
        if (!Array.isArray(parsed))
            return undefined;
        return parsed.every((x) => typeof x === 'string') ? parsed : undefined;
    }
    catch {
        return undefined;
    }
}
function deriveDeclarationName(match, def) {
    const nameCap = match['@declaration.name'] ??
        match[Object.keys(match).find((k) => k.startsWith('@declaration.') && k.endsWith('.name')) ?? ''];
    if (nameCap !== undefined)
        return nameCap.text;
    // Fall back to qualifiedName tail.
    const q = def.qualifiedName;
    if (q !== undefined && q.length > 0) {
        const dot = q.lastIndexOf('.');
        return dot === -1 ? q : q.slice(dot + 1);
    }
    return undefined;
}
/**
 * Map a lower-case declaration kind (from `@declaration.<kind>`) to a
 * graph `NodeLabel`. Silently returns `undefined` for kinds we don't
 * recognize — providers can emit richer captures without breaking the
 * driver.
 */
function normalizeNodeLabel(kindStr) {
    switch (kindStr.toLowerCase()) {
        case 'class':
            return 'Class';
        case 'interface':
            return 'Interface';
        case 'enum':
            return 'Enum';
        case 'struct':
            return 'Struct';
        case 'union':
            return 'Union';
        case 'trait':
            return 'Trait';
        case 'method':
            return 'Method';
        case 'function':
            return 'Function';
        case 'constructor':
            return 'Constructor';
        case 'field':
        case 'property':
            return 'Property';
        case 'variable':
            return 'Variable';
        // `const` / `let` declarations align with the legacy DAG parse phase,
        // which emits `Const` graph nodes via `@definition.const` capture for
        // `lexical_declaration`. Returning `'Const'` here lets resolveDefGraphId's
        // qualified-key path succeed for value receivers without relying on the
        // simple-key fallback (PR #1718 review Finding 1 / 2026-05-21-002 U4).
        case 'const':
            return 'Const';
        case 'typealias':
        case 'type_alias':
            return 'TypeAlias';
        case 'typedef':
            return 'Typedef';
        case 'record':
            return 'Record';
        case 'delegate':
            return 'Delegate';
        case 'annotation':
            return 'Annotation';
        case 'namespace':
            return 'Namespace';
        default:
            return undefined;
    }
}
function makeDefId(filePath, range, type, name) {
    return `def:${filePath}#${range.startLine}:${range.startCol}:${type}:${name}`;
}
// ─── Pass 3: collect raw imports ───────────────────────────────────────────
function pass3CollectImports(matches, parsedImports, provider) {
    if (provider.interpretImport === undefined)
        return;
    for (const match of matches) {
        const anchor = anchorCaptureFor(match, '@import.');
        if (anchor === undefined)
            continue;
        const parsed = provider.interpretImport(match);
        if (parsed === null)
            continue;
        parsedImports.push(parsed);
    }
}
// ─── Pass 4: collect type bindings ─────────────────────────────────────────
function pass4CollectTypeBindings(matches, drafts, positionIndex, filePath, provider, scopeTree) {
    const draftById = new Map();
    for (const d of drafts)
        draftById.set(d.id, d);
    for (const match of matches) {
        const anchor = anchorCaptureFor(match, '@type-binding.');
        if (anchor === undefined)
            continue;
        const parsed = provider.interpretTypeBinding?.(match);
        if (parsed === null || parsed === undefined)
            continue;
        const innermostId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
        if (innermostId === undefined)
            continue;
        const innermost = draftById.get(innermostId);
        if (innermost === undefined)
            continue;
        // Auto-hoist for scope-creating type bindings (e.g. Python's
        // `@type-binding.return` whose anchor is the function_definition
        // itself). Same condition as Pass 2 — when the anchor coincides
        // with the innermost scope's range, the binding belongs in the
        // enclosing scope (callers, not the function body, look up the
        // return type by the function's name).
        const autoHostedId = innermost.parent !== null && rangesEqual(anchor.range, innermost.range)
            ? innermost.parent
            : innermost.id;
        // `bindingScopeFor` may hoist the type binding to an outer scope.
        const hostId = provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? autoHostedId;
        const host = draftById.get(hostId) ?? innermost;
        const typeRef = {
            rawName: parsed.rawTypeName,
            declaredAtScope: host.id,
            source: parsed.source,
        };
        // Prefer stronger sources when multiple matches fire for the same
        // bound name in the same scope. Example: `u: User = find()` matches
        // both the annotation and constructor-inferred patterns; the explicit
        // annotation (stronger source) must win over the call-site guess
        // regardless of query-match arrival order.
        const existing = host.typeBindings.get(parsed.boundName);
        if (existing === undefined ||
            typeBindingStrength(typeRef.source) >= typeBindingStrength(existing.source)) {
            host.typeBindings.set(parsed.boundName, typeRef);
        }
    }
    // ── Transitive closure over identifier-chain type bindings ─────────
    // Captures like `(assignment left: (ident) right: (ident))` emit a
    // TypeRef whose `rawName` is the RHS identifier. When the RHS name is
    // itself a bound variable with a known type in the same scope (or a
    // parent scope), follow the chain so `alias` ultimately points at the
    // class type — not at another local variable name. Without this,
    // `resolveTypeRef` hits the chained name, sees it's a local Variable
    // (non-type kind), and strict-returns null.
    for (const draft of drafts) {
        for (const [name, ref] of draft.typeBindings) {
            const resolved = followChainedRef(ref, draftById);
            if (resolved !== ref)
                draft.typeBindings.set(name, resolved);
        }
    }
}
/** Max chain depth: practical programs rarely exceed 4-5 re-bindings;
 *  the cap just prevents runaway loops when providers emit cycles. */
const CHAIN_MAX_DEPTH = 16;
/**
 * Follow an identifier-chain TypeRef through successive typeBindings
 * lookups in the declaring scope and its ancestors. Returns the terminal
 * TypeRef (or the original if the chain dead-ends or cycles).
 */
function followChainedRef(start, draftById) {
    let current = start;
    const visited = new Set();
    for (let depth = 0; depth < CHAIN_MAX_DEPTH; depth++) {
        // A rawName containing a dot (`models.User`) goes through
        // `QualifiedNameIndex` at resolution time — don't follow it here.
        if (current.rawName.includes('.'))
            return current;
        // Look up the current rawName in the declaring scope and walk up
        // the chain until we hit a scope that has a binding for it.
        let scopeId = current.declaredAtScope;
        let next;
        while (scopeId !== null) {
            const scope = draftById.get(scopeId);
            if (scope === undefined)
                break;
            next = scope.typeBindings.get(current.rawName);
            if (next !== undefined)
                break;
            scopeId = scope.parent;
        }
        if (next === undefined)
            return current; // dead end — nothing to chain to
        if (next === current)
            return current; // self-ref
        if (visited.has(next.rawName))
            return current; // cycle guard
        visited.add(next.rawName);
        current = next;
    }
    return current;
}
/**
 * Priority ordering when multiple `TypeRef`s compete for the same bound
 * name in the same scope. Higher number wins; ties keep the later match
 * (last-write-wins preserves historical order within a tier).
 *
 * Rationale: explicit annotations always beat inferred ones because they
 * reflect user intent. `self`/`cls` are treated as strongly as annotations
 * because they are language-required receiver types.
 */
function typeBindingStrength(source) {
    switch (source) {
        case 'annotation':
        case 'parameter-annotation':
        case 'return-annotation':
        case 'self':
            return 2;
        case 'assignment-inferred':
        case 'constructor-inferred':
        case 'receiver-propagated':
            return 1;
        default:
            return 0;
    }
}
// ─── Pass 5: collect reference sites ───────────────────────────────────────
function pass5CollectReferences(matches, positionIndex, filePath, referenceSites, provider, scopeTree) {
    for (const match of matches) {
        const anchor = anchorCaptureFor(match, '@reference.');
        if (anchor === undefined)
            continue;
        const kind = referenceKindFromAnchor(anchor.name);
        if (kind === undefined)
            continue;
        const nameCap = match['@reference.name'] ?? anchor;
        const inScopeId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
        if (inScopeId === undefined)
            continue;
        const callForm = kind === 'call'
            ? classifyCallFormForMatch(match, anchor.name, provider, scopeTree, inScopeId)
            : undefined;
        const explicitReceiver = extractExplicitReceiver(match);
        const arity = extractArity(match);
        const argumentTypes = extractArgumentTypes(match);
        const argumentTypeClasses = parseJsonParameterTypeClassesCapture(match['@reference.parameter-type-classes']);
        const site = {
            name: nameCap.text,
            atRange: anchor.range,
            inScope: inScopeId,
            kind,
            ...(callForm !== undefined ? { callForm } : {}),
            ...(explicitReceiver !== undefined ? { explicitReceiver } : {}),
            ...(arity !== undefined ? { arity } : {}),
            ...(argumentTypes !== undefined ? { argumentTypes } : {}),
            ...(argumentTypeClasses !== undefined ? { argumentTypeClasses } : {}),
        };
        referenceSites.push(site);
    }
}
function referenceKindFromAnchor(name) {
    const suffix = name.slice('@reference.'.length);
    // Strip sub-tag after the kind (`@reference.call.member` → `call`).
    const firstDot = suffix.indexOf('.');
    const head = firstDot === -1 ? suffix : suffix.slice(0, firstDot);
    switch (head.toLowerCase()) {
        case 'call':
            return 'call';
        case 'read':
            return 'read';
        case 'write':
            return 'write';
        case 'type':
        case 'type_reference':
            return 'type-reference';
        case 'inherits':
            return 'inherits';
        case 'import_use':
        case 'import-use':
            return 'import-use';
        default:
            return undefined;
    }
}
function classifyCallFormForMatch(match, anchorName, provider, scopeTree, inScopeId) {
    // Declarative sub-tag path first: `@reference.call.member` → 'member'.
    const suffix = anchorName.slice('@reference.call.'.length);
    switch (suffix.toLowerCase()) {
        case 'free':
            return 'free';
        case 'member':
            return 'member';
        case 'constructor':
            return 'constructor';
        case 'index':
            return 'index';
    }
    // Hook-based path: provider knows.
    const hook = provider.classifyCallForm;
    if (hook !== undefined) {
        const scope = scopeTree.getScope(inScopeId);
        if (scope !== undefined)
            return hook(match, scope);
    }
    return 'free';
}
function extractExplicitReceiver(match) {
    const cap = match['@reference.receiver'];
    if (cap === undefined)
        return undefined;
    return { name: cap.text };
}
function extractArity(match) {
    const cap = match['@reference.arity'];
    if (cap === undefined)
        return undefined;
    const n = Number.parseInt(cap.text, 10);
    return Number.isFinite(n) ? n : undefined;
}
function extractArgumentTypes(match) {
    const cap = match['@reference.parameter-types'];
    if (cap === undefined)
        return undefined;
    try {
        const parsed = JSON.parse(cap.text);
        if (Array.isArray(parsed) && parsed.every((x) => typeof x === 'string'))
            return parsed;
    }
    catch {
        /* malformed — fall through */
    }
    return undefined;
}
// ─── Internal: range + capture utilities ───────────────────────────────────
function rangesEqual(a, b) {
    return (a.startLine === b.startLine &&
        a.startCol === b.startCol &&
        a.endLine === b.endLine &&
        a.endCol === b.endCol);
}
/**
 * Capture names that are never anchors — they are sub-tags nested inside a
 * larger anchor (e.g., the receiver expression inside a `@reference.call`
 * may span more source than the called name, but is not the call itself).
 *
 * The list is maintained here centrally rather than per-pass because the
 * set is small and stable; adding a new sub-tag convention is a one-line
 * change.
 */
const KNOWN_SUB_TAGS = new Set([
    '@declaration.name',
    '@declaration.qualified_name',
    '@import.name',
    '@import.source',
    '@import.alias',
    '@type-binding.name',
    '@type-binding.type',
    '@reference.name',
    '@reference.receiver',
    '@reference.operator',
    '@reference.arity',
    '@reference.parameter-types',
    '@reference.parameter-type-classes',
    '@declaration.parameter-count',
    '@declaration.required-parameter-count',
    '@declaration.parameter-types',
    '@declaration.parameter-type-classes',
    '@declaration.template-constraints',
    '@declaration.is-explicit',
]);
/**
 * Return the anchor capture for a match — the one whose name begins with
 * `prefix` AND is not in the known-sub-tag set. When multiple candidates
 * remain, the broadest-ranged one wins: tree-sitter queries often tag
 * both a whole statement and a sub-token under the same topic
 * (`@scope.function` + `@scope.function.name`); the anchor is the
 * statement-level one.
 */
function anchorCaptureFor(match, prefix) {
    let best;
    let bestSpan = -1;
    for (const name of Object.keys(match)) {
        if (!name.startsWith(prefix))
            continue;
        if (KNOWN_SUB_TAGS.has(name))
            continue;
        const cap = match[name];
        const span = (cap.range.endLine - cap.range.startLine) * 1_000_000 +
            (cap.range.endCol - cap.range.startCol);
        if (span > bestSpan) {
            bestSpan = span;
            best = cap;
        }
    }
    return best;
}
