/**
 * GitNexus backend — `StructuralPrepass` against `gitnexus` CLI.
 *
 * GitNexus indexes a codebase into a graph (LadybugDB) with tree-sitter
 * symbol extraction + cross-file resolution + Leiden community detection.
 * It exposes Cypher queries via the `gitnexus cypher <query>` CLI command,
 * which returns JSON with a markdown-table inner format. We shell out to
 * the CLI and parse the markdown table.
 *
 * The distinguishing capability vs CodeGraph is `clusterFiles()` — GitNexus
 * tracks `Community` nodes (via Leiden community detection on the call graph)
 * which give natural component-boundary suggestions for SpecVerse `components:`.
 *
 * Storage: GitNexus stores per-repo data in `.gitnexus/` and a global
 * registry pointer at `~/.gitnexus/`. Like CodeGraph, indexing happens once;
 * subsequent queries hit the persistent index.
 */
import { execSync, execFileSync } from 'child_process';
import { existsSync, mkdtempSync, readFileSync } from 'fs';
import { homedir, tmpdir } from 'os';
import { join } from 'path';
import { createHash } from 'crypto';
import { walkSourceTree, detectLanguage, globToRegex } from './walk.js';
import { extractEmits, extractDbWrites, extractExternalCalls, extractThrows, extractAsyncBoundaries, countBranchPoints, sliceBody, } from './method-patterns.js';
export class GitNexusBackend {
    sourceDir = '';
    /**
     * Canonical repo name as registered in `~/.gitnexus/registry.json`.
     * Resolved by `init()` from the registry; passed as `--repo <name>`
     * to every `gitnexus cypher` call so multi-repo registries don't
     * cross-contaminate query results.
     */
    repoName = '';
    gitnexusPath;
    reuseExistingIndex;
    /**
     * Per-invocation `GITNEXUS_HOME`. Threaded into every `gitnexus`
     * subprocess via `env:` so this backend's registry-state is isolated
     * from concurrent invocations. `null` means "don't set GITNEXUS_HOME"
     * (inherit gitnexus's default).
     */
    gitnexusHome;
    capabilities = {
        callGraph: true,
        crossFileResolution: true,
        componentClustering: true, // Leiden community detection (the differentiator)
        fullTextSearch: true, // hybrid BM25 + semantic + RRF
        fileWatching: false, // GitNexus doesn't auto-sync; needs explicit re-analyze
        routeExtraction: true, // Route nodes + HANDLES_ROUTE edges (engines 6.30.0+)
        implementsExtraction: true, // IMPLEMENTS edges (engines 6.30.0+)
        validationSchemaExtraction: true, // Const nodes (engines 6.30.0+)
        processExtraction: true, // Process nodes + STEP_IN_PROCESS edges (engines 6.30.0+)
    };
    constructor(options = {}) {
        this.gitnexusPath = options.gitnexusPath ?? this.detectBinary('gitnexus');
        this.reuseExistingIndex = options.reuseExistingIndex ?? false;
        // Per-invocation registry isolation by default. `mkdtempSync` gives
        // a unique dir even under concurrent constructor calls (the kernel
        // does the uniqueness for us). Callers can opt out with 'inherit'
        // or pass their own path. The temp dir is left for /tmp eviction —
        // gitnexus registries are small (a JSON file + per-repo pointers).
        if (options.gitnexusHome === 'inherit') {
            this.gitnexusHome = null;
        }
        else if (options.gitnexusHome) {
            this.gitnexusHome = options.gitnexusHome;
        }
        else {
            this.gitnexusHome = mkdtempSync(join(tmpdir(), 'specverse-gitnexus-'));
        }
    }
    /**
     * Build the env to thread into every `gitnexus` subprocess. Sets
     * `GITNEXUS_HOME` to the per-invocation dir (registry isolation);
     * otherwise inherits. `GITNEXUS_HOME` is the var gitnexus reads for its
     * global-registry location — NOT `GITNEXUS_DIR` (a no-op env name).
     */
    execEnv() {
        return this.gitnexusHome === null
            ? process.env
            : { ...process.env, GITNEXUS_HOME: this.gitnexusHome };
    }
    static isAvailable() {
        try {
            execSync('gitnexus --version', { stdio: 'ignore', timeout: 5000 });
            return true;
        }
        catch {
            return false;
        }
    }
    async init(sourceDir) {
        this.sourceDir = sourceDir;
        const gitnexusDir = join(sourceDir, '.gitnexus');
        // Compute a unique repo name for this source path. GitNexus's default
        // name resolution walks UP the directory tree to find package.json /
        // git remote, which collides catastrophically when many sibling run
        // dirs all live under one project root (we saw 25 entries named
        // "specverse-demo-self" mapped to different .gitnexus storages).
        // Passing --name <unique> guarantees one storage per source path.
        this.repoName = computeUniqueRepoName(sourceDir);
        if (!this.reuseExistingIndex || !existsSync(gitnexusDir)) {
            if (!existsSync(gitnexusDir)) {
                execSync(`${this.gitnexusPath} analyze --skip-git --name ${shellEscape(this.repoName)} .`, {
                    cwd: sourceDir,
                    stdio: 'ignore',
                    timeout: 600_000,
                    env: this.execEnv(),
                });
            }
        }
    }
    /**
     * Resolve a sourceDir to its canonical gitnexus repo name by reading
     * `~/.gitnexus/registry.json`. Falls back to the dir's basename if the
     * registry isn't readable or has no matching entry — but a basename
     * fallback is unsafe in multi-repo registries (it can collide with
     * another project that happens to have the same dir name), so we log
     * a warning to stderr in that case.
     */
    resolveRepoName(sourceDir) {
        const registryPath = join(homedir(), '.gitnexus', 'registry.json');
        const fallback = basenameOrSource(sourceDir);
        if (!existsSync(registryPath))
            return fallback;
        let registryContent;
        try {
            registryContent = readFileSync(registryPath, 'utf8');
        }
        catch (err) {
            process.stderr.write(`[gitnexus-backend] failed to read registry: ${err.message}; using basename fallback "${fallback}".\n`);
            return fallback;
        }
        const result = resolveRepoNameFromRegistry(registryContent, sourceDir);
        if (result.warning)
            process.stderr.write(`[gitnexus-backend] ${result.warning}\n`);
        return result.name ?? fallback;
    }
    async index() {
        const start = Date.now();
        if (!existsSync(join(this.sourceDir, '.gitnexus'))) {
            execSync(`${this.gitnexusPath} analyze --skip-git .`, {
                cwd: this.sourceDir,
                stdio: 'ignore',
                timeout: 600_000,
                env: this.execEnv(),
            });
        }
        // GitNexus prints stats during analyze; query for them via Cypher
        const rows = this.cypher('MATCH (n) RETURN count(*) AS c');
        const totalNodes = rows.length > 0 ? Number(rows[0].c) : 0;
        const fileRows = this.cypher('MATCH (n:File) RETURN count(*) AS c');
        const fileCount = fileRows.length > 0 ? Number(fileRows[0].c) : 0;
        return {
            files: fileCount,
            symbols: totalNodes - fileCount,
            durationMs: Date.now() - start,
        };
    }
    async listFiles(filter) {
        // Same pattern as CodeGraph backend — FS walk, not the index, so adapters
        // can find arbitrary file types (schema.prisma, package.json, etc.) the
        // tree-sitter indexer doesn't track.
        let result = walkSourceTree(this.sourceDir);
        if (filter?.dir)
            result = result.filter((f) => f.startsWith(filter.dir));
        if (filter?.lang)
            result = result.filter((f) => detectLanguage(f) === filter.lang);
        if (filter?.glob) {
            const re = globToRegex(filter.glob);
            result = result.filter((f) => re.test(f));
        }
        return result;
    }
    async listClasses(filter) {
        let cypher = `MATCH (n:Class) RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.isExported AS isExported`;
        if (filter?.dir) {
            const rel = this.toRelativePath(filter.dir);
            cypher = `MATCH (n:Class) WHERE n.filePath STARTS WITH '${this.escape(rel)}' RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.isExported AS isExported`;
        }
        const rows = this.cypher(cypher);
        return rows
            .filter((r) => !filter?.namePattern || filter.namePattern.test(r.name))
            .map((r) => this.rowToSymbol(r, 'class'));
    }
    async listMethods(filter) {
        // GitNexus doesn't store class membership as a property; we derive it
        // from the Method node's `name` plus filePath. For the `class` filter,
        // we scope by the class's file path range.
        let cypher = `MATCH (n:Method) RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.isExported AS isExported`;
        if (filter?.class) {
            // Find the class's file + line range first, then filter methods by it.
            const classRows = this.cypher(`MATCH (c:Class {name: '${this.escape(filter.class)}'}) RETURN c.filePath AS filePath, c.startLine AS startLine, c.endLine AS endLine LIMIT 1`);
            if (classRows.length === 0)
                return [];
            const cls = classRows[0];
            cypher = `MATCH (n:Method) WHERE n.filePath = '${this.escape(cls.filePath)}' AND n.startLine >= ${Number(cls.startLine)} AND n.startLine <= ${Number(cls.endLine)} RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.isExported AS isExported`;
        }
        const rows = this.cypher(cypher);
        let methods = rows.map((r) => {
            const sym = this.rowToSymbol(r, 'method');
            // Approximate qualifiedName: ClassName.methodName when class context is known
            if (filter?.class)
                sym.qualifiedName = `${filter.class}.${sym.name}`;
            return sym;
        });
        if (filter?.nameIn && filter.nameIn.length > 0) {
            const allowedNames = new Set(filter.nameIn);
            methods = methods.filter((m) => allowedNames.has(m.name));
        }
        return methods;
    }
    async listInterfaces(filter) {
        let cypher = `MATCH (n:Interface) RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.isExported AS isExported`;
        if (filter?.dir) {
            const rel = this.toRelativePath(filter.dir);
            cypher = `MATCH (n:Interface) WHERE n.filePath STARTS WITH '${this.escape(rel)}' RETURN n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.isExported AS isExported`;
        }
        const rows = this.cypher(cypher);
        return rows
            .filter((r) => !filter?.namePattern || filter.namePattern.test(r.name))
            .map((r) => this.rowToSymbol(r, 'interface'));
    }
    async listImports(file) {
        // GitNexus tracks imports via CodeRelation edges with type='IMPORTS'.
        const rel = this.toRelativePath(file);
        const cypher = `MATCH (f:File {filePath: '${this.escape(rel)}'})-[:CodeRelation {type: "IMPORTS"}]->(target) RETURN target.name AS name, target.filePath AS targetPath`;
        const rows = this.cypher(cypher);
        return rows.map((r) => ({
            fromFile: file,
            toModule: r.targetPath || r.name,
            symbols: [r.name],
        }));
    }
    async fileSourceText(file) {
        return readFileSync(file, 'utf8');
    }
    async callers(symbol) {
        const cypher = `MATCH (caller)-[:CodeRelation {type: "CALLS"}]->(callee) WHERE callee.name = '${this.escape(symbol)}' RETURN caller.name AS name, caller.filePath AS filePath, caller.startLine AS startLine, caller.endLine AS endLine, labels(caller)[0] AS kind LIMIT 100`;
        const rows = this.cypher(cypher);
        return rows.map((r) => this.rowToSymbol(r, this.mapLabel(r.kind)));
    }
    async callees(symbol) {
        const cypher = `MATCH (caller)-[:CodeRelation {type: "CALLS"}]->(callee) WHERE caller.name = '${this.escape(symbol)}' RETURN callee.name AS name, callee.filePath AS filePath, callee.startLine AS startLine, callee.endLine AS endLine, labels(callee)[0] AS kind LIMIT 100`;
        const rows = this.cypher(cypher);
        return rows.map((r) => this.rowToSymbol(r, this.mapLabel(r.kind)));
    }
    /**
     * Cluster files via GitNexus's Leiden community detection.
     * This is the distinguishing feature vs CodeGraph — surfaces natural
     * component boundaries for SpecVerse `components:` inference.
     *
     * GitNexus's edge model: a single `CodeRelation` table with a `type`
     * property — relationship discrimination happens via WHERE on that
     * property, not by edge label. Community membership is `MEMBER_OF`.
     *
     * Note: GitNexus's MEMBER_OF connects symbols (not files directly) to
     * communities. We aggregate per-community by filePath of the member symbols.
     */
    async clusterFiles() {
        const cypher = `MATCH (member)-[:CodeRelation {type: "MEMBER_OF"}]->(c:Community) RETURN c.id AS id, c.heuristicLabel AS label, collect(DISTINCT member.filePath) AS files`;
        const rows = this.cypher(cypher);
        return rows.map((r) => ({
            id: String(r.label || r.id || 'unnamed'),
            files: Array.isArray(r.files) ? r.files.filter(Boolean) : [],
        }));
    }
    /**
     * Engines 6.30.0+ — TODO #51 phase 1.
     * Native query against GitNexus's Route nodes + HANDLES_ROUTE edges.
     * Each Route has filePath + httpVerb + path properties; the handler is
     * resolved via the HANDLES_ROUTE edge. responseKeys / errorKeys /
     * middleware are populated when GitNexus's handler-shape detector fires.
     */
    async listRoutes() {
        const cypher = `MATCH (h)-[:CodeRelation {type: "HANDLES_ROUTE"}]->(r:Route) RETURN r.filePath AS filePath, r.httpVerb AS httpVerb, r.path AS routePath, h.name AS handlerName, h.startLine AS startLine, h.endLine AS endLine, r.responseKeys AS responseKeys, r.errorKeys AS errorKeys, r.middleware AS middleware`;
        const rows = this.cypher(cypher);
        return rows
            .filter((r) => r.filePath && r.httpVerb && r.routePath)
            .map((r) => ({
            filePath: String(r.filePath),
            httpVerb: String(r.httpVerb).toLowerCase(),
            routePath: String(r.routePath),
            handlerName: String(r.handlerName || 'anonymous'),
            sourceLineRange: (r.startLine && r.endLine)
                ? [Number(r.startLine), Number(r.endLine)]
                : undefined,
            responseKeys: Array.isArray(r.responseKeys) ? r.responseKeys.filter(Boolean) : undefined,
            errorKeys: Array.isArray(r.errorKeys) ? r.errorKeys.filter(Boolean) : undefined,
            middleware: Array.isArray(r.middleware) ? r.middleware.filter(Boolean) : undefined,
        }));
    }
    /**
     * Engines 6.30.0+ — TODO #51 phase 2.
     * Native query against GitNexus's IMPLEMENTS edges (Class → Interface).
     */
    async listImplementsRelationships() {
        const cypher = `MATCH (c:Class)-[:CodeRelation {type: "IMPLEMENTS"}]->(i:Interface) RETURN c.name AS className, c.filePath AS classFilePath, i.name AS interfaceName, i.filePath AS interfaceFilePath`;
        const rows = this.cypher(cypher);
        return rows
            .filter((r) => r.className && r.interfaceName)
            .map((r) => ({
            className: String(r.className),
            interfaceName: String(r.interfaceName),
            classFilePath: String(r.classFilePath || ''),
            interfaceFilePath: r.interfaceFilePath ? String(r.interfaceFilePath) : undefined,
        }));
    }
    /**
     * Engines 6.30.0+ — TODO #51 phase 3.
     * Native query against GitNexus's Const nodes whose name suggests a
     * validation schema (Zod/Yup/Joi). The adapter parses `content` for
     * library-specific shape extraction.
     */
    async listConstSchemas() {
        const cypher = `MATCH (c:Const) WHERE c.name =~ ".*[Ss]chema$" OR c.content =~ ".*z\\\\.(object|string|number|enum|literal|array|union|tuple|record|map).*" OR c.content =~ ".*yup\\\\.(object|string|number).*" OR c.content =~ ".*Joi\\\\.(object|string|number).*" RETURN c.name AS name, c.filePath AS filePath, c.content AS content, c.startLine AS startLine, c.endLine AS endLine`;
        const rows = this.cypher(cypher);
        return rows
            .filter((r) => r.name && r.content)
            .map((r) => {
            const content = String(r.content);
            let library;
            if (/\bz\.(object|string|number|enum|literal|array|union)/.test(content))
                library = 'zod';
            else if (/\byup\.(object|string|number)/.test(content))
                library = 'yup';
            else if (/\bJoi\.(object|string|number)/.test(content))
                library = 'joi';
            return {
                name: String(r.name),
                filePath: String(r.filePath || ''),
                content,
                sourceLineRange: (r.startLine && r.endLine)
                    ? [Number(r.startLine), Number(r.endLine)]
                    : undefined,
                library,
            };
        });
    }
    /**
     * Engines 6.30.0+ — TODO #51 phase 4.
     * Native query against GitNexus's Process nodes + STEP_IN_PROCESS edges.
     * Each process is a heuristic-labelled execution flow. We return them
     * with steps in order; adapters filter by stepCount and processType.
     */
    async listProcesses() {
        // First query: process metadata.
        const procCypher = `MATCH (p:Process) RETURN p.id AS id, p.heuristicLabel AS label, p.entryPointId AS entryPointId, p.terminalId AS terminalId, p.stepCount AS stepCount, p.processType AS processType`;
        const procRows = this.cypher(procCypher);
        // Second query: ordered steps for all processes.
        const stepCypher = `MATCH (s)-[r:CodeRelation {type: "STEP_IN_PROCESS"}]->(p:Process) RETURN p.id AS processId, s.id AS symbolId, r.step AS step ORDER BY p.id, r.step`;
        const stepRows = this.cypher(stepCypher);
        const stepsByProcess = new Map();
        for (const sr of stepRows) {
            const pid = String(sr.processId);
            if (!stepsByProcess.has(pid))
                stepsByProcess.set(pid, []);
            stepsByProcess.get(pid).push({
                symbolId: String(sr.symbolId),
                step: Number(sr.step ?? 0),
            });
        }
        return procRows
            .filter((p) => p.id && p.entryPointId)
            .map((p) => ({
            id: String(p.id),
            label: String(p.label || p.id),
            entryPointId: String(p.entryPointId),
            terminalId: String(p.terminalId || ''),
            stepCount: Number(p.stepCount ?? 0),
            processType: String(p.processType || 'intra_community'),
            steps: stepsByProcess.get(String(p.id)) ?? [],
        }));
    }
    /**
     * GitNexus's call graph traversal — same `CodeRelation` table pattern
     * as MEMBER_OF, with `type='CALLS'` as the discriminating property.
     * (Override the default callers/callees from the parent class above
     * because the relation type isn't `:CALLS` — it's `:CodeRelation` with
     * a property filter.)
     */
    async getMethodDetails(qualifiedName) {
        const lastDot = qualifiedName.lastIndexOf('.');
        if (lastDot < 0)
            return null;
        const className = qualifiedName.slice(0, lastDot);
        const methodName = qualifiedName.slice(lastDot + 1);
        // Find the method via class-scoped query
        const methods = await this.listMethods({ class: className, nameIn: [methodName] });
        if (methods.length === 0)
            return null;
        const method = methods[0];
        const fileText = readFileSync(method.filePath, 'utf8');
        const fileLines = fileText.split('\n');
        const bodyLines = fileLines.slice(method.startLine - 1, method.endLine);
        const body = bodyLines.join('\n');
        // Calls — use the call graph (GitNexus's strength like CodeGraph's)
        const calls = await this.callees(qualifiedName);
        return {
            qualifiedName,
            signature: method.signature ?? '',
            body,
            filePath: method.filePath,
            startLine: method.startLine,
            endLine: method.endLine,
            language: method.language,
            calls,
            emits: extractEmits(body),
            dbWrites: extractDbWrites(body),
            externalCalls: extractExternalCalls(body),
            throws: extractThrows(body),
            asyncBoundaries: extractAsyncBoundaries(body),
            branchPoints: countBranchPoints(body),
            bodyTextSliced: sliceBody(body),
        };
    }
    // ────────────────────────────────────────────────────────────────────
    // Internals
    // ────────────────────────────────────────────────────────────────────
    /**
     * Run a Cypher query via the `gitnexus cypher` CLI and parse the
     * markdown-table output into a row array. Each row is a map of
     * column name → value (string, number, or array depending on Cypher).
     *
     * Passes `--repo <name>` to disambiguate when multiple repos are
     * registered globally. The name comes from `~/.gitnexus/registry.json`
     * (resolved by `init()`), NOT the basename of sourceDir — the basename
     * is unsafe because gitnexus derives the registered name from the git
     * remote / package.json, which usually differs from the dir name
     * (e.g. clones into `./source/` register as `nestjs-realworld-example-app`).
     */
    cypher(query) {
        if (!this.repoName) {
            throw new Error('[gitnexus-backend] repoName not set — did you call init() before querying? ' +
                'init() resolves the canonical repo name from ~/.gitnexus/registry.json.');
        }
        const stdout = execFileSync(this.gitnexusPath, ['cypher', '--repo', this.repoName, query], {
            cwd: this.sourceDir,
            encoding: 'utf8',
            timeout: 60_000,
            maxBuffer: 50 * 1024 * 1024,
            env: this.execEnv(),
        });
        if (!stdout.trim())
            return [];
        let payload;
        try {
            payload = JSON.parse(stdout);
        }
        catch {
            throw new Error(`gitnexus cypher: failed to parse JSON output:\n${stdout.slice(0, 500)}`);
        }
        if (payload.error) {
            throw new Error(`gitnexus cypher error: ${payload.error}`);
        }
        if (!payload.markdown)
            return [];
        return parseMarkdownTable(payload.markdown);
    }
    rowToSymbol(row, kind) {
        const filePath = String(row.filePath || '');
        const absPath = filePath.startsWith('/') ? filePath : join(this.sourceDir, filePath);
        return {
            kind,
            name: String(row.name),
            qualifiedName: String(row.name), // GitNexus's qualified IDs are colon-separated; we use plain name for now
            filePath: absPath,
            startLine: Number(row.startLine ?? 0),
            endLine: Number(row.endLine ?? 0),
            signature: row.signature ?? undefined,
            docstring: row.docstring ?? undefined,
            isExported: row.isExported === 'true' || row.isExported === true,
            language: detectLanguage(absPath) || 'unknown',
        };
    }
    mapLabel(label) {
        switch (label) {
            case 'Class': return 'class';
            case 'Method': return 'method';
            case 'Function': return 'function';
            case 'Interface': return 'interface';
            case 'Const': return 'constant';
            default: return 'function';
        }
    }
    toRelativePath(p) {
        return p.startsWith(this.sourceDir)
            ? p.slice(this.sourceDir.length).replace(/^\//, '')
            : p;
    }
    escape(s) {
        return s.replace(/'/g, "\\'");
    }
    detectBinary(name) {
        try {
            const path = execSync(`which ${name}`, { encoding: 'utf8', timeout: 3000 }).trim();
            if (path)
                return path;
        }
        catch {
            // Fall through
        }
        return name;
    }
}
/**
 * Extract the basename of a sourceDir for fallback-naming. `/foo/bar/source/`
 * → `source`. Empty path → `source` (the gitnexus default for unnamed dirs).
 */
export function basenameOrSource(sourceDir) {
    return sourceDir.split('/').filter(Boolean).pop() || 'source';
}
/**
 * Pure-function version of the registry-lookup logic — takes the registry
 * file content + the sourceDir and returns the canonical name (or null if
 * not found, plus an optional warning). Exported for unit testing without
 * needing gitnexus installed or a real ~/.gitnexus directory.
 *
 * Match order:
 *   1. Exact `path === sourceDir`
 *   2. `storagePath` is inside sourceDir (handles symlink-resolved paths)
 */
export function resolveRepoNameFromRegistry(registryContent, sourceDir) {
    let entries;
    try {
        entries = JSON.parse(registryContent);
    }
    catch (err) {
        return {
            name: null,
            warning: `failed to parse registry JSON: ${err.message}`,
        };
    }
    if (!Array.isArray(entries)) {
        return { name: null, warning: 'registry is not an array' };
    }
    const match = entries.find((e) => e.path === sourceDir) ??
        entries.find((e) => e.storagePath?.startsWith(sourceDir + '/'));
    if (match)
        return { name: match.name, warning: null };
    return {
        name: null,
        warning: `no registry entry for ${sourceDir}; using basename fallback. ` +
            `Multi-repo queries may cross-contaminate.`,
    };
}
/**
 * Parse a GitHub-flavored markdown table into a row array.
 *
 * Input shape:
 *   | col1 | col2 |
 *   | --- | --- |
 *   | val1 | val2 |
 *
 * Returns: [ { col1: 'val1', col2: 'val2' }, ... ]
 *
 * Cell values are strings; callers can convert to number/boolean/JSON
 * as needed. Cells starting with `[` are parsed as JSON arrays
 * (GitNexus's `collect()` returns arrays in markdown form).
 */
function parseMarkdownTable(md) {
    const lines = md.split('\n').filter((l) => l.trim().length > 0);
    if (lines.length < 2)
        return [];
    const headerLine = lines[0];
    const headers = headerLine
        .split('|')
        .map((s) => s.trim())
        .filter(Boolean);
    // Skip the separator row (---|---|---)
    const rows = [];
    for (let i = 2; i < lines.length; i++) {
        const cells = lines[i]
            .split('|')
            .map((s) => s.trim())
            .filter((_, idx, arr) => idx > 0 && idx < arr.length - 1 || arr.length === headers.length);
        // Sometimes the leading/trailing | are missing — fall back to a permissive split.
        const rawCells = lines[i].split('|');
        const cleaned = rawCells.length === headers.length + 2
            ? rawCells.slice(1, -1).map((s) => s.trim())
            : rawCells.map((s) => s.trim()).filter(Boolean);
        const row = {};
        for (let j = 0; j < headers.length; j++) {
            let val = cleaned[j] ?? '';
            // Try to parse JSON arrays (collect() output)
            if (typeof val === 'string' && val.startsWith('[') && val.endsWith(']')) {
                try {
                    val = JSON.parse(val);
                }
                catch {
                    // leave as string
                }
            }
            row[headers[j]] = val;
        }
        rows.push(row);
    }
    return rows;
}
/**
 * Generate a unique repo name from an absolute source path. GitNexus's
 * default name-resolution walks UP the directory tree to find package.json
 * or git metadata; that collides when many sibling run dirs share one
 * project root. Naming each indexed source by `<basename>-<8charHash>`
 * makes every analyze() create its own registry entry + storage.
 *
 * Hash uses sha256 of the absolute path so it's stable across calls.
 */
function computeUniqueRepoName(sourceDir) {
    const segments = sourceDir.split('/').filter(Boolean);
    const basename = segments[segments.length - 1] ?? 'src';
    // Include the parent segment too for human readability when source
    // dirs nest 3-4 levels deep (e.g. `runs/analyse/idle-meta/<run>/input`
    // → use `<run>-input-<hash>`).
    const parent = segments[segments.length - 2] ?? '';
    const stem = parent && parent !== basename ? `${parent}-${basename}` : basename;
    // Sanitise to gitnexus-friendly chars (alphanumeric + dash).
    const safeStem = stem.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/--+/g, '-').slice(0, 60);
    const hash = createHash('sha256').update(sourceDir).digest('hex').slice(0, 8);
    return `${safeStem}-${hash}`;
}
/** Escape a value for safe use in a shell command. Conservative — wrap
 *  in single quotes and escape existing single quotes via close-escape-open. */
function shellEscape(value) {
    return `'${value.replace(/'/g, `'\\''`)}'`;
}
//# sourceMappingURL=gitnexus.js.map