import { fork } from 'node:child_process';
import { existsSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { analyzeWorkspace } from './analyze.js';
import { describeWorkspace } from './describe.js';
import { inspectWorkspaceDocumentSet } from './documents.js';
import { readSectionedWorkspaceBundle } from './bundle-io.js';
import { createSharedMarkdownConverter } from '../markdown/convert.js';
import { buildCodebaseIndexIsolated } from '../code/isolated.js';
import { previewCodebaseSize } from '../code/preview.js';
import { openCodeIndexStore } from '../code/store.js';
import { slimifyIndex, slimifyIndexCooperative } from '../code/slim.js';
import { chunkFromIndex, chunkFromIndexCooperative, chunkFromIndexStreaming, } from '../code/chunk.js';
import { chunkDocument, chunkDocumentFromMarkdown, } from '../doc/chunk.js';
import { DEFAULT_EXCLUDE_DIRS, abortIfNeeded, createYielder, isCooperative, mapWithConcurrency, normalizeRelativePath, sanitizeForkExecArgv, } from '../utils.js';
import { OccAbortError, isOccAbortError } from '../errors.js';
// Step descriptors shared by the sequential and concurrent phase paths.
const STEP_DESCRIPTION = { phase: 'description', detail: 'Describe workspace' };
const STEP_ANALYSIS = { phase: 'stats', detail: 'Analyze workspace totals' };
const STEP_DOCUMENTS = { phase: 'inspect', detail: 'Inspect documents' };
const STEP_CODE_PREVIEW = { phase: 'code-preview', detail: 'Preview code index size' };
const STEP_CODE_INDEX = { phase: 'index', detail: 'Build code index' };
const STEP_CODE_CHUNK = { phase: 'code-chunk', detail: 'Chunk code files' };
const STEP_DOCUMENT_CHUNK = { phase: 'document-chunk', detail: 'Chunk documents' };
const STEP_OUTLINE = { phase: 'outline', detail: 'Build workspace outline' };
const SECTIONED_BUNDLE_READ_SECTIONS = 6;
function reportStep(onProgress, step, completed, total, startedAt) {
    onProgress?.({
        phase: step.phase,
        total,
        completed,
        detail: step.detail,
        scope: 'workspace',
        startedAt,
        elapsedMs: Date.now() - startedAt,
    });
}
function commonWorkspaceOptions(options) {
    return {
        excludeDir: options.excludeDir ?? [...DEFAULT_EXCLUDE_DIRS],
        noGitignore: options.noGitignore,
        ignorePatterns: options.ignorePatterns,
        overlayIgnorePatterns: options.overlayIgnorePatterns,
        largeFileLimitMb: options.largeFileLimitMb,
    };
}
function buildOutline(rootDir, description, documents, codeIndex) {
    const root = {
        id: `workspace:${rootDir}`,
        kind: 'root',
        title: path.basename(rootDir) || rootDir,
        path: rootDir,
        children: [],
    };
    for (const project of description?.projects ?? []) {
        root.children.push({
            id: `project:${project.path}`,
            kind: 'project',
            title: project.path === '.' ? path.basename(rootDir) || rootDir : project.path,
            path: project.path,
            metrics: {
                confidence: project.confidence,
                frameworks: project.frameworks.length,
            },
            children: [],
        });
    }
    const moduleCounts = new Map();
    for (const file of codeIndex?.files ?? []) {
        const firstSegment = file.relativePath.split('/')[0];
        if (!firstSegment || firstSegment === file.relativePath)
            continue;
        moduleCounts.set(firstSegment, (moduleCounts.get(firstSegment) ?? 0) + 1);
    }
    for (const [modulePath, files] of [...moduleCounts.entries()]
        .sort((left, right) => right[1] - left[1])
        .slice(0, 20)) {
        root.children.push({
            id: `module:${modulePath}`,
            kind: 'module',
            title: modulePath,
            path: modulePath,
            metrics: { files },
            children: [],
        });
    }
    for (const doc of documents?.documents ?? []) {
        root.children.push({
            id: `document:${doc.relativePath}`,
            kind: 'document',
            title: doc.title ?? doc.relativePath,
            path: doc.relativePath,
            metrics: {
                words: doc.words,
                tokenEstimate: doc.tokenEstimate,
                headingCount: doc.headingCount,
            },
            children: doc.topLevelSections.map(section => ({
                id: `section:${doc.relativePath}:${section}`,
                kind: 'section',
                title: section,
                path: doc.relativePath,
                children: [],
            })),
        });
    }
    return root;
}
function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function detectCodeDocumentReferences(codeIndex, documents, limit) {
    if (!codeIndex || !documents || limit <= 0)
        return [];
    const symbolNodes = codeIndex.nodes
        .filter(node => ['class', 'interface', 'type-alias', 'enum', 'function'].includes(node.type))
        .filter(node => node.name.length >= 3)
        .sort((left, right) => right.name.length - left.name.length || left.name.localeCompare(right.name));
    const seenNames = new Set();
    const symbols = symbolNodes
        .filter(node => {
        if (seenNames.has(node.name))
            return false;
        seenNames.add(node.name);
        return true;
    })
        .slice(0, 1_000);
    // Compile each symbol pattern once instead of per line.
    const compiled = symbols.map(symbol => ({
        symbol,
        pattern: new RegExp(`\\b${escapeRegExp(symbol.name)}\\b`),
    }));
    // One alternation over all (longest-first, already-escaped) names, used
    // purely as a per-line skip filter — extraction still runs the per-symbol
    // patterns, so output order and the limit early-return are unchanged.
    let prescreen = null;
    if (symbols.length > 0) {
        try {
            prescreen = new RegExp(`\\b(?:${symbols.map(symbol => escapeRegExp(symbol.name)).join('|')})\\b`);
        }
        catch {
            prescreen = null; // Fall back to scanning every line with the compiled patterns.
        }
    }
    const references = [];
    for (const doc of documents.documents) {
        if (!doc.markdownContent)
            continue;
        const lines = doc.markdownContent.split(/\r?\n/);
        for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
            const line = lines[lineIndex];
            if (!line.trim())
                continue;
            if (prescreen && !prescreen.test(line))
                continue;
            for (const { symbol, pattern } of compiled) {
                if (!pattern.test(line))
                    continue;
                references.push({
                    documentPath: doc.relativePath,
                    symbolName: symbol.name,
                    symbolType: symbol.type,
                    symbolPath: symbol.relativePath ?? normalizeRelativePath(path.relative(codeIndex.repoRoot, symbol.path)),
                    symbolLine: symbol.line,
                    line: lineIndex + 1,
                    context: line.trim().slice(0, 240),
                });
                if (references.length >= limit)
                    return references;
            }
        }
    }
    return references;
}
async function detectCodeDocumentReferencesCooperative(codeIndex, documents, limit, yielder) {
    if (!codeIndex || !documents || limit <= 0)
        return [];
    const symbolNodes = codeIndex.nodes
        .filter(node => ['class', 'interface', 'type-alias', 'enum', 'function'].includes(node.type))
        .filter(node => node.name.length >= 3)
        .sort((left, right) => right.name.length - left.name.length || left.name.localeCompare(right.name));
    const seenNames = new Set();
    const symbols = symbolNodes
        .filter(node => {
        if (seenNames.has(node.name))
            return false;
        seenNames.add(node.name);
        return true;
    })
        .slice(0, 1_000);
    const compiled = symbols.map(symbol => ({
        symbol,
        pattern: new RegExp(`\\b${escapeRegExp(symbol.name)}\\b`),
    }));
    let prescreen = null;
    if (symbols.length > 0) {
        try {
            prescreen = new RegExp(`\\b(?:${symbols.map(symbol => escapeRegExp(symbol.name)).join('|')})\\b`);
        }
        catch {
            prescreen = null;
        }
    }
    const references = [];
    for (const doc of documents.documents) {
        if (!doc.markdownContent) {
            await yielder.tick();
            continue;
        }
        const lines = doc.markdownContent.split(/\r?\n/);
        for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
            const line = lines[lineIndex];
            if (!line.trim()) {
                if ((lineIndex + 1) % 2_000 === 0)
                    await yielder.tick();
                continue;
            }
            if (prescreen && !prescreen.test(line)) {
                if ((lineIndex + 1) % 2_000 === 0)
                    await yielder.tick();
                continue;
            }
            for (const { symbol, pattern } of compiled) {
                if (!pattern.test(line))
                    continue;
                references.push({
                    documentPath: doc.relativePath,
                    symbolName: symbol.name,
                    symbolType: symbol.type,
                    symbolPath: symbol.relativePath ?? normalizeRelativePath(path.relative(codeIndex.repoRoot, symbol.path)),
                    symbolLine: symbol.line,
                    line: lineIndex + 1,
                    context: line.trim().slice(0, 240),
                });
                if (references.length >= limit)
                    return references;
            }
            if ((lineIndex + 1) % 2_000 === 0)
                await yielder.tick();
        }
        await yielder.tick();
    }
    return references;
}
async function buildDocumentChunks(documents, options, onProgress, yielder) {
    if (!documents)
        return [];
    const maxChunks = options.maxDocumentChunks ?? 500;
    const total = documents.documents.length;
    let completed = 0;
    let accumulatedChunks = 0;
    // Workers skip documents once the running chunk total reaches the cap, so
    // conversion stops near it instead of chunking every document (documents
    // already in flight still finish — up to the concurrency width of overshoot).
    // Items are dispatched in document order, so every document that contributed
    // to the total precedes any skipped one; flattening in document order plus
    // the post-join slice stays the authoritative cap, and the returned chunks
    // are identical to chunking everything.
    const perDocument = await mapWithConcurrency(documents.documents, 5, async (doc) => {
        if (accumulatedChunks >= maxChunks)
            return undefined;
        let docChunks;
        try {
            docChunks =
                doc.markdownContent != null
                    ? // Markdown was already produced by document inspection — reuse it
                        // instead of re-reading and re-converting the file. Per-document
                        // values are spread last so caller chunk options cannot clobber them.
                        chunkDocumentFromMarkdown(doc.markdownContent, {
                            ...options.chunk,
                            filePath: doc.filePath,
                            relativePath: doc.relativePath,
                        })
                    : await chunkDocument(doc.filePath, {
                        ...options.chunk,
                        relativePath: doc.relativePath,
                    });
        }
        catch {
            // Document conversion errors are already represented in inspectDocuments.
        }
        if (docChunks)
            accumulatedChunks += docChunks.length;
        onProgress?.({
            phase: 'document-chunk',
            total,
            completed: ++completed,
            detail: doc.relativePath,
            currentPath: doc.filePath,
            scope: 'document',
        });
        if (yielder)
            await yielder.tick();
        return docChunks;
    }, options.signal);
    const chunks = [];
    for (const docChunks of perDocument) {
        if (docChunks)
            chunks.push(...docChunks);
    }
    return chunks.slice(0, maxChunks);
}
async function buildCodeChunks(codeIndex, options, onProgress, yielder) {
    if (!codeIndex)
        return [];
    const chunks = options.streamCodeChunks
        ? await chunkFromIndexStreaming(codeIndex, options.codeChunk, {
            signal: options.signal,
            yielder,
        })
        : yielder
            ? await chunkFromIndexCooperative(codeIndex, options.codeChunk, yielder)
            : chunkFromIndex(codeIndex, options.codeChunk);
    const capped = options.maxCodeChunks != null ? chunks.slice(0, Math.max(0, options.maxCodeChunks)) : chunks;
    onProgress?.({
        phase: 'code-chunk',
        total: capped.length,
        completed: capped.length,
        detail: 'Code chunking complete',
        scope: 'code',
    });
    return capped;
}
function mapCodeChunk(chunk) {
    return {
        kind: 'code',
        chunkId: `${chunk.relativePath}#code-${chunk.chunkIndex}`,
        filePath: chunk.filePath,
        relativePath: chunk.relativePath,
        title: chunk.title,
        content: chunk.content,
        chunkIndex: chunk.chunkIndex,
        startLine: chunk.startLine,
        endLine: chunk.endLine,
        language: chunk.language,
        symbolNames: chunk.symbolNames,
        symbolTypes: chunk.symbolTypes,
    };
}
function mapDocumentChunksToCorpus(chunks) {
    const perFileIndex = new Map();
    return chunks.map(chunk => {
        const chunkIndex = perFileIndex.get(chunk.relativePath) ?? 0;
        perFileIndex.set(chunk.relativePath, chunkIndex + 1);
        return {
            kind: 'document',
            chunkId: chunk.chunkId,
            filePath: chunk.filePath,
            relativePath: chunk.relativePath,
            title: chunk.title,
            content: chunk.content,
            chunkIndex,
            startLine: chunk.startLine,
            endLine: chunk.endLine,
            format: chunk.format,
            headingPath: chunk.headingPath,
            anchor: chunk.anchor,
            tokenEstimate: chunk.tokenEstimate,
            wordCount: chunk.wordCount,
        };
    });
}
async function buildCorpusChunks(codeChunks, documentChunks, options, yielder) {
    if (!options.includeCorpusChunks)
        return undefined;
    let chunks;
    if (!yielder) {
        chunks = [...(codeChunks ?? []).map(mapCodeChunk), ...mapDocumentChunksToCorpus(documentChunks ?? [])];
    }
    else {
        chunks = [];
        let mapped = 0;
        for (const chunk of codeChunks ?? []) {
            chunks.push(mapCodeChunk(chunk));
            if (++mapped % 512 === 0)
                await yielder.tick();
        }
        const perFileIndex = new Map();
        for (const chunk of documentChunks ?? []) {
            const chunkIndex = perFileIndex.get(chunk.relativePath) ?? 0;
            perFileIndex.set(chunk.relativePath, chunkIndex + 1);
            chunks.push({
                kind: 'document',
                chunkId: chunk.chunkId,
                filePath: chunk.filePath,
                relativePath: chunk.relativePath,
                title: chunk.title,
                content: chunk.content,
                chunkIndex,
                startLine: chunk.startLine,
                endLine: chunk.endLine,
                format: chunk.format,
                headingPath: chunk.headingPath,
                anchor: chunk.anchor,
                tokenEstimate: chunk.tokenEstimate,
                wordCount: chunk.wordCount,
            });
            if (++mapped % 512 === 0)
                await yielder.tick();
        }
    }
    chunks.sort((left, right) => left.relativePath.localeCompare(right.relativePath) ||
        left.kind.localeCompare(right.kind) ||
        left.chunkIndex - right.chunkIndex);
    const maxChunks = options.maxCorpusChunks;
    return maxChunks != null ? chunks.slice(0, Math.max(0, maxChunks)) : chunks;
}
function resolveBundleRunnerPath() {
    const directory = path.dirname(fileURLToPath(import.meta.url));
    const builtRunner = path.join(directory, 'bundle-runner.js');
    if (existsSync(builtRunner))
        return builtRunner;
    return path.join(directory, 'bundle-runner.ts');
}
function serializableBundleOptions(options) {
    const { signal: _signal, onProgress: _onProgress, store: _store, cooperative: _cooperative, ...rest } = options;
    return {
        ...rest,
        offThread: false,
        chunk: rest.chunk
            ? {
                maxTokens: rest.chunk.maxTokens,
                overlapTokens: rest.chunk.overlapTokens,
            }
            : undefined,
        codeChunk: rest.codeChunk
            ? {
                maxTokens: rest.codeChunk.maxTokens,
                overlapTokens: rest.codeChunk.overlapTokens,
                files: rest.codeChunk.files,
            }
            : undefined,
    };
}
export async function bundleWorkspaceIsolated(rootDir, options = {}, onProgress) {
    if (options.store) {
        throw new Error('offThread bundle requires cacheDir or no store; a caller-provided store instance cannot cross the process boundary');
    }
    abortIfNeeded(options.signal);
    const runnerPath = resolveBundleRunnerPath();
    const progress = onProgress ?? options.onProgress;
    const childOptions = serializableBundleOptions(options);
    const resultReadYielder = createYielder(isCooperative(options.cooperative) ? options.cooperative : { yieldEveryMs: 0 }, options.signal);
    return new Promise((resolve, reject) => {
        let settled = false;
        let abortTimer;
        const child = fork(runnerPath, [], {
            stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
            execArgv: sanitizeForkExecArgv(),
        });
        const cleanup = () => {
            if (abortTimer)
                clearTimeout(abortTimer);
            options.signal?.removeEventListener('abort', handleAbort);
        };
        const rejectAbort = () => {
            cleanup();
            reject(new OccAbortError());
        };
        const handleAbort = () => {
            if (settled)
                return;
            child.send({ type: 'abort' });
            abortTimer = setTimeout(() => {
                if (!settled)
                    child.kill();
            }, 1000);
        };
        options.signal?.addEventListener('abort', handleAbort, { once: true });
        let stderr = '';
        child.stderr?.on('data', (chunk) => {
            stderr += chunk.toString();
        });
        child.on('message', (msg) => {
            if (msg.type === 'progress' && msg.event) {
                progress?.(msg.event);
            }
            else if (msg.type === 'aborted') {
                settled = true;
                rejectAbort();
            }
            else if (msg.type === 'complete' && msg.resultPath) {
                settled = true;
                void (async () => {
                    try {
                        const parentReadStartedAt = Date.now();
                        let completedReadSections = 0;
                        progress?.({
                            phase: 'deserialize',
                            total: SECTIONED_BUNDLE_READ_SECTIONS,
                            completed: 0,
                            detail: 'Parent result read started',
                            scope: 'workspace-parent-read',
                            startedAt: parentReadStartedAt,
                            elapsedMs: 0,
                            parentReadStartedAt,
                        });
                        const reportSectionTiming = (timing) => {
                            progress?.({
                                phase: 'deserialize',
                                total: SECTIONED_BUNDLE_READ_SECTIONS,
                                completed: ++completedReadSections,
                                detail: `Parent read ${timing.section}`,
                                scope: 'workspace-parent-read',
                                startedAt: parentReadStartedAt,
                                elapsedMs: Date.now() - parentReadStartedAt,
                                parentReadStartedAt,
                                section: timing.section,
                                sectionStartedAt: timing.startedAt,
                                sectionFinishedAt: timing.finishedAt,
                                sectionDurationMs: timing.durationMs,
                            });
                        };
                        const bundle = await readSectionedWorkspaceBundle(msg.resultPath, options.signal, resultReadYielder, reportSectionTiming);
                        const parentReadFinishedAt = Date.now();
                        progress?.({
                            phase: 'deserialize',
                            total: SECTIONED_BUNDLE_READ_SECTIONS,
                            completed: SECTIONED_BUNDLE_READ_SECTIONS,
                            detail: 'Parent result read complete',
                            scope: 'workspace-parent-read',
                            startedAt: parentReadStartedAt,
                            elapsedMs: parentReadFinishedAt - parentReadStartedAt,
                            parentReadStartedAt,
                            parentReadFinishedAt,
                            parentReadDurationMs: parentReadFinishedAt - parentReadStartedAt,
                        });
                        const cleanupStartedAt = Date.now();
                        await rm(msg.resultPath, { recursive: true, force: true }).catch(() => undefined);
                        const cleanupFinishedAt = Date.now();
                        progress?.({
                            phase: 'deserialize',
                            total: 1,
                            completed: 1,
                            detail: 'Parent result cleanup complete',
                            scope: 'workspace-parent-cleanup',
                            startedAt: cleanupStartedAt,
                            elapsedMs: cleanupFinishedAt - cleanupStartedAt,
                            cleanupDurationMs: cleanupFinishedAt - cleanupStartedAt,
                        });
                        cleanup();
                        resolve(bundle);
                    }
                    catch (err) {
                        await rm(msg.resultPath, { recursive: true, force: true }).catch(() => undefined);
                        cleanup();
                        reject(err instanceof Error ? err : new Error(String(err)));
                    }
                })();
            }
            else if (msg.type === 'error') {
                settled = true;
                cleanup();
                reject(new Error(msg.message ?? 'Off-thread bundle failed'));
            }
        });
        child.on('error', err => {
            settled = true;
            cleanup();
            reject(err);
        });
        child.on('close', code => {
            if (settled)
                return;
            cleanup();
            if (options.signal?.aborted) {
                rejectAbort();
                return;
            }
            if (code !== 0) {
                reject(new Error(`Bundle runner exited with code ${code}: ${stderr.trim()}`));
            }
            else {
                reject(new Error('Bundle runner exited without sending a result'));
            }
        });
        child.send({ type: 'start', rootDir: path.resolve(rootDir), options: childOptions });
    });
}
export async function bundleWorkspace(rootDir, options = {}) {
    if (options.offThread) {
        return bundleWorkspaceIsolated(rootDir, options, options.onProgress);
    }
    const startedAt = Date.now();
    const resolvedRoot = path.resolve(rootDir);
    const includeDescription = options.includeDescription !== false;
    const includeAnalysis = options.includeAnalysis !== false;
    const includeDocuments = options.includeDocuments !== false;
    const includeCode = options.includeCode !== false;
    const includeCodeChunks = options.includeCodeChunks ?? false;
    const includeCorpusChunks = options.includeCorpusChunks ?? false;
    const includeDocumentChunks = (options.includeDocumentChunks ?? false) || includeCorpusChunks;
    const shouldBuildCodeChunks = includeCode && (includeCodeChunks || includeCorpusChunks);
    const streamCodeChunks = shouldBuildCodeChunks && options.streamCodeChunks === true;
    const includeCodeDocumentReferences = options.includeCodeDocumentReferences ?? true;
    const concurrentPhases = options.concurrentPhases !== false;
    const onProgress = options.onProgress;
    const cooperative = isCooperative(options.cooperative);
    const yielder = cooperative ? createYielder(options.cooperative, options.signal) : undefined;
    const errors = [];
    const shared = commonWorkspaceOptions(options);
    // One conversion per document per bundle: the analysis and documents phases
    // (which may run concurrently) coalesce on this converter's promises.
    const convertToMarkdown = createSharedMarkdownConverter();
    const steps = [
        ...(includeDescription ? [STEP_DESCRIPTION] : []),
        ...(includeAnalysis ? [STEP_ANALYSIS] : []),
        ...(includeDocuments ? [STEP_DOCUMENTS] : []),
        ...(includeCode ? [STEP_CODE_PREVIEW] : []),
        ...(includeCode ? [STEP_CODE_INDEX] : []),
        ...(shouldBuildCodeChunks ? [STEP_CODE_CHUNK] : []),
        ...(includeDocumentChunks ? [STEP_DOCUMENT_CHUNK] : []),
        STEP_OUTLINE,
    ];
    let completedSteps = 0;
    const scopedCodeProgress = event => onProgress?.({ ...event, scope: event.scope ?? 'code' });
    // Resolves the code index either through a persistent store (cacheDir/store
    // options — cache hits skip parsing, misses build in-process) or through the
    // pre-existing isolated subprocess build.
    const acquireCodeIndex = async () => {
        const slim = options.slimCodeIndex ?? true;
        const contentMode = shouldBuildCodeChunks && !streamCodeChunks ? 'full' : (options.contentMode ?? (slim ? 'none' : 'excerpt'));
        if (options.store || options.cacheDir) {
            const store = options.store ??
                openCodeIndexStore({
                    repoRoot: resolvedRoot,
                    cacheDir: options.cacheDir,
                    excludeDir: shared.excludeDir,
                    noGitignore: shared.noGitignore,
                    ignorePatterns: shared.ignorePatterns,
                    overlayIgnorePatterns: shared.overlayIgnorePatterns,
                    contentMode,
                    maxFiles: options.maxCodeFiles,
                    maxBytes: options.maxCodeBytes,
                    onBudgetExceeded: 'truncate',
                });
            const session = await store.getSession({
                strategy: options.cacheStrategy ?? 'ensure-fresh',
                signal: options.signal,
                onProgress: scopedCodeProgress,
                cooperative: options.cooperative,
            });
            const index = session.getIndex();
            const codeIndex = slim ? (yielder ? await slimifyIndexCooperative(index, yielder) : slimifyIndex(index)) : index;
            return {
                codeIndex,
                chunkSource: streamCodeChunks ? codeIndex : index.contentMode === 'full' ? index : undefined,
            };
        }
        const index = await buildCodebaseIndexIsolated({
            repoRoot: resolvedRoot,
            ...shared,
            contentMode,
            maxFiles: options.maxCodeFiles,
            maxBytes: options.maxCodeBytes,
            onBudgetExceeded: 'truncate',
            slim: false,
            signal: options.signal,
        }, scopedCodeProgress);
        const codeIndex = slim
            ? yielder
                ? await slimifyIndexCooperative(index, yielder)
                : slimifyIndex(index)
            : index;
        return {
            codeIndex,
            chunkSource: streamCodeChunks
                ? codeIndex
                : index.contentMode === 'full'
                    ? index
                    : undefined,
        };
    };
    reportStep(onProgress, { phase: 'bundle', detail: 'Workspace bundle started' }, completedSteps, steps.length, startedAt);
    let description;
    let analysis;
    let documents;
    let codePreview;
    let codeIndex;
    let codeChunkSource;
    if (concurrentPhases) {
        // Phases run concurrently as error-capturing tasks. Errors are collected
        // per phase and assembled post-join in the canonical sequential order
        // (description, analysis, documents, code-preview, code) so errors[]
        // content and order match the concurrentPhases: false path. Step
        // completion order may vary; the shared counter stays monotonic and inner
        // per-phase events are already scope-tagged. Once the signal aborts, tasks
        // skip un-started phase bodies and suppress further step emissions; the
        // post-join abortIfNeeded turns the abort into a rejection.
        let caughtAbort;
        const phaseErrors = {
            description: [],
            analysis: [],
            documents: [],
            'code-preview': [],
            code: [],
        };
        const capture = (scope, err) => {
            if (isOccAbortError(err)) {
                caughtAbort ??= err;
                return;
            }
            const error = err;
            phaseErrors[scope].push({ scope, path: resolvedRoot, message: error.message });
        };
        const tasks = [];
        if (includeDescription) {
            tasks.push((async () => {
                if (options.signal?.aborted)
                    return; // already aborted — skip the phase entirely
                try {
                    description = await describeWorkspace(resolvedRoot, shared);
                }
                catch (err) {
                    capture('description', err);
                }
                if (options.signal?.aborted)
                    return; // aborted mid-phase — suppress the completion event
                reportStep(onProgress, STEP_DESCRIPTION, ++completedSteps, steps.length, startedAt);
            })());
        }
        if (includeAnalysis) {
            tasks.push((async () => {
                if (options.signal?.aborted)
                    return; // already aborted — skip the phase entirely
                try {
                    analysis = await analyzeWorkspace(resolvedRoot, {
                        ...shared,
                        includeDocuments,
                        includeCode,
                        convertToMarkdown,
                    }, event => onProgress?.({ ...event, scope: event.scope ?? 'workspace' }));
                }
                catch (err) {
                    capture('analysis', err);
                }
                if (options.signal?.aborted)
                    return; // aborted mid-phase — suppress the completion event
                reportStep(onProgress, STEP_ANALYSIS, ++completedSteps, steps.length, startedAt);
            })());
        }
        if (includeDocuments) {
            tasks.push((async () => {
                if (options.signal?.aborted)
                    return; // already aborted — skip the phase entirely
                try {
                    documents = await inspectWorkspaceDocumentSet(resolvedRoot, {
                        ...shared,
                        includeMarkdown: includeCodeDocumentReferences,
                        includeDataFiles: options.includeDataFiles,
                        maxFiles: options.maxDocumentFiles,
                        maxReferenceFiles: options.maxReferenceFiles,
                        convertToMarkdown,
                    }, event => onProgress?.({ ...event, scope: event.scope ?? 'document' }));
                }
                catch (err) {
                    capture('documents', err);
                }
                if (options.signal?.aborted)
                    return; // aborted mid-phase — suppress the completion event
                reportStep(onProgress, STEP_DOCUMENTS, ++completedSteps, steps.length, startedAt);
            })());
        }
        if (includeCode) {
            // Code preview then index — sequential within this task.
            tasks.push((async () => {
                if (options.signal?.aborted)
                    return; // already aborted — skip the phase entirely
                try {
                    codePreview = await previewCodebaseSize({
                        repoRoot: resolvedRoot,
                        ...shared,
                        maxFiles: options.maxCodeFiles,
                        maxBytes: options.maxCodeBytes,
                        signal: options.signal,
                    });
                }
                catch (err) {
                    capture('code-preview', err);
                }
                if (options.signal?.aborted)
                    return; // aborted mid-phase — suppress events and skip the index build
                reportStep(onProgress, STEP_CODE_PREVIEW, ++completedSteps, steps.length, startedAt);
                if (options.signal?.aborted)
                    return; // abort fired from the preview step emission
                try {
                    const acquired = await acquireCodeIndex();
                    codeIndex = acquired.codeIndex;
                    codeChunkSource = acquired.chunkSource;
                }
                catch (err) {
                    capture('code', err);
                }
                if (options.signal?.aborted)
                    return; // aborted mid-phase — suppress the completion event
                reportStep(onProgress, STEP_CODE_INDEX, ++completedSteps, steps.length, startedAt);
            })());
        }
        await Promise.all(tasks);
        errors.push(...phaseErrors.description, ...phaseErrors.analysis, ...phaseErrors.documents, ...phaseErrors['code-preview'], ...phaseErrors.code);
        abortIfNeeded(options.signal);
        if (caughtAbort)
            throw caughtAbort;
    }
    else {
        if (includeDescription) {
            abortIfNeeded(options.signal);
            try {
                description = await describeWorkspace(resolvedRoot, shared);
            }
            catch (err) {
                const error = err;
                errors.push({ scope: 'description', path: resolvedRoot, message: error.message });
            }
            reportStep(onProgress, steps[completedSteps], ++completedSteps, steps.length, startedAt);
        }
        if (includeAnalysis) {
            abortIfNeeded(options.signal);
            try {
                analysis = await analyzeWorkspace(resolvedRoot, {
                    ...shared,
                    includeDocuments,
                    includeCode,
                    convertToMarkdown,
                }, event => onProgress?.({ ...event, scope: event.scope ?? 'workspace' }));
            }
            catch (err) {
                const error = err;
                errors.push({ scope: 'analysis', path: resolvedRoot, message: error.message });
            }
            reportStep(onProgress, steps[completedSteps], ++completedSteps, steps.length, startedAt);
        }
        if (includeDocuments) {
            abortIfNeeded(options.signal);
            try {
                documents = await inspectWorkspaceDocumentSet(resolvedRoot, {
                    ...shared,
                    includeMarkdown: includeCodeDocumentReferences,
                    includeDataFiles: options.includeDataFiles,
                    maxFiles: options.maxDocumentFiles,
                    maxReferenceFiles: options.maxReferenceFiles,
                    convertToMarkdown,
                }, event => onProgress?.({ ...event, scope: event.scope ?? 'document' }));
            }
            catch (err) {
                const error = err;
                errors.push({ scope: 'documents', path: resolvedRoot, message: error.message });
            }
            reportStep(onProgress, steps[completedSteps], ++completedSteps, steps.length, startedAt);
        }
        if (includeCode) {
            abortIfNeeded(options.signal);
            try {
                codePreview = await previewCodebaseSize({
                    repoRoot: resolvedRoot,
                    ...shared,
                    maxFiles: options.maxCodeFiles,
                    maxBytes: options.maxCodeBytes,
                    signal: options.signal,
                });
            }
            catch (err) {
                const error = err;
                errors.push({ scope: 'code-preview', path: resolvedRoot, message: error.message });
            }
            reportStep(onProgress, steps[completedSteps], ++completedSteps, steps.length, startedAt);
            abortIfNeeded(options.signal);
            try {
                const acquired = await acquireCodeIndex();
                codeIndex = acquired.codeIndex;
                codeChunkSource = acquired.chunkSource;
            }
            catch (err) {
                const error = err;
                errors.push({ scope: 'code', path: resolvedRoot, message: error.message });
            }
            reportStep(onProgress, steps[completedSteps], ++completedSteps, steps.length, startedAt);
        }
    }
    let codeChunks;
    if (shouldBuildCodeChunks) {
        try {
            if (!codeChunkSource) {
                throw new Error('Code chunks require a full-content code index.');
            }
            codeChunks = await buildCodeChunks(codeChunkSource, options, onProgress, yielder);
        }
        catch (err) {
            const error = err;
            errors.push({ scope: 'code-chunk', path: resolvedRoot, message: error.message });
            codeChunks = [];
        }
        reportStep(onProgress, steps[completedSteps], ++completedSteps, steps.length, startedAt);
    }
    const documentChunks = includeDocumentChunks
        ? await buildDocumentChunks(documents, options, onProgress, yielder)
        : undefined;
    if (includeDocumentChunks) {
        reportStep(onProgress, steps[completedSteps], ++completedSteps, steps.length, startedAt);
    }
    const outline = buildOutline(resolvedRoot, description, documents, codeIndex);
    const referenceLimit = includeCodeDocumentReferences ? (options.maxCodeDocumentReferences ?? 200) : 0;
    const codeDocumentReferences = yielder
        ? await detectCodeDocumentReferencesCooperative(codeIndex, documents, referenceLimit, yielder)
        : detectCodeDocumentReferences(codeIndex, documents, referenceLimit);
    reportStep(onProgress, steps[completedSteps], ++completedSteps, steps.length, startedAt);
    reportStep(onProgress, { phase: 'bundle', detail: 'Workspace bundle complete' }, steps.length, steps.length, startedAt);
    return {
        schemaVersion: 1,
        rootDir: resolvedRoot,
        generatedAt: new Date().toISOString(),
        description,
        analysis,
        documents,
        codePreview,
        codeIndex,
        documentChunks,
        codeChunks,
        corpusChunks: await buildCorpusChunks(codeChunks, documentChunks, options, yielder),
        outline,
        codeDocumentReferences,
        errors,
    };
}
//# sourceMappingURL=bundle.js.map