"use strict";
/**
 * multi-framework-parser.ts
 * Main orchestrator for multi-framework OpenTelemetry trace parsing
 *
 * Features:
 * - Auto-detect framework (LangChain, CrewAI, Autogen)
 * - Dependency injection for parsers
 * - Unified API across all frameworks
 * - Comprehensive edge case handling
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiFrameworkParser = void 0;
const decimal_js_1 = __importDefault(require("decimal.js"));
const dagBuilder_1 = require("./utils/dagBuilder");
const treeBuilder_1 = require("./utils/treeBuilder");
const langgraph_1 = require("./frameworks/langgraph");
const langchain_1 = require("./frameworks/langchain");
const crewai_1 = require("./frameworks/crewai");
const autogen_1 = require("./frameworks/autogen");
const generic_1 = require("./frameworks/generic");
const index_1 = require("./frameworks/llamaindex/index");
class MultiFrameworkParser {
    constructor() {
        this.parsers = new Map();
        // Register default parsers
        this.registerParser('langgraph', new langgraph_1.LangGraphParser());
        this.registerParser('langchain', new langchain_1.LangChainParser());
        this.registerParser('crewai', new crewai_1.CrewAIParser());
        this.registerParser('autogen', new autogen_1.AutogenParser());
        this.registerParser('llamaindex', new index_1.LlamaIndexParser());
        this.defaultParser = new generic_1.GenericParser();
    }
    extractTraceDimensions(spans) {
        const pick = (v) => {
            const s = String(v ?? '').trim();
            return s.length > 0 ? s : undefined;
        };
        let environment;
        let version;
        for (const span of spans ?? []) {
            const ra = span?.resourceAttributes ?? {};
            const attrs = span?.attributes ?? {};
            if (!environment) {
                environment =
                    pick(ra?.['deployment.environment']) ??
                        pick(attrs?.['deployment.environment']) ??
                        pick(attrs?.['resource.deployment.environment']);
            }
            if (!version) {
                version =
                    pick(ra?.['service.version']) ??
                        pick(attrs?.['service.version']) ??
                        pick(attrs?.['resource.service.version']);
            }
            if (environment && version)
                break;
        }
        return { environment, version };
    }
    applyTraceDimensions(items, dims) {
        if (!dims.environment && !dims.version)
            return items;
        return (items ?? []).map((it) => ({ ...it, ...dims }));
    }
    /**
     * Register a custom framework parser
     */
    registerParser(framework, parser) {
        this.parsers.set(framework.toLowerCase(), parser);
    }
    /**
     * Unregister a parser
     */
    unregisterParser(framework) {
        this.parsers.delete(framework.toLowerCase());
    }
    /**
     * Get a specific parser
     */
    getParser(framework) {
        return this.parsers.get(framework.toLowerCase());
    }
    /**
     * AUTO-DETECT framework from spans
     */
    detectFramework(spans) {
        // Prefer langgraph before langchain
        const order = ['langgraph', 'langchain', 'autogen', 'crewai', 'llamaindex'];
        for (const f of order) {
            const parser = this.parsers.get(f);
            if (parser && parser.canParse(spans)) {
                console.log(`[Framework] Detected: ${f}`);
                return f;
            }
        }
        console.log('[Framework] Using generic parser');
        return 'generic';
    }
    /**
     * GET TOOLS from detected framework
     */
    getTools(spans) {
        const parser = this.getParserForSpans(spans);
        return parser.getTools(spans);
    }
    /**
     * GET AGENTS from detected framework
     */
    getAgents(spans) {
        const parser = this.getParserForSpans(spans);
        return parser.getAgents(spans);
    }
    /**
     * GET MODELS from detected framework
     */
    getModels(spans) {
        const parser = this.getParserForSpans(spans);
        return parser.getModels(spans);
    }
    /**
     * GENERATE DAG from detected framework
     */
    generateDag(spans) {
        const parser = this.getParserForSpans(spans);
        return parser.generateDag(spans);
    }
    /**
     * Convert timestamp (string ISO or numeric Unix ns/ms/s) to ms since epoch.
     * Uses number; may lose sub-ms precision for large nanosecond values. Prefer timestampToDecimalMs for sorting.
     */
    static timestampToMs(value) {
        return MultiFrameworkParser.timestampToDecimalMs(value).toNumber();
    }
    /**
     * Normalize timestamp to nanoseconds (Decimal) for comparison/sorting only.
     * Single scale so ns/ms/s/ISO are comparable; no conversion to ms needed.
     */
    static timestampToDecimalNs(value) {
        if (value == null)
            return new decimal_js_1.default(0);
        if (typeof value === 'number' && Number.isFinite(value)) {
            if (value >= 1e15)
                return new decimal_js_1.default(value); // already ns
            if (value >= 1e12)
                return new decimal_js_1.default(value).mul(1e6); // ms -> ns
            if (value > 0)
                return new decimal_js_1.default(value).mul(1e9); // seconds -> ns
            return new decimal_js_1.default(0);
        }
        if (typeof value === 'string') {
            const s = value.trim();
            const numLike = /^-?\d+\.?\d*$/;
            if (numLike.test(s)) {
                const d = new decimal_js_1.default(s);
                if (d.gte(1e15))
                    return d;
                if (d.gte(1e12))
                    return d.mul(1e6);
                if (d.gt(0))
                    return d.mul(1e9);
                return new decimal_js_1.default(0);
            }
            const t = new Date(s).getTime();
            return Number.isFinite(t) ? new decimal_js_1.default(t).mul(1e6) : new decimal_js_1.default(0); // ms -> ns
        }
        return new decimal_js_1.default(0);
    }
    /**
     * Timestamp in ms (Decimal) for duration/output. Use timestampToDecimalNs for comparison only.
     */
    static timestampToDecimalMs(value) {
        return MultiFrameworkParser.timestampToDecimalNs(value).div(1e6);
    }
    /**
     * Compare two spans by timestamp (high-precision), then by id for stable order when equal.
     */
    static compareSpanByTimestamp(a, b) {
        const cmp = MultiFrameworkParser.timestampToDecimalNs(a.timestamp).cmp(MultiFrameworkParser.timestampToDecimalNs(b.timestamp));
        if (cmp !== 0)
            return cmp;
        return (a.id ?? '').localeCompare(b.id ?? '', 'en');
    }
    /**
     * GENERATE LINEAR FLOW from detected framework (sorted by timestamp, stepNumber stable).
     */
    generateLinearFlow(spans) {
        const parser = this.getParserForSpans(spans);
        const sortedSpans = [...spans].sort((a, b) => MultiFrameworkParser.compareSpanByTimestamp(a, b));
        const raw = parser.generateLinearFlow(sortedSpans);
        const sorted = [...raw].sort((a, b) => (a.stepNumber ?? 0) - (b.stepNumber ?? 0));
        return sorted.map((step) => ({ ...step, stepNumber: step.stepNumber ?? 0 }));
    }
    /**
     * PARSE ALL - comprehensive parsing in one call
     */
    parse(spans) {
        const framework = this.detectFramework(spans);
        const parser = this.getParserForSpans(spans);
        const dims = this.extractTraceDimensions(spans);
        console.log(`[Parse] Processing ${spans.length} spans with ${framework} parser`);
        const tools = parser.getTools(spans);
        const agents = parser.getAgents(spans);
        const models = parser.getModels(spans);
        const modelsFromUnscoped = parser?.getModelsFromUnscoped?.(spans) ?? [];
        const { agents: enrichedAgents, tools: enrichedTools } = this.enrichAgentsAndToolsFromModels({
            framework,
            spans,
            agents,
            tools,
            models,
        });
        const spanDag = parser.generateDag(spans);
        const linearFlow = parser.generateLinearFlow(spans);
        const metadata = { ...this.calculateMetadata({
                framework,
                spans,
                tools: enrichedTools,
                agents: enrichedAgents,
                models,
                modelsFromUnscoped,
            }), ...dims };
        const finalTools = this.applyTraceDimensions(enrichedTools, dims);
        const finalAgents = this.applyTraceDimensions(enrichedAgents, dims);
        const finalModels = this.applyTraceDimensions(models, dims);
        const { graph: dag } = (0, dagBuilder_1.buildCallDagGraph)(spans, finalTools, finalAgents, finalModels);
        const { input, output } = this.extractInputOutput(spans);
        console.log(`[Parse] Results: ${enrichedTools.length} tools, ${enrichedAgents.length} agents, ${models.length} models`);
        return {
            framework: framework,
            tools: finalTools,
            agents: finalAgents,
            models: finalModels,
            unScopedModels: modelsFromUnscoped,
            environment: dims.environment,
            version: dims.version,
            input,
            output,
            dag,
            spanDag,
            linearFlow,
            metadata
        };
    }
    /**
     * PARSE with specific framework parser
     */
    parseWithFramework(spans, framework) {
        const parser = this.parsers.get(framework.toLowerCase()) || this.defaultParser;
        const dims = this.extractTraceDimensions(spans);
        console.log(`[Parse] Using ${framework} parser for ${spans.length} spans`);
        const tools = parser.getTools(spans);
        const agents = parser.getAgents(spans);
        const models = parser.getModels(spans);
        const modelsFromUnscoped = parser?.getModelsFromUnscoped?.(spans) ?? [];
        const { agents: enrichedAgents, tools: enrichedTools } = this.enrichAgentsAndToolsFromModels({
            framework,
            spans,
            agents,
            tools,
            models,
        });
        const spanDag = parser.generateDag(spans);
        const linearFlow = parser.generateLinearFlow(spans);
        const metadata = { ...this.calculateMetadata({
                framework,
                spans,
                tools: enrichedTools,
                agents: enrichedAgents,
                models,
                modelsFromUnscoped,
            }), ...dims };
        const finalTools = this.applyTraceDimensions(enrichedTools, dims);
        const finalAgents = this.applyTraceDimensions(enrichedAgents, dims);
        const finalModels = this.applyTraceDimensions(models, dims);
        const { graph: dag } = (0, dagBuilder_1.buildCallDagGraph)(spans, finalTools, finalAgents, finalModels);
        const { input, output } = this.extractInputOutput(spans);
        return {
            framework: framework,
            tools: finalTools,
            agents: finalAgents,
            models: finalModels,
            unScopedModels: modelsFromUnscoped,
            environment: dims.environment,
            version: dims.version,
            input,
            output,
            dag,
            spanDag,
            linearFlow,
            metadata
        };
    }
    /**
     * Get parser for spans
     */
    getParserForSpans(spans) {
        const framework = this.detectFramework(spans);
        return this.parsers.get(framework) || this.defaultParser;
    }
    /**
     * GET DAG DATA - returns only DAG graph data (folded: unique nodes per tool/agent/model/prompt/response).
     * Mirrors getLinearFlowData in structure: prompt → unique entities → response; nodes shown once in acyclic graph.
     */
    getDagData(spans) {
        const { input, output, renderLinear } = this.getLinearFlowData(spans);
        const { dag: foldedDag, promptEntityId, responseEntityId, nodeIdsWIthNameMap } = this.foldDagWithPromptAndResponse(renderLinear, spans);
        return {
            dag: foldedDag,
            input,
            output,
            entityOfSpanId: new Map(),
            promptEntityId,
            responseEntityId,
            nodeIdsWIthNameMap,
        };
    }
    /**
     * Convert verified linear flow (renderLinear) to DAG format.
     * Creates unique nodes and edges with proper labels (prompt, response, tool:<name>, agent:<name>, model:<name>).
     */
    foldDagWithPromptAndResponse(renderLinear, _spans) {
        // Create unique nodes with proper node labels
        const nodeMap = new Map();
        for (const n of renderLinear.nodes ?? []) {
            let nodeLabel;
            if (n.kind === 'prompt') {
                nodeLabel = 'prompt';
            }
            else if (n.kind === 'response') {
                nodeLabel = 'response';
            }
            else if (n.kind === 'tool') {
                nodeLabel = `tool:${n.label}`;
            }
            else if (n.kind === 'agent') {
                nodeLabel = `agent:${n.label}`;
            }
            else if (n.kind === 'model') {
                nodeLabel = `model:${n.label}`;
            }
            else {
                nodeLabel = n.label;
            }
            if (!nodeMap.has(nodeLabel)) {
                nodeMap.set(nodeLabel, {
                    id: nodeLabel,
                    kind: n.kind,
                    label: n.label,
                });
            }
        }
        const nodes = Array.from(nodeMap.values());
        // Create node ID mapping from original IDs to new labels
        const idToLabel = new Map();
        for (const n of renderLinear.nodes ?? []) {
            let nodeLabel;
            if (n.kind === 'prompt') {
                nodeLabel = 'prompt';
            }
            else if (n.kind === 'response') {
                nodeLabel = 'response';
            }
            else if (n.kind === 'tool') {
                nodeLabel = `tool:${n.label}`;
            }
            else if (n.kind === 'agent') {
                nodeLabel = `agent:${n.label}`;
            }
            else if (n.kind === 'model') {
                nodeLabel = `model:${n.label}`;
            }
            else {
                nodeLabel = n.label;
            }
            idToLabel.set(n.id, nodeLabel);
        }
        // Create edges with proper from/to labels and count
        const edges = (renderLinear.edges ?? []).map((e) => ({
            from: idToLabel.get(e.from) ?? e.from,
            fromId: e.from,
            to: idToLabel.get(e.to) ?? e.to,
            toId: e.to,
            type: e.type,
            count: e.count ?? 1,
        }));
        // Find actual prompt and response entity IDs using existing logic
        const { promptEntityId: foundPromptEntityId, responseEntityId: foundResponseEntityId } = this.findPromptAndResponseEntitiesFromLinear(renderLinear, _spans, idToLabel);
        const dag = { nodes, edges };
        return {
            dag,
            promptEntityId: foundPromptEntityId ?? 'prompt',
            responseEntityId: foundResponseEntityId ?? 'response',
            nodeIdsWIthNameMap: idToLabel.entries()
        };
    }
    /**
     * Adapted version of findPromptAndResponseEntities that works with linear flow data
     */
    findPromptAndResponseEntitiesFromLinear(renderLinear, spans, idToLabel) {
        const sorted = [...spans].sort((a, b) => MultiFrameworkParser.compareSpanByTimestamp(a, b));
        const hasPrompt = (s) => {
            if (s.attributes?.['gen_ai.prompt'] != null || s.attributes?.['genai.prompt'] != null)
                return true;
            if (s.events?.some((e) => e.attributes?.['gen_ai.prompt'] != null || e.attributes?.['genai.prompt'] != null)) {
                return true;
            }
            const looseEvents = s?.eventsAttributes ?? s?.Events?.Attributes;
            if (!Array.isArray(looseEvents))
                return false;
            return looseEvents.some((e) => e?.['gen_ai.prompt'] != null || e?.['genai.prompt'] != null);
        };
        const hasCompletion = (s) => {
            if (s.attributes?.['gen_ai.completion'] != null || s.attributes?.['genai.completion'] != null)
                return true;
            if (s.events?.some((e) => e.attributes?.['gen_ai.completion'] != null ||
                e.attributes?.['genai.completion'] != null)) {
                return true;
            }
            const looseEvents = s?.eventsAttributes ?? s?.Events?.Attributes;
            if (!Array.isArray(looseEvents))
                return false;
            return looseEvents.some((e) => e?.['gen_ai.completion'] != null || e?.['genai.completion'] != null);
        };
        const promptSpan = sorted.find(hasPrompt);
        const completionSpan = [...sorted].reverse().find(hasCompletion);
        // Get first and last entity from linear flow
        const entityNodes = (renderLinear.nodes ?? []).filter(n => n.kind !== 'prompt' && n.kind !== 'response');
        const firstEntityId = entityNodes.length > 0 ? idToLabel.get(entityNodes[0].id) : undefined;
        const lastEntityId = entityNodes.length > 0 ? idToLabel.get(entityNodes[entityNodes.length - 1].id) : undefined;
        const promptEntityId = promptSpan
            ? (idToLabel.get(promptSpan.id) ?? firstEntityId)
            : firstEntityId;
        const responseEntityId = completionSpan
            ? (idToLabel.get(completionSpan.id) ?? lastEntityId)
            : lastEntityId;
        return { promptEntityId, responseEntityId };
    }
    /**
     * GET LINEAR FLOW DATA - uses same logic as getTraceViewDagData (entity hierarchy, DFS order).
     * Returns linearFlow derived from the trace view order so linear and trace view stay in sync.
     */
    getLinearFlowData(spans) {
        const { input, output } = this.extractInputOutput(spans);
        const linearFlow = this.generateLinearFlow(spans);
        const renderLinear = this.buildRenderLinearGraph(linearFlow, input, output);
        return { input, output, renderLinear };
    }
    /**
     * TRACE VIEW DAG (span-level, no clubbing):
     * - Only nodes: prompt / agent / tool / model / response
     * - Edges represent a single LINEAR flow (prompt -> step1 -> ... -> response)
     * - Hierarchy is represented by *ordering* (agent then its tool(s) then their model(s)),
     *   derived from real span parent relationships + timestamps (no invented links).
     * - Nodes/edges are returned precomputed for the UI (no frontend stitching required).
     */
    getTraceViewDagData(spans) {
        const parser = this.getParserForSpans(spans);
        const dims = this.extractTraceDimensions(spans);
        // Use framework parser entity extraction for accurate classification
        const tools = this.applyTraceDimensions(parser.getTools(spans), dims);
        const agents = this.applyTraceDimensions(parser.getAgents(spans), dims);
        const models = this.applyTraceDimensions(parser.getModels(spans), dims);
        const toolBySpanId = new Map(tools.map((t) => [String(t.spanId), t]));
        const agentBySpanId = new Map(agents.map((a) => [String(a.id), a]));
        const modelBySpanId = new Map(models.map((m) => [String(m.spanId), m]));
        const spanById = new Map(spans.map((s) => [String(s.id), s]));
        const getFirstAttr = (attrs, ...keys) => {
            const a = attrs ?? {};
            for (const k of keys) {
                const v = a[k];
                if (v !== undefined && v !== null && String(v).trim() !== '')
                    return v;
            }
            return undefined;
        };
        const classify = (spanId) => {
            const tool = toolBySpanId.get(spanId);
            if (tool) {
                return { kind: 'tool', label: tool.name || 'Tool', request: tool.input, response: tool.output };
            }
            const agent = agentBySpanId.get(spanId);
            if (agent) {
                const span = spanById.get(spanId);
                const req = getFirstAttr(span?.attributes, 'gen_ai.workflow.input', 'genai.workflow.input', 'gen_ai.prompt', 'genai.prompt');
                const res = getFirstAttr(span?.attributes, 'gen_ai.workflow.output', 'genai.workflow.output', 'gen_ai.completion', 'genai.completion');
                return { kind: 'agent', label: agent.name || 'Agent', request: req, response: res };
            }
            const model = modelBySpanId.get(spanId);
            if (model) {
                const span = spanById.get(spanId);
                const req = getFirstAttr(span?.attributes, 'gen_ai.prompt', 'genai.prompt', 'gen_ai.request', 'genai.request');
                const res = getFirstAttr(span?.attributes, 'gen_ai.completion', 'genai.completion', 'gen_ai.response', 'genai.response');
                return { kind: 'model', label: model.modelName || 'Model', request: req, response: res };
            }
            return null;
        };
        const isEntitySpan = (sid) => toolBySpanId.has(sid) || agentBySpanId.has(sid) || modelBySpanId.has(sid);
        const kindRank = (k) => (k === 'agent' ? 0 : k === 'tool' ? 1 : 2);
        // Get all entity span IDs
        const entitySpanIds = spans
            .map((s) => String(s.id))
            .filter((sid) => isEntitySpan(sid));
        const entitySet = new Set(entitySpanIds);
        const kindOf = (id) => {
            if (agentBySpanId.has(id))
                return 'agent';
            if (toolBySpanId.has(id))
                return 'tool';
            if (modelBySpanId.has(id))
                return 'model';
            return undefined;
        };
        // Build nearest-entity-parent for each entity span (based strictly on span ancestry)
        const nearestEntityParent = (sid) => {
            let cur = spanById.get(sid)?.parentSpanId ?? null;
            let guard = 0;
            const seen = new Set();
            while (cur && guard < 200) {
                if (seen.has(cur))
                    return undefined;
                seen.add(cur);
                if (entitySet.has(cur))
                    return cur;
                cur = spanById.get(cur)?.parentSpanId ?? null;
                guard++;
            }
            return undefined;
        };
        // Build a children map between entity nodes (entity-parent -> entity-children)
        const children = new Map();
        const roots = [];
        for (const sid of entitySpanIds) {
            const p = nearestEntityParent(sid);
            if (p) {
                children.set(p, [...(children.get(p) ?? []), sid]);
            }
            else {
                roots.push(sid);
            }
        }
        const timeOf = (id) => MultiFrameworkParser.timestampToDecimalNs(spanById.get(id)?.timestamp);
        // Sort children by (timestamp asc, kind rank asc) to keep agent->tool->model ordering stable
        const sortIds = (ids) => ids.slice().sort((a, b) => {
            const ta = timeOf(a);
            const tb = timeOf(b);
            const tc = ta.cmp(tb);
            if (tc !== 0)
                return tc;
            const ka = kindOf(a);
            const kb = kindOf(b);
            if (ka && kb)
                return kindRank(ka) - kindRank(kb);
            return 0;
        });
        // DFS traversal that yields a single linear list representing hierarchy without extra edges.
        const visited = new Set();
        const orderedEntityIds = [];
        const dfs = (id) => {
            if (visited.has(id))
                return;
            visited.add(id);
            orderedEntityIds.push(id);
            const kids = children.get(id) ?? [];
            for (const c of sortIds(kids))
                dfs(c);
        };
        for (const r of sortIds(roots))
            dfs(r);
        // (Safety) include any entity nodes not reached due to weird ancestry
        for (const sid of sortIds(entitySpanIds)) {
            if (!visited.has(sid))
                dfs(sid);
        }
        const { input, output } = this.extractInputOutput(spans);
        const nodes = [{ id: 'prompt', kind: 'prompt', label: 'Prompt', lane: 'middle', order: 0, text: input }];
        for (let i = 0; i < orderedEntityIds.length; i++) {
            const sid = orderedEntityIds[i];
            const s = spanById.get(sid);
            const info = classify(sid);
            if (!info || !s)
                continue;
            nodes.push({
                id: sid,
                kind: info.kind,
                label: info.label,
                lane: this.laneForKind(info.kind),
                order: i + 1,
                request: info.request,
                response: info.response,
                duration: typeof s.duration === 'number' ? s.duration / 1000000 : undefined, // ns -> ms for UI
                timestamp: s.timestamp,
            });
        }
        // Response node after all entity nodes
        nodes.push({
            id: 'response',
            kind: 'response',
            label: 'Response',
            lane: 'middle',
            order: (orderedEntityIds.length || 0) + 1,
            text: output,
        });
        // Linear edges: prompt -> step1 -> ... -> response
        const edges = [];
        if (orderedEntityIds.length > 0) {
            edges.push({ from: 'prompt', to: orderedEntityIds[0], type: 'prompt' });
            for (let i = 0; i < orderedEntityIds.length - 1; i++) {
                edges.push({ from: orderedEntityIds[i], to: orderedEntityIds[i + 1], type: 'sequence' });
            }
            edges.push({ from: orderedEntityIds[orderedEntityIds.length - 1], to: 'response', type: 'response' });
        }
        else {
            edges.push({ from: 'prompt', to: 'response', type: 'sequence' });
        }
        return { input, output, renderLinear: { nodes, edges } };
    }
    /**
     * Check if a model is a descendant of any agent or tool in the span hierarchy
     */
    isModelDescendantOfAgentOrTool(modelSpanId, agents, tools, spans) {
        const spanById = new Map(spans.map((s) => [s.id, s]));
        const agentIds = new Set(agents.map((a) => a.id));
        const toolSpanIds = new Set(tools.map((t) => t.spanId));
        const findNearestAncestor = (startParent, predicate) => {
            let cur = startParent;
            let guard = 0;
            const seen = new Set();
            while (cur && guard < 100) {
                if (predicate(cur))
                    return cur;
                if (seen.has(cur))
                    return null;
                seen.add(cur);
                const s = spanById.get(cur);
                cur = s?.parentSpanId ?? null;
                guard++;
            }
            return null;
        };
        const modelSpan = spanById.get(modelSpanId);
        const parentId = modelSpan?.parentSpanId ?? null;
        const agentId = findNearestAncestor(parentId, (sid) => agentIds.has(sid));
        const toolId = findNearestAncestor(parentId, (sid) => toolSpanIds.has(sid));
        return !!(agentId || toolId);
    }
    /**
     * Filter root nodes to exclude models that are descendants of agents or tools
     * Models inside agents/tools should appear as children, not as root-level nodes
     */
    filterRootNodes(rootNodes, agents, tools, spans) {
        return rootNodes.filter((node) => {
            // If it's a model, check if it's a descendant of an agent or tool
            if (node.type === 'model') {
                return !this.isModelDescendantOfAgentOrTool(node.spanId, agents, tools, spans);
            }
            // Keep all agents and tools as root nodes
            return true;
        });
    }
    /**
     * GET DETAILS DATA - returns agents, tools, models, metadata, and pre-built trees
     */
    getDetailsData(spans) {
        const framework = this.detectFramework(spans);
        const dims = this.extractTraceDimensions(spans);
        const parser = this.getParserForSpans(spans);
        const tools = parser.getTools(spans);
        const agents = parser.getAgents(spans);
        const models = parser.getModels(spans);
        const modelsFromUnscoped = parser?.getModelsFromUnscoped?.(spans) ?? [];
        const sortedSpans = [...spans].sort((a, b) => MultiFrameworkParser.compareSpanByTimestamp(a, b));
        const { agents: enrichedAgents, tools: enrichedTools } = this.enrichAgentsAndToolsFromModels({
            framework,
            spans: sortedSpans,
            agents,
            tools,
            models,
        });
        const finalTools = this.applyTraceDimensions(enrichedTools, dims);
        const finalAgents = this.applyTraceDimensions(enrichedAgents, dims);
        const finalModels = this.applyTraceDimensions(models, dims);
        const metadata = { ...this.calculateMetadata({
                framework,
                spans: sortedSpans,
                tools,
                agents,
                models,
                modelsFromUnscoped,
            }), ...dims };
        // Build trees for frontend display - pass all models so they can appear as children
        const entityHierarchyRaw = (0, treeBuilder_1.buildEntityHierarchy)(finalAgents, finalTools, finalModels, sortedSpans);
        // Filter root nodes to exclude models that are descendants of agents/tools
        // These models will still appear as children in the hierarchy, but not as root-level nodes
        const entityHierarchy = this.filterRootNodes(entityHierarchyRaw, finalAgents, finalTools, sortedSpans);
        const spanTree = (0, treeBuilder_1.buildSpanTree)(sortedSpans);
        const { input, output } = this.extractInputOutput(sortedSpans);
        return {
            agents: finalAgents,
            tools: finalTools,
            models: finalModels,
            metadata,
            framework: framework,
            input,
            output,
            entityHierarchy,
            spanTree,
        };
    }
    laneForKind(kind) {
        if (kind === 'tool')
            return 'tools';
        if (kind === 'model' || kind === 'workflow')
            return 'models';
        if (kind === 'agent')
            return 'agents';
        return 'models';
    }
    buildRenderLinearGraph(linearFlow, input, output) {
        const steps = [...(linearFlow ?? [])].sort((a, b) => (a.stepNumber ?? 0) - (b.stepNumber ?? 0));
        const stepKind = (t) => {
            if (t === 'tool')
                return 'tool';
            if (t === 'llm')
                return 'model';
            if (t === 'agent')
                return 'agent';
            if (t === 'workflow')
                return 'workflow';
            return 'other';
        };
        const nodes = [
            { id: 'prompt', kind: 'prompt', label: 'Prompt', lane: 'middle', order: 0, text: input },
        ];
        for (let i = 0; i < steps.length; i++) {
            const s = steps[i];
            const kind = stepKind(String(s.type ?? 'other'));
            const id = String(s.spanId ?? `step-${i + 1}`);
            nodes.push({
                id,
                kind: kind,
                label: String(s.name ?? id),
                lane: this.laneForKind(kind),
                order: i + 1,
            });
        }
        nodes.push({ id: 'response', kind: 'response', label: 'Response', lane: 'middle', order: steps.length + 1, text: output });
        const edges = [];
        if (steps.length > 0) {
            edges.push({ from: 'prompt', to: String(steps[0].spanId), type: 'prompt' });
            for (let i = 0; i < steps.length - 1; i++) {
                edges.push({ from: String(steps[i].spanId), to: String(steps[i + 1].spanId), type: 'sequence' });
            }
            edges.push({ from: String(steps[steps.length - 1].spanId), to: 'response', type: 'response' });
        }
        return { nodes, edges };
    }
    /**
     * Extract the initial user input prompt and final response from the trace
     */
    extractInputOutput(spans) {
        let input;
        let output;
        // Sort spans by timestamp (high-precision) to find chronological order
        const sortedSpans = [...spans].sort((a, b) => MultiFrameworkParser.compareSpanByTimestamp(a, b));
        // Strategy 1: Look for gen_ai.workflow.input/output in agent spans (langgraph/langchain with openlit)
        const agentSpans = sortedSpans.filter((s) => s.attributes?.['gen_ai.operation.name'] === 'invoke_agent' ||
            s.name?.toLowerCase().includes('agent'));
        // Find the root agent span (earliest agent span, or one with no parent that's also an agent)
        const rootAgentSpan = agentSpans.find((s) => !s.parentSpanId || s.parentSpanId === '') ||
            agentSpans[0];
        if (rootAgentSpan) {
            // Extract input from workflow input
            const workflowInput = rootAgentSpan.attributes?.['gen_ai.workflow.input'];
            if (workflowInput) {
                input = this.extractUserPromptFromWorkflowInput(workflowInput);
            }
        }
        // Find the last agent span for the output
        const lastAgentSpan = agentSpans[agentSpans.length - 1];
        if (lastAgentSpan) {
            const workflowOutput = lastAgentSpan.attributes?.['gen_ai.workflow.output'];
            if (workflowOutput) {
                output = this.extractFinalResponseFromWorkflowOutput(workflowOutput);
            }
        }
        // Strategy 2: Autogen - look for "autogen process" spans and parse "message" attribute
        if (!input || !output) {
            const autogenSpans = sortedSpans.filter(s => s.name?.startsWith('autogen process'));
            if (autogenSpans.length > 0) {
                if (!input) {
                    const firstMsg = autogenSpans[0].attributes?.['message'];
                    if (firstMsg && typeof firstMsg === 'string') {
                        input = this.extractAutogenContent(firstMsg) || input;
                    }
                }
                if (!output) {
                    const lastMsg = autogenSpans[autogenSpans.length - 1].attributes?.['message'];
                    if (lastMsg && typeof lastMsg === 'string') {
                        output = this.extractAutogenContent(lastMsg) || output;
                    }
                }
            }
        }
        // Strategy 3: Look for gen_ai.prompt/completion in ANY span if we didn't find input/output yet.
        // This covers cases like langchain_8.json where prompt/completion are on specific chat spans but not on a root workflow span.
        if (!input) {
            // Find the *first* span that has a prompt
            const promptSpan = sortedSpans.find(s => s.attributes?.['gen_ai.prompt']);
            if (promptSpan) {
                const prompt = promptSpan.attributes?.['gen_ai.prompt'];
                if (prompt) {
                    input = this.extractUserPromptFromPromptString(prompt);
                }
            }
        }
        if (!output) {
            // Find the *last* span that has a completion (likely the final answer)
            // Iterate backwards
            for (let i = sortedSpans.length - 1; i >= 0; i--) {
                const completion = sortedSpans[i].attributes?.['gen_ai.completion'];
                if (completion) {
                    output = completion;
                    break;
                }
            }
        }
        // Strategy 4: Look for HTTP request/response spans (FastAPI, etc.) for input as a last resort
        if (!input) {
            const httpServerSpans = sortedSpans.filter((s) => s.attributes?.['http.method'] === 'POST' &&
                (!s.parentSpanId || s.parentSpanId === ''));
            const rootHttpSpan = httpServerSpans[0];
            if (rootHttpSpan) {
                // Try to get from any custom attributes that might store request body
                const reqBody = rootHttpSpan.attributes?.['http.request.body'] ||
                    rootHttpSpan.attributes?.['request.body'];
                if (reqBody) {
                    input = typeof reqBody === 'string' ? reqBody : JSON.stringify(reqBody);
                }
            }
        }
        return { input, output };
    }
    /**
     * Helper to extract content from Autogen nested message JSON
     */
    extractAutogenContent(messageJson) {
        try {
            if (messageJson === '{}')
                return undefined;
            const parsed = JSON.parse(messageJson);
            // Pattern 1: { response: { chat_message: { content: "..." } } }
            if (parsed.response?.chat_message?.content) {
                return parsed.response.chat_message.content;
            }
            // Pattern 2: { content: "..." }
            if (parsed.content) {
                return parsed.content;
            }
            // Pattern 3: Nested JSON in content (sometimes content is a stringified JSON)
            // This happens with tool outputs sometimes, might want to skip or parse.
            // For now, if we found a string content, return it.
        }
        catch {
            // ignore
        }
        return undefined;
    }
    /**
     * Extract user prompt from gen_ai.workflow.input JSON string
     */
    extractUserPromptFromWorkflowInput(workflowInput) {
        try {
            const parsed = JSON.parse(workflowInput);
            const messages = parsed?.messages;
            if (Array.isArray(messages) && messages.length > 0) {
                // Find the first HumanMessage - it contains the user's original prompt
                for (const msg of messages) {
                    if (typeof msg === 'string') {
                        // Parse content from stringified message like: "content=\"what's the combined headcount...\" additional_kwargs={}"
                        const contentMatch = msg.match(/content=['"](.*?)['"] additional_kwargs/s);
                        if (contentMatch) {
                            return contentMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n');
                        }
                        // Alternative pattern: content='...'
                        const altMatch = msg.match(/^content=['"](.+?)['"](?:\s|$)/s);
                        if (altMatch) {
                            return altMatch[1];
                        }
                    }
                }
                // Fallback: return the first message as-is
                const firstMsg = messages[0];
                if (typeof firstMsg === 'string') {
                    const match = firstMsg.match(/content=['"](.*?)['"](?:\s|additional_kwargs)/s);
                    if (match)
                        return match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n');
                }
            }
        }
        catch {
            // Not valid JSON, return undefined
        }
        return undefined;
    }
    /**
     * Extract final response from gen_ai.workflow.output JSON string
     */
    extractFinalResponseFromWorkflowOutput(workflowOutput) {
        try {
            const parsed = JSON.parse(workflowOutput);
            const messages = parsed?.messages;
            if (Array.isArray(messages) && messages.length > 0) {
                // Work backwards to find the last message with actual content (not tool calls)
                for (let i = messages.length - 1; i >= 0; i--) {
                    const msg = messages[i];
                    if (typeof msg === 'string') {
                        // Check if this message has finish_reason: 'stop' (indicates final response)
                        if (msg.includes("finish_reason': 'stop'") || msg.includes('finish_reason\': "stop"')) {
                            const contentMatch = msg.match(/content=['"](.+?)['"](?:\s|additional_kwargs)/s);
                            if (contentMatch && contentMatch[1].trim()) {
                                return contentMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
                            }
                        }
                        // Check for non-empty content that's not just empty string
                        const contentMatch = msg.match(/content=['"](.+?)['"](?:\s|additional_kwargs)/s);
                        if (contentMatch && contentMatch[1].trim() && !msg.includes("tool_calls': [")) {
                            return contentMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
                        }
                    }
                }
            }
        }
        catch {
            // Not valid JSON
        }
        return undefined;
    }
    /**
     * Extract user prompt from gen_ai.prompt string format
     * Format: "system: ...\nhuman: ...\nai: ..."
     */
    extractUserPromptFromPromptString(prompt) {
        if (!prompt)
            return undefined;
        // Look for "human: ..." pattern
        const humanMatch = prompt.match(/human:\s*(.+?)(?:\nai:|$)/s);
        if (humanMatch) {
            return humanMatch[1].trim();
        }
        // Fallback: if no human prefix, return the whole prompt
        return prompt;
    }
    /**
     * Calculate metadata
     */
    calculateMetadata({ framework, spans, tools, agents, models, modelsFromUnscoped, }) {
        const llmModels = framework === 'llamaindex' ? modelsFromUnscoped : models;
        // Calculate total duration (time from first to last span) with high precision
        const timestampsMs = spans.map(s => MultiFrameworkParser.timestampToDecimalMs(s.timestamp));
        const durations = spans.map(s => new decimal_js_1.default(s.duration).div(1000000)); // ns -> ms
        const minTimestamp = timestampsMs.length > 0 ? decimal_js_1.default.min(...timestampsMs) : new decimal_js_1.default(0);
        const maxTimestamp = timestampsMs.length > 0
            ? decimal_js_1.default.max(...timestampsMs.map((t, i) => t.plus(durations[i] ?? 0)))
            : new decimal_js_1.default(0);
        const totalDuration = maxTimestamp.minus(minTimestamp).toNumber();
        // Calculate total cost
        const totalCost = llmModels.reduce((sum, m) => sum + (m.cost || 0), 0);
        // Count spans
        const uniqueSpans = new Set(spans.map(s => s.id));
        const duplicateSpanCount = spans.length - uniqueSpans.size;
        // Count tool and LLM calls
        const toolCallCount = tools.length;
        const llmCallCount = llmModels.length;
        const agentCount = agents.length;
        // Calculate total tokens from models (avoiding double counting from agents/tools)
        const totalTokens = llmModels.reduce((sum, m) => sum + (m.tokenUsage?.totalTokens || 0), 0);
        // Time to first token: from each model's ttft or its span's gen_ai attributes (ms)
        const spanById = new Map(spans.map((s) => [s.id, s]));
        const ttftValues = [];
        for (const m of llmModels) {
            const fromModel = m?.ttft;
            let ttftMs;
            if (typeof fromModel === 'number' && Number.isFinite(fromModel)) {
                ttftMs = fromModel;
            }
            else if (Array.isArray(fromModel) && fromModel.length > 0) {
                const sum = fromModel.reduce((a, b) => a + (Number.isFinite(b) ? b : 0), 0);
                if (Number.isFinite(sum))
                    ttftMs = sum;
            }
            if (ttftMs == null) {
                const span = spanById.get(m.spanId);
                const attrs = span?.attributes ?? {};
                const raw = attrs['gen_ai.server.time_to_first_token'] ??
                    attrs['genai.server.time_to_first_token'] ??
                    attrs['gen_ai.time_to_first_token'] ??
                    attrs['genai.time_to_first_token'];
                if (raw != null) {
                    const n = parseFloat(String(raw));
                    if (Number.isFinite(n))
                        ttftMs = n;
                }
            }
            if (ttftMs != null && Number.isFinite(ttftMs))
                ttftValues.push(ttftMs);
        }
        const totalTimeToFirstToken = ttftValues.length > 0 ? ttftValues.reduce((a, b) => a + b, 0) : undefined;
        const averageTimeToFirstToken = ttftValues.length > 0 ? (totalTimeToFirstToken ?? 0) / ttftValues.length : undefined;
        return {
            totalDuration,
            totalCost,
            totalTokens,
            spanCount: spans.length,
            toolCallCount,
            llmCallCount,
            agentCount,
            uniqueSpanCount: uniqueSpans.size,
            duplicateSpanCount,
            totalTimeToFirstToken,
            averageTimeToFirstToken
        };
    }
    /**
     * LlamaIndex: models are not always children of agent spans (e.g. siblings under AgentWorkflow.run).
     * Roll up model metrics to agents in the same workflow root when ancestry-based rollup did not match.
     */
    rollupModelsToAgentsLlamaIndex(spans, agents, models, spanById, agentIds, modelIdsAlreadyRolledToAgent, mergeIntoAgent, extractTtft, nsToMs) {
        const isWorkflowRoot = (s) => {
            const n = s.name ?? '';
            return n === 'AgentWorkflow.run' || n.startsWith('AgentWorkflow.run');
        };
        const parentOf = new Map(spans.map((s) => [s.id, s.parentSpanId ?? null]));
        const childMap = new Map();
        for (const s of spans) {
            const pid = s.parentSpanId ?? '';
            if (!pid)
                continue;
            if (!childMap.has(pid))
                childMap.set(pid, []);
            childMap.get(pid).push(s);
        }
        const getDescendantIds = (rootId) => {
            const out = new Set();
            const queue = [rootId];
            while (queue.length) {
                const id = queue.shift();
                for (const c of childMap.get(id) ?? []) {
                    if (!out.has(c.id)) {
                        out.add(c.id);
                        queue.push(c.id);
                    }
                }
            }
            return out;
        };
        const findWorkflowRoot = (startSpanId) => {
            let cur = startSpanId;
            let guard = 0;
            const seen = new Set();
            while (cur && guard < 500) {
                const s = spanById.get(cur);
                if (s && isWorkflowRoot(s))
                    return s;
                if (seen.has(cur))
                    return null;
                seen.add(cur);
                cur = parentOf.get(cur) ?? null;
                guard++;
            }
            return null;
        };
        const workflowRootIds = new Set(spans.filter(isWorkflowRoot).map((s) => s.id));
        const rootIdToAgentIdsUnderRoot = new Map();
        for (const rootId of workflowRootIds) {
            const descendantIds = getDescendantIds(rootId);
            const agentIdsUnderRoot = agents
                .filter((a) => descendantIds.has(a.id))
                .map((a) => a.id);
            if (agentIdsUnderRoot.length > 0) {
                const rootSpan = spanById.get(rootId);
                const startTime = (t) => MultiFrameworkParser.timestampToDecimalNs(spanById.get(t)?.timestamp ?? 0);
                agentIdsUnderRoot.sort((a, b) => startTime(a).cmp(startTime(b)));
                rootIdToAgentIdsUnderRoot.set(rootId, agentIdsUnderRoot);
            }
        }
        const modelStartNs = (spanId) => MultiFrameworkParser.timestampToDecimalNs(spanById.get(spanId)?.timestamp ?? 0);
        for (const model of models) {
            if (modelIdsAlreadyRolledToAgent.has(model.spanId))
                continue;
            const span = spanById.get(model.spanId);
            const root = findWorkflowRoot(model.spanId);
            if (!root || !rootIdToAgentIdsUnderRoot.has(root.id))
                continue;
            const agentIdsUnderRoot = rootIdToAgentIdsUnderRoot.get(root.id);
            const modelStart = modelStartNs(model.spanId);
            const agentStartedBeforeModel = agentIdsUnderRoot.filter((aid) => modelStartNs(aid).lte(modelStart));
            const agentId = agentStartedBeforeModel.length > 0
                ? agentStartedBeforeModel[agentStartedBeforeModel.length - 1]
                : agentIdsUnderRoot[0];
            if (!agentId || !agentIds.has(agentId))
                continue;
            const ttft = extractTtft(model, span);
            const durationMs = nsToMs(span?.duration);
            const tokenUsage = model?.tokenUsage;
            mergeIntoAgent(agentId, model, ttft, durationMs, tokenUsage);
        }
    }
    enrichAgentsAndToolsFromModels({ framework, spans, agents, tools, models, }) {
        const spanById = new Map(spans.map((s) => [s.id, s]));
        const agentIds = new Set(agents.map((a) => a.id));
        const toolSpanIds = new Set(tools.map((t) => t.spanId));
        const emptyTokens = () => ({ inputTokens: 0, outputTokens: 0, totalTokens: 0 });
        const addTokens = (acc, next, options) => {
            if (!next)
                return acc;
            acc.inputTokens += Number(next.inputTokens || 0);
            acc.outputTokens += Number(next.outputTokens || 0);
            acc.totalTokens += Number(next.totalTokens || 0);
            // Cache read/write: only count once per model id to avoid double-counting
            const modelId = options?.modelId;
            const cacheSeen = options?.cacheSeenModelIds;
            if (modelId != null && cacheSeen != null) {
                if (!cacheSeen.has(modelId)) {
                    cacheSeen.add(modelId);
                    if (next.cacheRead != null)
                        acc.cacheRead = (acc.cacheRead ?? 0) + Number(next.cacheRead);
                    if (next.cacheWrite != null)
                        acc.cacheWrite = (acc.cacheWrite ?? 0) + Number(next.cacheWrite);
                }
            }
            else if (modelId == null || cacheSeen == null) {
                if (next.cacheRead != null)
                    acc.cacheRead = (acc.cacheRead ?? 0) + Number(next.cacheRead);
                if (next.cacheWrite != null)
                    acc.cacheWrite = (acc.cacheWrite ?? 0) + Number(next.cacheWrite);
            }
            return acc;
        };
        const extractTtft = (model, span) => {
            const fromModel = model?.ttft;
            if (typeof fromModel === 'number' && Number.isFinite(fromModel))
                return fromModel;
            const attrs = span?.attributes ?? {};
            const raw = attrs['gen_ai.server.time_to_first_token'] ?? attrs['genai.server.time_to_first_token'];
            if (raw === undefined || raw === null)
                return undefined;
            const n = parseFloat(String(raw));
            return Number.isFinite(n) ? n : undefined;
        };
        const nsToMs = (ns) => typeof ns === 'number' && Number.isFinite(ns) ? ns / 1000000 : 0;
        const findNearestAncestor = (startParent, predicate) => {
            let cur = startParent;
            let guard = 0;
            const seen = new Set();
            while (cur && guard < 100) {
                if (predicate(cur))
                    return cur;
                if (seen.has(cur))
                    return null;
                seen.add(cur);
                const s = spanById.get(cur);
                cur = s?.parentSpanId ?? null;
                guard++;
            }
            return null;
        };
        const agentRollup = new Map();
        const toolRollup = new Map();
        const getRollup = (m, id) => {
            const existing = m.get(id);
            if (existing)
                return existing;
            const r = { ttft: [], durationMs: 0, tokens: emptyTokens(), cacheSeenModelIds: new Set() };
            m.set(id, r);
            return r;
        };
        const modelIdsRolledToAgent = new Set();
        for (const model of models) {
            const span = spanById.get(model.spanId);
            const parentId = span?.parentSpanId ?? null;
            const agentId = findNearestAncestor(parentId, (sid) => agentIds.has(sid));
            const toolId = findNearestAncestor(parentId, (sid) => toolSpanIds.has(sid));
            const ttft = extractTtft(model, span);
            const durationMs = nsToMs(span?.duration);
            const tokenUsage = model?.tokenUsage;
            if (agentId) {
                modelIdsRolledToAgent.add(model.spanId);
                const r = getRollup(agentRollup, agentId);
                if (ttft !== undefined)
                    r.ttft.push(ttft);
                r.durationMs += durationMs;
                addTokens(r.tokens, tokenUsage, { modelId: model.id, cacheSeenModelIds: r.cacheSeenModelIds });
            }
            if (toolId) {
                const r = getRollup(toolRollup, toolId);
                if (ttft !== undefined)
                    r.ttft.push(ttft);
                r.durationMs += durationMs;
                addTokens(r.tokens, tokenUsage, { modelId: model.id, cacheSeenModelIds: r.cacheSeenModelIds });
            }
        }
        if (framework === 'llamaindex') {
            this.rollupModelsToAgentsLlamaIndex(spans, agents, models, spanById, agentIds, modelIdsRolledToAgent, (agentId, model, ttft, durationMs, tokenUsage) => {
                const r = getRollup(agentRollup, agentId);
                if (ttft !== undefined)
                    r.ttft.push(ttft);
                r.durationMs += durationMs;
                addTokens(r.tokens, tokenUsage, { modelId: model.id, cacheSeenModelIds: r.cacheSeenModelIds });
            }, extractTtft, nsToMs);
        }
        const hasRollupData = (r) => !!r &&
            (r.ttft.length > 0 ||
                r.durationMs > 0 ||
                !!((r.tokens.inputTokens ?? 0) ||
                    (r.tokens.outputTokens ?? 0) ||
                    (r.tokens.totalTokens ?? 0)));
        const enrichedAgents = agents.map((a) => {
            const r = agentRollup.get(a.id);
            const useFramework = hasRollupData(r);
            return {
                ...a,
                ttft: useFramework ? r.ttft : (a.ttft ?? []),
                cumulativeDurationMs: useFramework ? r.durationMs : (a.cumulativeDurationMs ?? 0),
                cumulativeTokenUsage: useFramework ? r.tokens : (a.cumulativeTokenUsage ?? emptyTokens()),
            };
        });
        const enrichedTools = tools.map((t) => {
            const r = toolRollup.get(t.spanId);
            const useFramework = hasRollupData(r);
            return {
                ...t,
                ttft: useFramework ? r.ttft : (t.ttft ?? []),
                cumulativeDurationMs: useFramework ? r.durationMs : (t.cumulativeDurationMs ?? 0),
                cumulativeTokenUsage: useFramework ? r.tokens : (t.cumulativeTokenUsage ?? emptyTokens()),
            };
        });
        return { agents: enrichedAgents, tools: enrichedTools };
    }
}
exports.MultiFrameworkParser = MultiFrameworkParser;
// Export default instance
exports.default = MultiFrameworkParser;
//# sourceMappingURL=frameworkParser.js.map