import { SYSTEM_PROMPT } from './spec-templates.js';
// ─── Constants ──────────────────────────────────────────
const TAG = 'Spec';
const QUERY_BODY_TRUNCATE = 500;
const MAX_INPUTS = 5;
export const MAX_LESSONS = 10;
export const MAX_LESSON_CHARS = 8_000;
const SPEC_SEARCH_POOL = 20;
const MAX_SPECS = 5;
const MAX_SESSIONS = 5;
const MAX_CODE_RESULTS = 3;
// ─── System prompt ──────────────────────────────────────
export { SPEC_SYSTEM_PROMPT } from './spec-templates.js';
export async function retrieveContext(query, store, linkedStores) {
    const { log } = await import('../ui.js');
    const { partitionLessons } = await import('../utils.js');
    const search = (s, typeFilter, maxResults) => s.search({ query, typeFilter, maxResults });
    // Fetch from primary store
    const [allSpecs, sessions, code] = await Promise.all([
        search(store, 'spec', SPEC_SEARCH_POOL),
        search(store, 'session_log', MAX_SESSIONS),
        search(store, 'code', MAX_CODE_RESULTS),
    ]);
    // Fetch specs from linked stores (cross-totem knowledge)
    if (linkedStores && linkedStores.length > 0) {
        const linkedResults = await Promise.all(linkedStores.map((ls) => search(ls, 'spec', MAX_SPECS).catch((err) => {
            // Network/connection failures → graceful degradation (return empty)
            // Config/parse errors → surface to user so they can fix their setup
            const msg = err instanceof Error ? err.message : String(err);
            if (msg.includes('ECONNREFUSED') ||
                msg.includes('ENOTFOUND') ||
                msg.includes('FetchError')) {
                return [];
            }
            log.warn(TAG, `Linked store query failed: ${msg}`);
            return [];
        })));
        allSpecs.push(...linkedResults.flat());
        // Re-sort by score after merging
        allSpecs.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
    }
    // Partition: lessons come from lessons.md, everything else is a spec/ADR
    const { lessons, specs } = partitionLessons(allSpecs, MAX_LESSONS, MAX_SPECS);
    return { specs, sessions, code, lessons };
}
function buildSearchQuery(issue) {
    const labels = issue.labels.join(' ');
    const bodySnippet = issue.body.slice(0, QUERY_BODY_TRUNCATE);
    return `${issue.title} ${labels} ${bodySnippet}`.trim();
}
const TEST_KEYWORD_RE = /\b(test(?:s|ing)?|verif(?:y|ies|ication)|example(?:s)?|fixture(?:s)?|hits|misses|rule-?tester)\b/i;
const TEST_EXPANSION = ' test testing infrastructure fixture verification testRule rule-tester';
/**
 * Expand a spec search query with test-infrastructure keywords when the
 * original query mentions testing concepts.  This helps the vector search
 * surface existing helpers like `rule-tester.ts`.
 */
export function expandSpecQuery(query) {
    return TEST_KEYWORD_RE.test(query) ? query + TEST_EXPANSION : query;
}
// ─── Prompt assembly ────────────────────────────────────
export async function assemblePrompt(inputs, context, systemPrompt) {
    const { formatLessonSection, formatResults, wrapXml } = await import('../utils.js');
    const sections = [systemPrompt];
    for (const { issue, freeText } of inputs) {
        if (issue) {
            const issueLabels = issue.labels.join(', ');
            sections.push(`\n=== ISSUE #${issue.number}: ${issue.title} ===`);
            sections.push(wrapXml('issue_title', issue.title));
            sections.push(`Labels: ${issueLabels || '(none)'}`);
            sections.push(`State: ${issue.state}`);
            if (issue.body) {
                sections.push('');
                sections.push(wrapXml('issue_body', issue.body));
            }
        }
        else if (freeText) {
            sections.push('\n=== TOPIC ===');
            sections.push(wrapXml('topic_text', freeText));
        }
    }
    // Totem knowledge
    const specSection = formatResults(context.specs, 'RELATED SPECS & ADRs');
    const sessionSection = formatResults(context.sessions, 'RELATED SESSION HISTORY');
    const codeSection = formatResults(context.code, 'RELATED CODE');
    if (specSection || sessionSection || codeSection) {
        sections.push('\n=== TOTEM KNOWLEDGE ===');
        if (specSection)
            sections.push(specSection);
        if (sessionSection)
            sections.push(sessionSection);
        if (codeSection)
            sections.push(codeSection);
    }
    // Lessons — full bodies, capped by total character budget
    const lessonSection = formatLessonSection(context.lessons, MAX_LESSON_CHARS);
    if (lessonSection)
        sections.push(lessonSection);
    // Prior art concierge (#1015): inject shared helper signatures
    const { formatSharedHelpers, getSharedHelpers } = await import('@mmnto/totem');
    const helperSection = formatSharedHelpers(getSharedHelpers());
    if (helperSection) {
        sections.push('\n' + helperSection);
    }
    return sections.join('\n');
}
/**
 * mmnto-ai/totem#1555: validate that --stdout and --out are not used together.
 * Run before any LLM call so a user-error surfaces in <50ms with no API cost.
 */
export function validateOutputOptions(options, TotemConfigErrorCtor) {
    if (options.stdout && options.out) {
        throw new TotemConfigErrorCtor('--stdout and --out cannot be used together.', 'Pick one: --out <path> writes to a specific file; --stdout writes to standard output.', 'CONFIG_INVALID');
    }
}
/**
 * Sanitize a free-form topic string for use as a filename stem. Replaces any
 * character outside `[a-zA-Z0-9_-]` with a single dash, collapses runs, and
 * trims leading/trailing dashes. Returns `''` for inputs that sanitize to
 * nothing — caller decides the fallback.
 */
export function sanitizeSpecFilename(input) {
    return input.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
}
/**
 * Derive the default spec output path under `<gitRoot>/.totem/specs/<stem>.md`.
 * Returns `null` for ambiguous cases (multi-input, empty topic) — the caller
 * falls back to stdout with a hint.
 */
export function resolveDefaultSpecPath(parsedInputs, cwd, deps) {
    if (parsedInputs.length !== 1)
        return null;
    const first = parsedInputs[0];
    let stem;
    if (first.issue) {
        stem = String(first.issue.number);
    }
    else if (first.freeText) {
        stem = sanitizeSpecFilename(first.freeText);
        if (!stem)
            return null;
    }
    else {
        return null;
    }
    const root = deps.resolveGitRoot(cwd) ?? cwd;
    return deps.pathJoin(root, '.totem', 'specs', `${stem}.md`);
}
// ─── Main command ───────────────────────────────────────
export async function specCommand(inputs, options) {
    const path = await import('node:path');
    const { createEmbedder, LanceStore: LanceStoreImpl, resolveGitRoot, sanitizeForTerminal, TotemConfigError, } = await import('@mmnto/totem');
    const { log } = await import('../ui.js');
    const { applyCodeBlindGuard, getSystemPrompt, loadConfig, loadEnv, requireEmbedding, resolveConfigPath, runOrchestrator, writeOutput, } = await import('../utils.js');
    validateOutputOptions(options, TotemConfigError);
    const unique = [...new Set(inputs)];
    if (unique.length > MAX_INPUTS) {
        throw new TotemConfigError(`Too many inputs (${unique.length}). Maximum is ${MAX_INPUTS}.`, `Pass at most ${MAX_INPUTS} inputs at a time.`, 'CONFIG_INVALID');
    }
    const cwd = process.cwd();
    const configPath = resolveConfigPath(cwd);
    loadEnv(cwd);
    const config = await loadConfig(configPath);
    // Connect to LanceDB
    const embedding = requireEmbedding(config);
    const embedder = createEmbedder(embedding);
    const store = new LanceStoreImpl(path.join(cwd, config.lanceDir), embedder, {
        absolutePathRoot: cwd,
    });
    await store.connect();
    // Connect to linked indexes (cross-totem knowledge)
    const linkedStores = [];
    if (config.linkedIndexes && config.linkedIndexes.length > 0) {
        for (const linkedPath of config.linkedIndexes) {
            try {
                const resolvedPath = path.resolve(cwd, linkedPath);
                const linkedConfigPath = resolveConfigPath(resolvedPath);
                const linkedConfig = await loadConfig(linkedConfigPath);
                const linkedEmbedding = linkedConfig.embedding;
                if (!linkedEmbedding)
                    continue; // Linked totem has no embedder — skip
                const linkedEmbedder = createEmbedder(linkedEmbedding);
                // Derive a link name for sourceContext — basename of the resolved
                // path with leading dot stripped, matching the MCP server's
                // `deriveLinkName` convention (mmnto/totem#1295).
                const linkName = path.basename(resolvedPath).replace(/^\./, '');
                const linkedStore = new LanceStoreImpl(path.join(resolvedPath, linkedConfig.lanceDir), linkedEmbedder, { sourceRepo: linkName, absolutePathRoot: resolvedPath });
                await linkedStore.connect();
                linkedStores.push(linkedStore);
                log.dim(TAG, `Linked index: ${linkedPath}`);
            }
            catch (err) {
                log.warn(TAG, `Could not connect to linked index at ${linkedPath} — skipping. ${err instanceof Error ? err.message : String(err)}`);
            }
        }
    }
    // Parse and fetch all inputs sequentially
    const { createIssueAdapter } = await import('../adapters/create-issue-adapter.js');
    const adapter = await createIssueAdapter(cwd, config);
    const parsed = [];
    const queryParts = [];
    for (const input of unique) {
        // Match GitHub, GitLab, or any URL ending in /issues/<number> or /-/issues/<number>
        const urlMatch = input.match(/^https?:\/\/[^/]+\/.*\/(?:-\/)?issues\/(\d+)/);
        // Support owner/repo#123 format for multi-repo disambiguation
        const hashIdx = input.indexOf('#');
        const isQualified = hashIdx > 0 && input.includes('/') && /^\d+$/.test(input.slice(hashIdx + 1));
        const qualifiedRepo = isQualified ? input.slice(0, hashIdx) : null;
        const qualifiedNum = isQualified ? parseInt(input.slice(hashIdx + 1), 10) : null;
        const issueNumber = /^\d+$/.test(input)
            ? parseInt(input, 10)
            : urlMatch
                ? parseInt(urlMatch[1], 10)
                : qualifiedNum;
        if (issueNumber) {
            // If qualified with owner/repo, create a repo-specific adapter
            let fetchAdapter = adapter;
            if (qualifiedRepo) {
                const { GitHubCliAdapter } = await import('../adapters/github-cli.js');
                fetchAdapter = new GitHubCliAdapter(cwd, qualifiedRepo);
            }
            log.info(TAG, `Fetching issue #${issueNumber}...`);
            const issue = fetchAdapter.fetchIssue(issueNumber);
            log.info(TAG, `Title: ${issue.title}`);
            parsed.push({ issue, freeText: null });
            queryParts.push(buildSearchQuery(issue));
        }
        else {
            log.info(TAG, `Topic: ${input}`);
            parsed.push({ issue: null, freeText: input });
            queryParts.push(input);
        }
    }
    // Retrieve context from LanceDB
    const query = expandSpecQuery(queryParts.join(' '));
    log.info(TAG, 'Querying Totem index...');
    const context = await retrieveContext(query, store, linkedStores.length > 0 ? linkedStores : undefined);
    const totalResults = context.specs.length + context.sessions.length + context.code.length + context.lessons.length;
    log.info(TAG, `Found: ${context.specs.length} specs, ${context.sessions.length} sessions, ${context.code.length} code, ${context.lessons.length} lessons`);
    // Resolve system prompt (allow .totem/prompts/spec.md override)
    const systemPrompt = getSystemPrompt('spec', SYSTEM_PROMPT, cwd, config.totemDir);
    // Code-blind grounding guard (mmnto-ai/totem#2106): 0 code retrieved → surface
    // an advisory banner + fold a suppression directive into the prompt; never
    // disables (strategy#474 interim ruling).
    const codeBlindGuard = applyCodeBlindGuard(context, systemPrompt);
    if (codeBlindGuard.banner)
        log.warn(TAG, codeBlindGuard.banner);
    // Assemble prompt
    const prompt = await assemblePrompt(parsed, context, codeBlindGuard.systemPrompt);
    log.dim(TAG, `Prompt: ${(prompt.length / 1024).toFixed(0)}KB`);
    // Grounded run artifact (mmnto-ai/totem#2100): always-on for spec — every
    // run is a future eval fixture. Per-item provenance bundle (mmnto-ai/totem#2101): every
    // retrieved item enters classed similarity-only; hash + summary are DERIVED
    // from the bundle, so the attested hash is recomputable from the artifact
    // surface alone.
    const { ADMISSION_COMPLETION_ONLY, calculateDeterministicHash, summarizeProvenance } = await import('@mmnto/totem');
    const { buildRetrievalGroundingBundle } = await import('../utils.js');
    const groundingBundle = buildRetrievalGroundingBundle(context);
    const content = await runOrchestrator({
        prompt,
        tag: TAG,
        options,
        config,
        cwd,
        // Anchor cache + artifacts at the config dir, not the invocation cwd —
        // without this, a `totem spec` from a subdirectory writes artifacts to
        // `<cwd>/.totem/` where the `totem artifact` verbs (which resolve from
        // the config path) can never find them (Greptile P1 on #2114).
        configRoot: path.dirname(configPath),
        totalResults,
        // Admission contract (mmnto-ai/totem#2102): the same value the slice-1
        // constant recorded, now caller-supplied — spec is factually completion-only.
        backendAdmissionClass: ADMISSION_COMPLETION_ONLY,
        runMetadata: { caller: 'spec', codeBlind: codeBlindGuard.codeBlind },
        artifact: {
            groundingHash: calculateDeterministicHash(groundingBundle),
            provenanceSummary: summarizeProvenance(groundingBundle),
            bundle: groundingBundle,
        },
    });
    if (content == null)
        return;
    if (options.stdout) {
        writeOutput(content);
        return;
    }
    if (options.out) {
        writeOutput(content, options.out);
        const safeOut = sanitizeForTerminal(options.out).replace(/[\n\t]+/g, ' ');
        log.success(TAG, `Written to ${safeOut}`);
        return;
    }
    const defaultPath = resolveDefaultSpecPath(parsed, cwd, {
        resolveGitRoot,
        pathJoin: path.join,
    });
    if (defaultPath) {
        writeOutput(content, defaultPath);
        const safeRelativePath = sanitizeForTerminal(path.relative(cwd, defaultPath)).replace(/[\n\t]+/g, ' ');
        log.success(TAG, `Spec saved to ${safeRelativePath}`);
    }
    else {
        log.dim(TAG, 'No default save path — writing to stdout. Use --out <path> to save.');
        writeOutput(content);
    }
}
//# sourceMappingURL=spec.js.map