// Optional structural exploration adapter contract + default registry (Epic #177, Issue #180).
// Adapters surface structural signals (test/source pairing, import graph, git history) and
// degrade gracefully when their data sources are missing. Output is normalized to EvidenceAtom
// from @oscharko-dev/keiko-contracts; the runner merges, dedupes, and caps adapter output so
// one broken adapter never blocks the rest. Stays within ADR-0019 rule 3b.
import { RepoSearchInvalidQueryError, RepoSearchInvalidRangeError } from "./errors.js";
import { testSourcePairingAdapter } from "./testSourcePairing.js";
import { importGraphAdapter } from "./importGraph.js";
import { gitHistoryAdapter } from "./gitHistory.js";
// ─── Default registry ─────────────────────────────────────────────────────────
export function createDefaultStructuralRegistry() {
    return {
        adapters: [testSourcePairingAdapter, importGraphAdapter, gitHistoryAdapter],
    };
}
async function probeAvailability(adapters, scope, fs) {
    return Promise.all(adapters.map(async (adapter) => ({
        adapter,
        available: await adapter.isAvailable(scope, fs).catch(() => false),
    })));
}
function isTypedAdapterError(error) {
    return (error instanceof RepoSearchInvalidQueryError || error instanceof RepoSearchInvalidRangeError);
}
function describeError(error) {
    return error instanceof Error ? error.message : String(error);
}
async function runOne(adapter, scope, query, limits, fs, deps) {
    try {
        const atoms = await adapter.lookup(scope, query, limits, fs, deps);
        return { name: adapter.name, atoms, error: undefined };
    }
    catch (error) {
        if (isTypedAdapterError(error)) {
            throw error;
        }
        return {
            name: adapter.name,
            atoms: [],
            error: { name: adapter.name, message: describeError(error) },
        };
    }
}
function mergeAtoms(outcomes, cap) {
    const seen = new Set();
    const merged = [];
    for (const outcome of outcomes) {
        for (const atom of outcome.atoms) {
            if (merged.length >= cap) {
                return merged;
            }
            if (seen.has(atom.stableId)) {
                continue;
            }
            seen.add(atom.stableId);
            merged.push(atom);
        }
    }
    return merged;
}
export async function runStructuralAdapters(registry, scope, query, limits, fs, deps) {
    const nowMs = deps?.nowMs ?? Date.now;
    const startMs = nowMs();
    const availability = await probeAvailability(registry.adapters, scope, fs);
    const unavailable = [];
    const available = [];
    for (const row of availability) {
        if (row.available) {
            available.push(row.adapter);
        }
        else {
            unavailable.push(row.adapter.name);
        }
    }
    const outcomes = await Promise.all(available.map((adapter) => runOne(adapter, scope, query, limits, fs, deps)));
    const errored = outcomes
        .map((outcome) => outcome.error)
        .filter((error) => error !== undefined);
    const cap = Math.min(limits.maxMatchesReturned, query.maxResults);
    const atoms = mergeAtoms(outcomes, cap);
    return {
        atoms,
        unavailable,
        errored,
        elapsedMs: nowMs() - startMs,
    };
}
