/**
 * Research commands and manifest operations.
 * @task T4465
 * @epic T4454
 */
import { existsSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { ExitCode } from '@cleocode/contracts';
import { CleoError } from '../errors.js';
import { getBackupDir, getManifestPath as getCentralManifestPath, getCleoDirAbsolute, getManifestArchivePath, getProjectRoot, } from '../paths.js';
import { atomicWrite, safeReadFile } from '../store/atomic.js';
import { appendJsonl, readJson, saveJson } from '../store/json.js';
import { logOperation } from '../tasks/add.js';
/**
 * Get the research file path.
 * @task T4465
 */
function getResearchPath(cwd) {
    return join(getCleoDirAbsolute(cwd), 'research.json');
}
/**
 * Get the manifest file path.
 * @task T4465
 */
function getManifestPath(cwd) {
    return getCentralManifestPath(cwd);
}
/**
 * Read or initialize the research file.
 * @task T4465
 */
async function readResearch(cwd) {
    const path = getResearchPath(cwd);
    const data = await readJson(path);
    return data ?? { entries: [] };
}
/**
 * Add a research entry.
 * @task T4465
 */
export async function addResearch(options, cwd, accessor) {
    // Validate task exists
    const task = await accessor.loadSingleTask(options.taskId);
    if (!task) {
        throw new CleoError(ExitCode.NOT_FOUND, `Task not found: ${options.taskId}`);
    }
    if (!options.topic || options.topic.trim().length === 0) {
        throw new CleoError(ExitCode.INVALID_INPUT, 'Research topic is required');
    }
    const research = await readResearch(cwd);
    const now = new Date().toISOString();
    const entry = {
        id: `R${Date.now().toString(36)}`,
        taskId: options.taskId,
        topic: options.topic.trim(),
        findings: options.findings ?? [],
        sources: options.sources ?? [],
        status: (options.findings?.length ?? 0) > 0 ? 'complete' : 'pending',
        createdAt: now,
        updatedAt: now,
    };
    research.entries.push(entry);
    await saveJson(getResearchPath(cwd), research, { backupDir: getBackupDir(cwd) });
    await logOperation('research_added', entry.id, {
        taskId: options.taskId,
        topic: options.topic,
    }, accessor);
    return entry;
}
/**
 * Show a specific research entry.
 * @task T4465
 */
export async function showResearch(researchId, cwd) {
    const research = await readResearch(cwd);
    const entry = research.entries.find((e) => e.id === researchId);
    if (!entry) {
        throw new CleoError(ExitCode.NOT_FOUND, `Research entry not found: ${researchId}`);
    }
    return entry;
}
/**
 * List research entries with optional filtering.
 * @task T4465
 */
export async function listResearch(options = {}, cwd) {
    const research = await readResearch(cwd);
    let entries = research.entries;
    if (options.taskId) {
        entries = entries.filter((e) => e.taskId === options.taskId);
    }
    if (options.status) {
        entries = entries.filter((e) => e.status === options.status);
    }
    return entries;
}
/**
 * List pending research entries.
 * @task T4465
 */
export async function pendingResearch(cwd) {
    return listResearch({ status: 'pending' }, cwd);
}
/**
 * Link a research entry to a task.
 * @task T4465
 */
export async function linkResearch(researchId, taskId, cwd, accessor) {
    const research = await readResearch(cwd);
    const entry = research.entries.find((e) => e.id === researchId);
    if (!entry) {
        throw new CleoError(ExitCode.NOT_FOUND, `Research entry not found: ${researchId}`);
    }
    // Validate task exists
    const linkedTask = await accessor.loadSingleTask(taskId);
    if (!linkedTask) {
        throw new CleoError(ExitCode.NOT_FOUND, `Task not found: ${taskId}`);
    }
    entry.taskId = taskId;
    entry.updatedAt = new Date().toISOString();
    await saveJson(getResearchPath(cwd), research, { backupDir: getBackupDir(cwd) });
    return { researchId, taskId };
}
/**
 * Update research findings.
 * @task T4465
 */
export async function updateResearch(researchId, updates, cwd) {
    const research = await readResearch(cwd);
    const entry = research.entries.find((e) => e.id === researchId);
    if (!entry) {
        throw new CleoError(ExitCode.NOT_FOUND, `Research entry not found: ${researchId}`);
    }
    if (updates.findings)
        entry.findings = updates.findings;
    if (updates.sources)
        entry.sources = updates.sources;
    if (updates.status)
        entry.status = updates.status;
    entry.updatedAt = new Date().toISOString();
    await saveJson(getResearchPath(cwd), research, { backupDir: getBackupDir(cwd) });
    return entry;
}
/**
 * Get research statistics.
 * @task T4474
 */
export async function statsResearch(cwd) {
    const research = await readResearch(cwd);
    const byStatus = {};
    const byTopic = {};
    for (const entry of research.entries) {
        byStatus[entry.status] = (byStatus[entry.status] || 0) + 1;
        byTopic[entry.topic] = (byTopic[entry.topic] || 0) + 1;
    }
    return {
        total: research.entries.length,
        byStatus,
        byTopic,
    };
}
/**
 * Get research entries linked to a specific task.
 * @task T4474
 */
export async function linksResearch(taskId, cwd) {
    if (!taskId) {
        throw new CleoError(ExitCode.INVALID_INPUT, 'Task ID is required');
    }
    const research = await readResearch(cwd);
    return research.entries.filter((e) => e.taskId === taskId);
}
/**
 * Archive old research entries by status.
 * Moves 'complete' entries older than a threshold to an archive,
 * or returns summary of archivable entries.
 * @task T4474
 */
export async function archiveResearch(cwd) {
    const research = await readResearch(cwd);
    const completed = research.entries.filter((e) => e.status === 'complete');
    const remaining = research.entries.filter((e) => e.status !== 'complete');
    // Write back only non-complete entries
    await saveJson(getResearchPath(cwd), { entries: remaining }, { backupDir: getBackupDir(cwd) });
    return {
        action: 'archive',
        entriesArchived: completed.length,
        entriesRemaining: remaining.length,
    };
}
// === MANIFEST OPERATIONS ===
/**
 * Read manifest entries from MANIFEST.jsonl.
 * @task T4465
 */
export async function readManifest(cwd) {
    const manifestPath = getManifestPath(cwd);
    const content = await safeReadFile(manifestPath);
    if (!content)
        return [];
    const entries = [];
    for (const line of content.split('\n')) {
        const trimmed = line.trim();
        if (!trimmed)
            continue;
        try {
            entries.push(JSON.parse(trimmed));
        }
        catch {
            // Skip malformed lines
        }
    }
    return entries;
}
/**
 * Append a manifest entry.
 * @task T4465
 */
export async function appendManifest(entry, cwd) {
    const manifestPath = getManifestPath(cwd);
    await appendJsonl(manifestPath, entry);
}
/**
 * Query manifest entries.
 * @task T4465
 */
export async function queryManifest(options = {}, cwd) {
    let entries = await readManifest(cwd);
    if (options.status) {
        entries = entries.filter((e) => e.status === options.status);
    }
    if (options.agentType) {
        entries = entries.filter((e) => e.agent_type === options.agentType);
    }
    if (options.topic) {
        entries = entries.filter((e) => e.topics.includes(options.topic));
    }
    if (options.taskId) {
        entries = entries.filter((e) => e.linked_tasks.includes(options.taskId));
    }
    if (options.limit && options.limit > 0) {
        entries = entries.slice(0, options.limit);
    }
    return entries;
}
/**
 * Read all manifest entries as extended entries.
 * @task T4787
 */
export async function readExtendedManifest(cwd) {
    const manifestPath = getManifestPath(cwd);
    const content = await safeReadFile(manifestPath);
    if (!content)
        return [];
    const entries = [];
    for (const line of content.split('\n')) {
        const trimmed = line.trim();
        if (!trimmed)
            continue;
        try {
            entries.push(JSON.parse(trimmed));
        }
        catch {
            // Skip malformed lines
        }
    }
    return entries;
}
/**
 * Filter manifest entries by criteria.
 * @task T4787
 */
export function filterManifestEntries(entries, filter) {
    let filtered = entries;
    if (filter.taskId) {
        const taskId = filter.taskId;
        filtered = filtered.filter((e) => e.id.startsWith(taskId) || e.linked_tasks?.includes(taskId));
    }
    if (filter.status) {
        filtered = filtered.filter((e) => e.status === filter.status);
    }
    if (filter.agent_type) {
        filtered = filtered.filter((e) => e.agent_type === filter.agent_type);
    }
    if (filter.topic) {
        filtered = filtered.filter((e) => e.topics.includes(filter.topic));
    }
    if (filter.actionable !== undefined) {
        filtered = filtered.filter((e) => e.actionable === filter.actionable);
    }
    if (filter.dateAfter) {
        filtered = filtered.filter((e) => e.date > filter.dateAfter);
    }
    if (filter.dateBefore) {
        filtered = filtered.filter((e) => e.date < filter.dateBefore);
    }
    if (filter.offset && filter.offset > 0) {
        filtered = filtered.slice(filter.offset);
    }
    if (filter.limit && filter.limit > 0) {
        filtered = filtered.slice(0, filter.limit);
    }
    return filtered;
}
/**
 * Show a manifest entry by ID with optional file content.
 * @task T4787
 */
export async function showManifestEntry(researchId, cwd) {
    const entries = await readExtendedManifest(cwd);
    const entry = entries.find((e) => e.id === researchId);
    if (!entry) {
        throw new CleoError(ExitCode.NOT_FOUND, `Research entry '${researchId}' not found`);
    }
    const root = getProjectRoot(cwd);
    let fileContent = null;
    try {
        const filePath = resolve(root, entry.file);
        if (existsSync(filePath)) {
            fileContent = readFileSync(filePath, 'utf-8');
        }
    }
    catch {
        // File may not exist or be unreadable
    }
    return {
        ...entry,
        fileContent,
        fileExists: fileContent !== null,
    };
}
/**
 * Search manifest entries by text with relevance scoring.
 * @task T4787
 */
export async function searchManifest(query, options, cwd) {
    const entries = await readExtendedManifest(cwd);
    const queryLower = query.toLowerCase();
    const scored = entries.map((entry) => {
        let score = 0;
        if (entry.title.toLowerCase().includes(queryLower)) {
            score += 0.5;
        }
        if (entry.topics.some((t) => t.toLowerCase().includes(queryLower))) {
            score += 0.3;
        }
        if (entry.key_findings?.some((f) => f.toLowerCase().includes(queryLower))) {
            score += 0.2;
        }
        if (entry.id.toLowerCase().includes(queryLower)) {
            score += 0.1;
        }
        return { entry, score };
    });
    const minConfidence = options?.confidence ?? 0.1;
    let results = scored.filter((s) => s.score >= minConfidence).sort((a, b) => b.score - a.score);
    if (options?.limit && options.limit > 0) {
        results = results.slice(0, options.limit);
    }
    return results.map((r) => ({
        ...r.entry,
        relevanceScore: Math.round(r.score * 100) / 100,
    }));
}
/**
 * Get pending manifest entries (partial, blocked, or needing followup).
 * @task T4787
 */
export async function pendingManifestEntries(epicId, cwd) {
    const entries = await readExtendedManifest(cwd);
    let pending = entries.filter((e) => e.status === 'partial' ||
        e.status === 'blocked' ||
        (e.needs_followup && e.needs_followup.length > 0));
    if (epicId) {
        pending = pending.filter((e) => e.id.startsWith(epicId) || e.linked_tasks?.includes(epicId));
    }
    return {
        entries: pending,
        total: pending.length,
        byStatus: {
            partial: pending.filter((e) => e.status === 'partial').length,
            blocked: pending.filter((e) => e.status === 'blocked').length,
            needsFollowup: pending.filter((e) => e.needs_followup && e.needs_followup.length > 0).length,
        },
    };
}
/**
 * Get manifest-based research statistics.
 * @task T4787
 */
export async function manifestStats(epicId, cwd) {
    const entries = await readExtendedManifest(cwd);
    let filtered = entries;
    if (epicId) {
        filtered = entries.filter((e) => e.id.startsWith(epicId) || e.linked_tasks?.includes(epicId));
    }
    const byStatus = {};
    const byType = {};
    let actionable = 0;
    let needsFollowup = 0;
    let totalFindings = 0;
    for (const entry of filtered) {
        byStatus[entry.status] = (byStatus[entry.status] || 0) + 1;
        byType[entry.agent_type] = (byType[entry.agent_type] || 0) + 1;
        if (entry.actionable)
            actionable++;
        if (entry.needs_followup && entry.needs_followup.length > 0)
            needsFollowup++;
        if (entry.key_findings)
            totalFindings += entry.key_findings.length;
    }
    return {
        total: filtered.length,
        byStatus,
        byType,
        actionable,
        needsFollowup,
        averageFindings: filtered.length > 0 ? Math.round((totalFindings / filtered.length) * 10) / 10 : 0,
    };
}
/**
 * Link a manifest entry to a task (adds taskId to linked_tasks array).
 * @task T4787
 */
export async function linkManifestEntry(taskId, researchId, cwd) {
    const manifestPath = getManifestPath(cwd);
    const entries = await readExtendedManifest(cwd);
    const entryIndex = entries.findIndex((e) => e.id === researchId);
    if (entryIndex === -1) {
        throw new CleoError(ExitCode.NOT_FOUND, `Research entry '${researchId}' not found`);
    }
    const entry = entries[entryIndex];
    if (entry.linked_tasks?.includes(taskId)) {
        return { taskId, researchId, alreadyLinked: true };
    }
    if (!entry.linked_tasks) {
        entry.linked_tasks = [];
    }
    entry.linked_tasks.push(taskId);
    const content = entries.map((e) => JSON.stringify(e)).join('\n') + '\n';
    await atomicWrite(manifestPath, content);
    return { taskId, researchId, alreadyLinked: false };
}
/**
 * Append an extended manifest entry.
 * Validates required fields before appending.
 * @task T4787
 */
export async function appendExtendedManifest(entry, cwd) {
    const errors = [];
    if (!entry.id)
        errors.push('id is required');
    if (!entry.file)
        errors.push('file is required');
    if (!entry.title)
        errors.push('title is required');
    if (!entry.date)
        errors.push('date is required');
    if (!entry.status)
        errors.push('status is required');
    if (!entry.agent_type)
        errors.push('agent_type is required');
    if (!entry.topics)
        errors.push('topics is required');
    if (entry.actionable === undefined)
        errors.push('actionable is required');
    if (errors.length > 0) {
        throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid manifest entry: ${errors.join(', ')}`);
    }
    const manifestPath = getManifestPath(cwd);
    await appendJsonl(manifestPath, entry);
    return { entryId: entry.id, file: getManifestPath() };
}
/**
 * Archive manifest entries older than a date.
 * @task T4787
 */
export async function archiveManifestEntries(beforeDate, cwd) {
    const manifestPath = getManifestPath(cwd);
    const archivePath = getManifestArchivePath(cwd);
    const entries = await readExtendedManifest(cwd);
    const toArchive = entries.filter((e) => e.date < beforeDate);
    const toKeep = entries.filter((e) => e.date >= beforeDate);
    if (toArchive.length === 0) {
        return {
            archived: 0,
            remaining: entries.length,
            archiveFile: getManifestArchivePath(),
        };
    }
    // Append archived entries to archive file
    const existingArchive = await safeReadFile(archivePath);
    const archiveContent = toArchive.map((e) => JSON.stringify(e)).join('\n') + '\n';
    const fullArchive = existingArchive
        ? existingArchive.trimEnd() + '\n' + archiveContent
        : archiveContent;
    await atomicWrite(archivePath, fullArchive);
    // Rewrite main manifest with remaining entries
    const remainingContent = toKeep.length > 0 ? toKeep.map((e) => JSON.stringify(e)).join('\n') + '\n' : '';
    await atomicWrite(manifestPath, remainingContent);
    return {
        archived: toArchive.length,
        remaining: toKeep.length,
        archiveFile: getManifestArchivePath(),
    };
}
/**
 * Find manifest entries with overlapping topics but conflicting key_findings.
 * @task T4787
 */
export async function findContradictions(cwd, params) {
    const entries = await readExtendedManifest(cwd);
    const byTopic = new Map();
    for (const entry of entries) {
        if (!entry.key_findings || entry.key_findings.length === 0)
            continue;
        for (const topic of entry.topics) {
            if (params?.topic && topic !== params.topic)
                continue;
            if (!byTopic.has(topic)) {
                byTopic.set(topic, []);
            }
            byTopic.get(topic).push(entry);
        }
    }
    const contradictions = [];
    const negationPairs = [
        [/\bdoes NOT\b/i, /\bdoes\b(?!.*\bnot\b)/i],
        [/\bcannot\b/i, /\bcan\b(?!.*\bnot\b)/i],
        [/\bno\s+\w+\s+required\b/i, /\brequired\b(?!.*\bno\b)/i],
        [
            /\bnot\s+(?:available|supported|possible|recommended)\b/i,
            /\b(?:available|supported|possible|recommended)\b(?!.*\bnot\b)/i,
        ],
        [/\bwithout\b/i, /\brequires?\b/i],
        [/\bavoid\b/i, /\buse\b/i],
        [/\bdeprecated\b/i, /\brecommended\b/i],
        [/\banti-pattern\b/i, /\bbest practice\b/i],
    ];
    for (const [topic, topicEntries] of byTopic) {
        if (topicEntries.length < 2)
            continue;
        for (let i = 0; i < topicEntries.length; i++) {
            for (let j = i + 1; j < topicEntries.length; j++) {
                const a = topicEntries[i];
                const b = topicEntries[j];
                const conflicts = [];
                for (const findingA of a.key_findings) {
                    for (const findingB of b.key_findings) {
                        for (const [patternNeg, patternPos] of negationPairs) {
                            if ((patternNeg.test(findingA) && patternPos.test(findingB)) ||
                                (patternPos.test(findingA) && patternNeg.test(findingB))) {
                                conflicts.push(`"${findingA}" vs "${findingB}"`);
                                break;
                            }
                        }
                    }
                }
                if (conflicts.length > 0) {
                    contradictions.push({
                        entryA: a,
                        entryB: b,
                        topic,
                        conflictDetails: conflicts.join('; '),
                    });
                }
            }
        }
    }
    return contradictions;
}
/**
 * Identify research entries replaced by newer work on same topic.
 * @task T4787
 */
export async function findSuperseded(cwd, params) {
    const entries = await readExtendedManifest(cwd);
    const byTopicAndType = new Map();
    for (const entry of entries) {
        for (const topic of entry.topics) {
            if (params?.topic && topic !== params.topic)
                continue;
            const key = `${topic}::${entry.agent_type}`;
            if (!byTopicAndType.has(key)) {
                byTopicAndType.set(key, []);
            }
            byTopicAndType.get(key).push(entry);
        }
    }
    const superseded = [];
    const seenPairs = new Set();
    for (const [key, groupEntries] of byTopicAndType) {
        if (groupEntries.length < 2)
            continue;
        const topic = key.split('::')[0];
        const sorted = [...groupEntries].sort((a, b) => a.date.localeCompare(b.date));
        for (let i = 0; i < sorted.length - 1; i++) {
            const pairKey = `${sorted[i].id}::${sorted[sorted.length - 1].id}::${topic}`;
            if (seenPairs.has(pairKey))
                continue;
            seenPairs.add(pairKey);
            superseded.push({
                old: sorted[i],
                replacement: sorted[sorted.length - 1],
                topic,
            });
        }
    }
    return superseded;
}
/**
 * Read protocol injection content for a given protocol type.
 * @task T4787
 */
export async function readProtocolInjection(protocolType, params, cwd) {
    const root = getProjectRoot(cwd);
    const protocolLocations = [
        resolve(root, 'protocols', `${protocolType}.md`),
        resolve(root, 'skills', '_shared', `${protocolType}.md`),
        resolve(root, 'agents', 'cleo-subagent', 'protocols', `${protocolType}.md`),
    ];
    let protocolContent = null;
    let protocolPath = null;
    for (const loc of protocolLocations) {
        if (existsSync(loc)) {
            try {
                protocolContent = readFileSync(loc, 'utf-8');
                protocolPath = loc.replace(root + '/', '');
                break;
            }
            catch { }
        }
    }
    if (!protocolContent || !protocolPath) {
        throw new CleoError(ExitCode.NOT_FOUND, `Protocol '${protocolType}' not found in src/protocols/, skills/_shared/, or agents/cleo-subagent/protocols/`);
    }
    return {
        protocolType,
        content: protocolContent,
        path: protocolPath,
        contentLength: protocolContent.length,
        estimatedTokens: Math.ceil(protocolContent.length / 4),
        taskId: params?.taskId || null,
        variant: params?.variant || null,
    };
}
/**
 * Compact MANIFEST.jsonl by removing duplicate/stale entries.
 * @task T4787
 */
export async function compactManifest(cwd) {
    const manifestPath = getManifestPath(cwd);
    const content = await safeReadFile(manifestPath);
    if (!content) {
        return {
            compacted: false,
            originalLines: 0,
            malformedRemoved: 0,
            duplicatesRemoved: 0,
            remainingEntries: 0,
        };
    }
    const lines = content.split('\n');
    const entries = [];
    let malformedCount = 0;
    for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed)
            continue;
        try {
            entries.push(JSON.parse(trimmed));
        }
        catch {
            malformedCount++;
        }
    }
    const originalCount = entries.length + malformedCount;
    const idMap = new Map();
    for (const entry of entries) {
        idMap.set(entry.id, entry);
    }
    const compacted = Array.from(idMap.values());
    const duplicatesRemoved = entries.length - compacted.length;
    const compactedContent = compacted.length > 0 ? compacted.map((e) => JSON.stringify(e)).join('\n') + '\n' : '';
    await atomicWrite(manifestPath, compactedContent);
    return {
        compacted: true,
        originalLines: originalCount,
        malformedRemoved: malformedCount,
        duplicatesRemoved,
        remainingEntries: compacted.length,
    };
}
/**
 * Validate research entries for a task.
 * @task T4787
 */
export async function validateManifestEntries(taskId, cwd) {
    const root = getProjectRoot(cwd);
    const entries = await readExtendedManifest(cwd);
    const linked = entries.filter((e) => e.id.startsWith(taskId) || e.linked_tasks?.includes(taskId));
    if (linked.length === 0) {
        return {
            taskId,
            valid: true,
            entriesFound: 0,
            issues: [],
            errorCount: 0,
            warningCount: 0,
        };
    }
    const issues = [];
    for (const entry of linked) {
        if (!entry.id)
            issues.push({ entryId: entry.id || '(unknown)', issue: 'Missing id', severity: 'error' });
        if (!entry.file)
            issues.push({ entryId: entry.id, issue: 'Missing file path', severity: 'error' });
        if (!entry.title)
            issues.push({ entryId: entry.id, issue: 'Missing title', severity: 'error' });
        if (!entry.date)
            issues.push({ entryId: entry.id, issue: 'Missing date', severity: 'error' });
        if (!entry.status)
            issues.push({ entryId: entry.id, issue: 'Missing status', severity: 'error' });
        if (!entry.agent_type)
            issues.push({ entryId: entry.id, issue: 'Missing agent_type', severity: 'error' });
        if (entry.status && !['completed', 'partial', 'blocked'].includes(entry.status)) {
            issues.push({
                entryId: entry.id,
                issue: `Invalid status: ${entry.status}`,
                severity: 'error',
            });
        }
        if (entry.file) {
            const filePath = resolve(root, entry.file);
            if (!existsSync(filePath)) {
                issues.push({
                    entryId: entry.id,
                    issue: `Output file not found: ${entry.file}`,
                    severity: 'warning',
                });
            }
        }
        if (entry.agent_type === 'research' &&
            (!entry.key_findings || entry.key_findings.length === 0)) {
            issues.push({
                entryId: entry.id,
                issue: 'Research entry missing key_findings',
                severity: 'warning',
            });
        }
    }
    return {
        taskId,
        valid: issues.filter((i) => i.severity === 'error').length === 0,
        entriesFound: linked.length,
        issues,
        errorCount: issues.filter((i) => i.severity === 'error').length,
        warningCount: issues.filter((i) => i.severity === 'warning').length,
    };
}
// === BRAIN Lifecycle (temporal decay) ===
export * from './brain-lifecycle.js';
export * from './brain-links.js';
export * from './brain-migration.js';
// === BRAIN Retrieval functions (3-layer pattern) ===
export * from './brain-retrieval.js';
export * from './brain-search.js';
// === BRAIN Memory modules (brain.db backed) ===
export * from './decisions.js';
export * from './learnings.js';
// === JSONL Memory modules (legacy, still active) ===
export * from './patterns.js';
//# sourceMappingURL=index.js.map