"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CliAnalyzeService = void 0;
exports.envelopesFromStreamingUpdate = envelopesFromStreamingUpdate;
exports.shouldExposeLiveStreamingUpdate = shouldExposeLiveStreamingUpdate;
/**
 * CLI Analyze Facade.
 *
 * Wraps agentv3's service layer into a single `runTurn()` call with no
 * Express dependency. This is the CLI's only touch-point with the agentv3
 * internals — everything else (commands, REPL, IO) depends on this facade.
 *
 * Compared to HTTP route's `runAgentDrivenAnalysis()`, this omits:
 *   - SSE broadcasting (no HTTP response)
 *   - conversation_step derivation (frontend-only concern)
 *   - scene reconstruction payload (deferred to PR-future)
 *   - LLM telemetry logging subscription (best-effort, not critical for CLI)
 *
 * It keeps:
 *   - prepareSession / analyze / conclusion capture
 *   - HTML report generation (written to CLI's session folder, not /api/reports)
 *   - sdkSessionId surfacing for subsequent resume
 */
const fs = __importStar(require("fs"));
const assistantApplicationService_1 = require("../../assistant/application/assistantApplicationService");
const agentAnalyzeSessionService_1 = require("../../assistant/application/agentAnalyzeSessionService");
const traceProcessorService_1 = require("../../services/traceProcessorService");
const sessionLogger_1 = require("../../services/sessionLogger");
const sessionPersistenceService_1 = require("../../services/sessionPersistenceService");
const htmlReportGenerator_1 = require("../../services/htmlReportGenerator");
const agentReportData_1 = require("../../services/agentReportData");
const agentResultNormalizer_1 = require("../../services/agentResultNormalizer");
const persistAgentSession_1 = require("../../services/persistAgentSession");
const finalResultQualityGate_1 = require("../../services/finalResultQualityGate");
const claimVerificationRunner_1 = require("../../services/verifier/claimVerificationRunner");
const enhancedSessionContext_1 = require("../../agent/context/enhancedSessionContext");
const runtimePaths_1 = require("../../runtimePaths");
const ragStore_1 = require("../../services/ragStore");
const symbolResolver_1 = require("../../services/symbol/symbolResolver");
const workingTraceProcessor_1 = require("../../services/workingTraceProcessor");
const traceProcessorInstaller_1 = require("./traceProcessorInstaller");
const runtimeSelection_1 = require("../../agentRuntime/runtimeSelection");
const dataContract_1 = require("../../types/dataContract");
function envelopesFromStreamingUpdate(update) {
    if (update.type !== 'data')
        return [];
    const raw = Array.isArray(update.content) ? update.content : [update.content];
    return raw.filter((item) => Boolean(item && typeof item === 'object' && (0, dataContract_1.validateDataEnvelope)(item).length === 0));
}
function shouldExposeLiveStreamingUpdate(update) {
    return update.type !== 'conclusion' && update.type !== 'answer_token';
}
/**
 * Singleton per CLI process.
 * - Own `AssistantApplicationService` — no HTTP routes touch it, so the 30-min
 *   idle cleanup (only scheduled from agentRoutes.ts) never runs.
 * - `SessionPersistenceService` writes to `backend/data/sessions/sessions.db`
 *   (the same DB the HTTP server uses — intentional, so REPL sessions are
 *   visible to the web UI and vice versa).
 */
class CliAnalyzeService {
    constructor() {
        // Independent AssistantApplicationService instance — intentionally separate
        // from the HTTP route's instance. The 30-min idle cleanup timer is registered
        // *only* from agentRoutes.ts at server startup, not from this constructor,
        // so a CLI-owned AppService is never subject to abandonment cleanup. This
        // matters because CLI sessions have no SSE clients (AppService's signal for
        // "abandoned"), so a shared instance would prematurely cull them.
        // ⚠ If a future change moves the cleanup timer into AssistantApplicationService's
        // constructor, this design breaks silently — pass `enableIdleCleanup: false` then.
        this.appService = new assistantApplicationService_1.AssistantApplicationService();
        this.persistence = sessionPersistenceService_1.SessionPersistenceService.getInstance();
        this.analyzeService = new agentAnalyzeSessionService_1.AgentAnalyzeSessionService({
            assistantAppService: this.appService,
            createSessionLogger: sessionLogger_1.createSessionLogger,
            sessionPersistenceService: this.persistence,
            // sessionContextManager omitted — AgentAnalyzeSessionService defaults to
            // the module-level singleton internally.
            // Only invoked on resume; PR1 covers fresh analyze only. Returning null
            // lets prepareSession fall through to a new session rather than throw.
            buildRecoveredResultFromContext: () => null,
        });
    }
    async loadTrace(tracePath) {
        await this.ensureTraceProcessorAvailable();
        return (0, traceProcessorService_1.getTraceProcessorService)().loadTraceFromFilePath(tracePath);
    }
    /**
     * Resume-only path: try to reload an existing trace by its original id,
     * preserving identity so the persisted session's `traceId` still matches.
     * Returns true on success, false if the trace file has been evicted from
     * `uploads/traces/` (caller should then degrade to a fresh load).
     */
    async reloadTraceById(traceId) {
        await this.ensureTraceProcessorAvailable();
        const info = await (0, traceProcessorService_1.getTraceProcessorService)().getOrLoadTrace(traceId);
        return info !== undefined;
    }
    async queryTrace(traceId, sql) {
        await this.ensureTraceProcessorAvailable();
        return (0, traceProcessorService_1.getTraceProcessorService)().query(traceId, sql);
    }
    async runTurn(input) {
        // Resolve traceId: either passed in (we assume caller already loaded), or load now.
        let traceId = input.traceId;
        if (!traceId) {
            if (!input.tracePath) {
                throw new Error('runTurn requires either tracePath or traceId');
            }
            traceId = await this.loadTrace(input.tracePath);
        }
        if (isCliE2eFakeMode()) {
            return runCliE2eFakeTurn(input, traceId);
        }
        const { sessionId, session } = this.analyzeService.prepareSession({
            traceId,
            query: input.query,
            requestedSessionId: input.sessionId,
            referenceTraceId: input.referenceTraceId,
        });
        const effectiveReferenceTraceId = input.referenceTraceId ?? session.referenceTraceId;
        // Bump runSequence for this turn. HTTP route gets the incremented value
        // from an externally-constructed runContext; CLI increments inline so the
        // turn index used by appendMessages (msg-<session>-turn<N>-role) is unique
        // across turns rather than colliding with prior turns of the same session.
        session.runSequence = (session.runSequence || 0) + 1;
        // Surface sessionId to the caller now, before analyze() starts emitting
        // events. Without this, callers must buffer events until runTurn resolves,
        // which accumulates the entire analyze run's output in memory.
        input.onSessionReady?.(sessionId);
        const orchestrator = session.orchestrator;
        // Subscribe to live updates. Wrap in off()-on-finally to avoid handler leaks
        // if runTurn is called multiple times within one CLI process (REPL path).
        const handler = (update) => {
            const envelopes = envelopesFromStreamingUpdate(update);
            if (envelopes.length > 0) {
                session.dataEnvelopes.push(...envelopes);
            }
            if (!shouldExposeLiveStreamingUpdate(update))
                return;
            try {
                input.onEvent(update);
            }
            catch (err) {
                // Don't let a renderer bug kill the analysis — log and continue.
                console.error('[CliAnalyzeService] onEvent handler threw:', err.message);
            }
        };
        orchestrator.on('update', handler);
        let result;
        try {
            result = await orchestrator.analyze(input.query, sessionId, traceId, {
                providerId: session.providerId,
                referenceTraceId: effectiveReferenceTraceId,
                analysisMode: input.analysisMode,
                codeAwareMode: input.codeAwareMode,
                codebaseIds: input.codebaseIds,
            });
        }
        finally {
            orchestrator.off('update', handler);
        }
        session.codeAwareMode = input.codeAwareMode;
        session.codebaseIds = input.codebaseIds;
        const normalized = (0, agentResultNormalizer_1.normalizeResultForReport)(result, {
            dataEnvelopes: session.dataEnvelopes,
        });
        result.conclusion = normalized.conclusion;
        if (normalized.conclusionContract) {
            result.conclusionContract = normalized.conclusionContract;
        }
        const qualityArtifacts = (0, claimVerificationRunner_1.runClaimVerification)({
            conclusionContract: normalized.conclusionContract,
            dataEnvelopes: session.dataEnvelopes,
            comparisonReportSection: session.comparisonReportSection,
            policy: 'record_only',
        });
        result.claimSupport = qualityArtifacts.claimSupport;
        result.claimVerificationResult = qualityArtifacts.claimVerificationResult;
        result.identityResolutions = qualityArtifacts.identityResolutions;
        session.claimSupport = qualityArtifacts.claimSupport;
        session.claimVerificationResult = qualityArtifacts.claimVerificationResult;
        session.identityResolutions = qualityArtifacts.identityResolutions;
        const finalQualityIssue = (0, finalResultQualityGate_1.applyFinalResultQualityGate)({ result, query: input.query });
        if (finalQualityIssue) {
            try {
                input.onEvent({
                    type: 'degraded',
                    content: {
                        module: 'cliAnalyzeService',
                        fallback: 'final_result_quality_gate',
                        code: finalQualityIssue.code,
                        partial: true,
                        message: result.terminationMessage || finalQualityIssue.message,
                    },
                    timestamp: Date.now(),
                });
            }
            catch (err) {
                console.error('[CliAnalyzeService] onEvent handler threw:', err.message);
            }
        }
        session.result = result;
        enhancedSessionContext_1.sessionContextManager.get(sessionId, traceId)?.annotateLatestCompletedTurn({
            success: result.success,
            findings: result.findings,
            message: result.conclusion,
            confidence: result.confidence,
            partial: result.partial,
            terminationReason: result.terminationReason,
            terminationMessage: result.terminationMessage,
            conclusionContract: normalized.conclusionContract,
            claimSupport: qualityArtifacts.claimSupport,
            claimVerificationResult: qualityArtifacts.claimVerificationResult,
            identityResolutions: qualityArtifacts.identityResolutions,
        });
        // Persist to SQLite BEFORE building the report — the snapshot is stashed on
        // the session as `_lastSnapshot` and read by the HTML generator. Routes
        // through the same shared helper the HTTP layer uses, so any future schema
        // change applies to both paths automatically.
        (0, persistAgentSession_1.persistAgentTurn)({
            session,
            sessionId,
            traceId,
            query: input.query,
            result: { conclusion: result.conclusion, totalDurationMs: result.totalDurationMs },
        });
        const persistedSnapshot = session._lastSnapshot;
        const runtimeSelection = persistedSnapshot?.agentRuntimeKind
            ? null
            : (0, runtimeSelection_1.resolveAgentRuntimeSelection)(session.providerId ?? null);
        // sdkSessionId is only populated on ClaudeRuntime (agentv3) — guarded call.
        const sdkSessionId = typeof orchestrator.getSdkSessionId === 'function'
            ? orchestrator.getSdkSessionId(sessionId, effectiveReferenceTraceId)
            : undefined;
        const reportOutput = this.buildReportHtml(session, result);
        return {
            sessionId,
            traceId,
            sdkSessionId,
            result,
            reportHtml: reportOutput.html,
            reportError: reportOutput.error,
            // The Claude model name is stored on ClaudeRuntime's config; not trivially
            // exposed via IOrchestrator. Left undefined for PR1; fills in PR2 via
            // CLAUDE_MODEL env read if needed for config.json provenance.
            model: process.env.CLAUDE_MODEL,
            providerId: persistedSnapshot?.agentRuntimeProviderId ?? session.providerId ?? null,
            agentRuntimeKind: persistedSnapshot?.agentRuntimeKind ?? runtimeSelection?.kind,
            providerSnapshotHash: persistedSnapshot?.agentRuntimeProviderSnapshotHash ?? session.providerSnapshotHash ?? null,
        };
    }
    /**
     * Build the HTML report for a completed turn. Routes through the shared
     * `normalizeResultForReport` + `buildAgentDrivenReportData` pipeline the
     * HTTP path uses, so CLI and web UI emit identical reports for the same
     * session (same sanitized conclusion text, same derived conclusionContract).
     */
    buildReportHtml(session, result) {
        try {
            const normalized = (0, agentResultNormalizer_1.normalizeResultForReport)(result, {
                dataEnvelopes: session.dataEnvelopes,
            });
            const reportData = (0, agentReportData_1.buildAgentDrivenReportData)({
                session,
                result: {
                    sessionId: session.sessionId,
                    success: normalized.success,
                    findings: normalized.findings,
                    hypotheses: normalized.hypotheses,
                    conclusion: normalized.conclusion,
                    conclusionContract: normalized.conclusionContract,
                    claimSupport: normalized.claimSupport ?? result.claimSupport,
                    claimVerificationResult: normalized.claimVerificationResult ?? result.claimVerificationResult,
                    identityResolutions: normalized.identityResolutions ?? result.identityResolutions,
                    confidence: normalized.confidence,
                    rounds: normalized.rounds,
                    totalDurationMs: normalized.totalDurationMs,
                    partial: normalized.partial,
                    terminationReason: normalized.terminationReason,
                    terminationMessage: normalized.terminationMessage,
                },
            });
            const html = (0, htmlReportGenerator_1.getHTMLReportGenerator)().generateAgentDrivenHTML(reportData);
            return { html };
        }
        catch (err) {
            return { error: err.message };
        }
    }
    async ensureTraceProcessorAvailable() {
        const traceProcessorPath = (0, workingTraceProcessor_1.getTraceProcessorPath)();
        if (CliAnalyzeService.checkedTraceProcessorPath === traceProcessorPath)
            return;
        if (!fs.existsSync(traceProcessorPath)) {
            if (process.env.TRACE_PROCESSOR_PATH) {
                throw new Error([
                    `trace_processor_shell binary not found at TRACE_PROCESSOR_PATH: ${traceProcessorPath}`,
                    '',
                    'Fix TRACE_PROCESSOR_PATH or unset it to let SmartPerfetto download the pinned binary automatically.',
                ].join('\n'));
            }
            await this.installTraceProcessor(traceProcessorPath);
        }
        try {
            fs.accessSync(traceProcessorPath, fs.constants.X_OK);
        }
        catch {
            throw new Error([
                `trace_processor_shell is not executable: ${traceProcessorPath}`,
                '',
                `Run \`chmod +x ${traceProcessorPath}\`, or set TRACE_PROCESSOR_PATH to an executable binary.`,
            ].join('\n'));
        }
        CliAnalyzeService.checkedTraceProcessorPath = traceProcessorPath;
    }
    async installTraceProcessor(traceProcessorPath) {
        if (!CliAnalyzeService.traceProcessorInstallPromise) {
            console.error(`trace_processor_shell not found. Downloading pinned Perfetto binary to ${traceProcessorPath}...`);
            CliAnalyzeService.traceProcessorInstallPromise = (0, traceProcessorInstaller_1.installTraceProcessorPrebuilt)(traceProcessorPath)
                .finally(() => {
                CliAnalyzeService.traceProcessorInstallPromise = null;
            });
        }
        await CliAnalyzeService.traceProcessorInstallPromise;
    }
    /**
     * Best-effort teardown. Called by CLI on process exit to stop the
     * trace_processor_shell subprocess — otherwise Node waits on it.
     */
    async shutdown() {
        try {
            await (0, traceProcessorService_1.getTraceProcessorService)().cleanup();
        }
        catch {
            /* ignore — already cleaned or never started */
        }
    }
}
exports.CliAnalyzeService = CliAnalyzeService;
CliAnalyzeService.checkedTraceProcessorPath = null;
CliAnalyzeService.traceProcessorInstallPromise = null;
function isCliE2eFakeMode() {
    return process.env.NODE_ENV === 'test' && process.env.SMARTPERFETTO_CLI_E2E_FAKE === '1';
}
async function runCliE2eFakeTurn(input, traceId) {
    const sessionId = input.sessionId || `agent-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
    input.onSessionReady?.(sessionId);
    const startedAt = Date.now();
    const timestamp = Date.now();
    const codeAware = buildCliE2eFakeCodeAwareContext(input);
    const baseConclusion = process.env.SMARTPERFETTO_CLI_E2E_FAKE_RESPONSE?.trim() || [
        'CLI E2E fake analysis completed.',
        `Question: ${input.query}`,
        `Trace: ${traceId}`,
        ...(input.referenceTraceId ? [`Reference trace: ${input.referenceTraceId}`] : []),
    ].join('\n');
    const fakeConclusion = codeAware.codeReferences.length > 0
        ? [
            baseConclusion,
            '',
            'Code-aware source references:',
            ...codeAware.codeReferences.map(ref => {
                const lineRange = ref.lineRange ? `:${ref.lineRange.start}-${ref.lineRange.end}` : '';
                const symbol = ref.symbol ? `${ref.symbol} ` : '';
                return `- CodeRef ${symbol}${ref.filePath}${lineRange} (chunkId=${ref.chunkId}, codebaseId=${ref.codebaseId})`;
            }),
        ].join('\n')
        : baseConclusion;
    input.onEvent({
        type: 'progress',
        content: {
            phase: 'cli-e2e-fake',
            message: 'running deterministic fake CLI analysis',
        },
        timestamp,
    });
    input.onEvent({
        type: 'thought',
        content: {
            thought: codeAware.codeReferences.length > 0
                ? 'Using SMARTPERFETTO_CLI_E2E_FAKE with deterministic code-aware symbol lookup to exercise source-level report rendering without a live LLM.'
                : 'Using SMARTPERFETTO_CLI_E2E_FAKE to exercise CLI persistence and rendering without a live LLM.',
        },
        timestamp,
    });
    input.onEvent({
        type: 'conclusion',
        content: {
            conclusion: fakeConclusion,
        },
        timestamp,
    });
    const totalDurationMs = Math.max(1, Date.now() - startedAt);
    const conclusionContract = codeAware.codeReferences.length > 0
        ? {
            schemaVersion: 'conclusion_contract_v1',
            mode: 'focused_answer',
            conclusions: [{
                    rank: 1,
                    statement: 'Deterministic code-aware CLI E2E conclusion references source-level CodeRefs.',
                    confidencePercent: 100,
                }],
            clusters: [],
            evidenceChain: [{
                    conclusionId: 'cli-e2e-code-aware',
                    text: 'CodeRef metadata was resolved from the registered local codebase RAG store.',
                }],
            claims: [],
            uncertainties: [],
            nextSteps: [],
            metadata: {
                confidencePercent: 100,
                rounds: 1,
            },
            codeReferences: codeAware.codeReferences,
        }
        : undefined;
    const claimSupport = [];
    const claimVerificationResult = {
        schemaVersion: 'claim_verifier@1',
        status: 'not_checked',
        policy: 'record_only',
        notCheckedReason: 'CLI E2E fake mode does not emit structured claims',
        passed: false,
        checkedClaimCount: 0,
        unsupportedClaimCount: 0,
        claimResults: [],
        issues: [],
    };
    return {
        sessionId,
        traceId,
        sdkSessionId: `cli-e2e-fake-${sessionId}`,
        model: 'cli-e2e-fake',
        providerId: null,
        agentRuntimeKind: 'openai-agents-sdk',
        providerSnapshotHash: null,
        reportHtml: buildCliE2eFakeReportHtml({
            sessionId,
            traceId,
            referenceTraceId: input.referenceTraceId,
            query: input.query,
            conclusion: fakeConclusion,
            conclusionContract,
            claimSupport,
            claimVerificationResult,
            identityResolutions: [],
            totalDurationMs,
        }),
        result: {
            sessionId,
            success: true,
            findings: [
                {
                    id: 'cli-e2e-fake-finding',
                    severity: 'info',
                    title: 'CLI E2E fake finding',
                    description: 'Deterministic finding emitted by the CLI E2E fake runtime.',
                    confidence: 1,
                    source: 'cli-e2e',
                },
            ],
            hypotheses: [],
            conclusion: fakeConclusion,
            ...(conclusionContract ? { conclusionContract } : {}),
            claimSupport,
            claimVerificationResult,
            identityResolutions: [],
            confidence: 1,
            rounds: 1,
            totalDurationMs,
        },
    };
}
function buildCliE2eFakeCodeAwareContext(input) {
    if (!input.codeAwareMode || input.codeAwareMode === 'off' || !input.codebaseIds?.length) {
        return { codeReferences: [] };
    }
    const store = new ragStore_1.RagStore((0, runtimePaths_1.backendLogPath)('rag_store.json'));
    const resolver = new symbolResolver_1.SymbolResolver(store);
    const symbols = [
        'MainActivity',
        'onActivityCreate',
        'LoadSimulator',
        'simulateAsyncNetworkLoad',
        'runChaosLoop',
        'LaunchConfig',
        'LoadConfig',
    ];
    const refs = new Map();
    for (const codebaseId of input.codebaseIds) {
        for (const symbol of symbols) {
            const resolved = resolver.resolveApp({ symbol, codebaseId, topK: 2 });
            for (const candidate of resolved.candidates) {
                const ref = candidateToCodeReference(candidate, codebaseId);
                if (ref)
                    refs.set(ref.chunkId, ref);
            }
        }
    }
    return { codeReferences: Array.from(refs.values()).slice(0, 8) };
}
function candidateToCodeReference(candidate, fallbackCodebaseId) {
    if (!candidate.chunkId || !candidate.filePath)
        return undefined;
    return {
        chunkId: candidate.chunkId,
        codebaseId: candidate.codebaseId ?? fallbackCodebaseId,
        filePath: candidate.filePath,
        ...(candidate.lineRange ? { lineRange: candidate.lineRange } : {}),
        ...(candidate.symbol ? { symbol: candidate.symbol } : {}),
    };
}
function buildCliE2eFakeReportHtml(input) {
    return (0, htmlReportGenerator_1.getHTMLReportGenerator)().generateAgentDrivenHTML({
        traceId: input.traceId,
        query: input.query,
        result: {
            sessionId: input.sessionId,
            success: true,
            findings: [
                {
                    id: 'cli-e2e-fake-finding',
                    severity: 'info',
                    title: 'CLI E2E fake finding',
                    description: 'Deterministic finding emitted by the CLI E2E fake runtime.',
                    confidence: 1,
                    source: 'cli-e2e',
                },
            ],
            hypotheses: [],
            conclusion: input.conclusion,
            ...(input.conclusionContract ? { conclusionContract: input.conclusionContract } : {}),
            claimSupport: input.claimSupport,
            claimVerificationResult: input.claimVerificationResult,
            identityResolutions: input.identityResolutions,
            confidence: 1,
            rounds: 1,
            totalDurationMs: input.totalDurationMs,
        },
        hypotheses: [],
        dialogue: [],
        timestamp: Date.now(),
    });
}
//# sourceMappingURL=cliAnalyzeService.js.map