{"version":3,"file":"viz-Cvb81rMm.mjs","names":[],"sources":["../src/viz/graph-model.ts","../src/viz/data-extractor.ts","../src/viz/index.ts"],"sourcesContent":["import * as z from 'zod'\n\nexport const EntityType = z.enum(['eoa', 'contract', 'exchange', 'mixer', 'unknown'])\nexport type EntityType = z.infer<typeof EntityType>\n\nexport const RiskLevel = z.enum(['low', 'medium', 'high', 'critical', 'unscored', 'unknown'])\nexport type RiskLevel = z.infer<typeof RiskLevel>\n\nexport const GraphNode = z.object({\n  id:         z.string().min(1),\n  label:      z.string().optional(),\n  entityType: EntityType.default('unknown'),\n  riskLevel:  RiskLevel.default('unknown'),\n  totalIn:    z.number().default(0),\n  totalOut:   z.number().default(0),\n  txCount:    z.number().int().default(0),\n  firstSeen:  z.string().optional(),\n  lastSeen:   z.string().optional(),\n})\nexport type GraphNode = z.infer<typeof GraphNode>\n\nexport const GraphEdge = z.object({\n  source:      z.string().min(1),\n  target:      z.string().min(1),\n  value:       z.number(),\n  txHash:      z.string().optional(),\n  blockNumber: z.number().int().optional(),\n  timestamp:   z.string().optional(),\n})\nexport type GraphEdge = z.infer<typeof GraphEdge>\n\nexport const GraphData = z.object({\n  nodes: z.array(GraphNode),\n  edges: z.array(GraphEdge),\n  metadata: z.object({\n    title:       z.string().default('Money Flow'),\n    generatedAt: z.string(),\n    truncated:   z.boolean().default(false),\n    totalNodes:  z.number().int().optional(),\n    hiddenNodes: z.number().int().optional(),\n  }),\n})\nexport type GraphData = z.infer<typeof GraphData>\n\nconst MAX_NODES = 100\n\nexport function truncateGraph(data: GraphData): GraphData {\n  if (data.nodes.length <= MAX_NODES) return data\n  const sorted = [...data.nodes].sort((a, b) => (b.totalIn + b.totalOut) - (a.totalIn + a.totalOut))\n  const kept = sorted.slice(0, MAX_NODES)\n  const keptIds = new Set(kept.map(n => n.id))\n  const filteredEdges = data.edges.filter(e => keptIds.has(e.source) && keptIds.has(e.target))\n  return {\n    nodes: kept,\n    edges: filteredEdges,\n    metadata: {\n      ...data.metadata,\n      truncated: true,\n      totalNodes: data.nodes.length,\n      hiddenNodes: data.nodes.length - MAX_NODES,\n    },\n  }\n}\n","import { readFile } from 'node:fs/promises'\nimport { GraphData, GraphNode, GraphEdge } from './graph-model.js'\n\n/**\n * Extracts all items from ```json code blocks in a markdown string.\n * If the parsed value is an array, spreads all items.\n * If the parsed value has 'nodes' and 'edges', wraps it as a single item.\n * Returns empty array if no JSON blocks found or parsing fails.\n */\nexport function parseEvidenceJson(markdown: string): unknown[] {\n  const results: unknown[] = []\n  const re = /```json\\s*\\n([\\s\\S]*?)```/g\n  let match: RegExpExecArray | null\n  while ((match = re.exec(markdown)) !== null) {\n    const raw = match[1]\n    if (!raw) continue\n    try {\n      const parsed: unknown = JSON.parse(raw)\n      if (Array.isArray(parsed)) {\n        results.push(...parsed)\n      } else if (\n        parsed !== null &&\n        typeof parsed === 'object' &&\n        'nodes' in (parsed as object) &&\n        'edges' in (parsed as object)\n      ) {\n        // Full GraphData object embedded in evidence\n        results.push(parsed)\n      }\n      // Ignore other object types\n    } catch {\n      // Malformed JSON in evidence — skip gracefully (T-04-06: no crash)\n    }\n  }\n  if (results.length > 0) return results\n\n  const rawJson = extractEmbeddedJson(markdown)\n  if (rawJson) {\n    try {\n      const parsed: unknown = JSON.parse(rawJson)\n      if (Array.isArray(parsed)) return parsed\n      if (parsed !== null && typeof parsed === 'object') return [parsed]\n    } catch {\n      // Ignore non-JSON evidence bodies.\n    }\n  }\n  return results\n}\n\nfunction extractEmbeddedJson(text: string): string | null {\n  const trimmed = text.trim()\n  const start = [...trimmed]\n    .map((char, index) => (char === '{' || char === '[' ? index : -1))\n    .find(index => index >= 0)\n  if (start === undefined) return null\n  return trimmed.slice(start)\n}\n\ntype SimpleTx = { from: string; to: string; value: number; txHash?: string; blockNumber?: number; timestamp?: string }\n\ntype CompactEvidence = {\n  schema?: string\n  outgoing_flows?: Array<{\n    src?: string\n    dst?: string\n    amount_usd_sum?: number\n    tx_count?: number\n    first_tx_id?: string\n  }>\n}\n\nfunction isSimpleTx(item: unknown): item is SimpleTx {\n  return (\n    item !== null &&\n    typeof item === 'object' &&\n    'from' in (item as object) &&\n    'to' in (item as object) &&\n    'value' in (item as object)\n  )\n}\n\nfunction isGraphDataLike(input: unknown): input is { nodes: unknown[]; edges: unknown[] } {\n  return (\n    input !== null &&\n    typeof input === 'object' &&\n    Array.isArray((input as Record<string, unknown>)['nodes']) &&\n    Array.isArray((input as Record<string, unknown>)['edges'])\n  )\n}\n\nfunction compactEvidenceToSimpleTxs(item: unknown): SimpleTx[] {\n  const compact = item as CompactEvidence\n  if (\n    !compact ||\n    typeof compact !== 'object' ||\n    compact.schema !== 'chain-insights.compact_evidence.v1' ||\n    !Array.isArray(compact.outgoing_flows)\n  ) {\n    return []\n  }\n\n  return compact.outgoing_flows\n    .filter(flow => typeof flow.src === 'string' && typeof flow.dst === 'string' && typeof flow.amount_usd_sum === 'number')\n    .map(flow => ({\n      from: flow.src!,\n      to: flow.dst!,\n      value: flow.amount_usd_sum!,\n      txHash: flow.first_tx_id,\n    }))\n}\n\n/**\n * Converts simple [{from, to, value}] transaction arrays into graph nodes.\n * Computes totalIn, totalOut, txCount per node from edges.\n */\nfunction buildGraphFromSimpleTxs(items: SimpleTx[]): { nodes: GraphNode[]; edges: GraphEdge[] } {\n  // Build edges first\n  const edges: GraphEdge[] = items.map(tx => GraphEdge.parse({\n    source: tx.from,\n    target: tx.to,\n    value: tx.value,\n    txHash: tx.txHash,\n    blockNumber: tx.blockNumber,\n    timestamp: tx.timestamp,\n  }))\n\n  // Collect unique addresses\n  const addresses = new Set<string>()\n  for (const tx of items) {\n    addresses.add(tx.from)\n    addresses.add(tx.to)\n  }\n\n  // Compute totals per address\n  const totals: Record<string, { totalIn: number; totalOut: number; txCount: number }> = {}\n  for (const addr of addresses) {\n    totals[addr] = { totalIn: 0, totalOut: 0, txCount: 0 }\n  }\n  for (const tx of items) {\n    const out = totals[tx.from]\n    const inp = totals[tx.to]\n    if (out) {\n      out.totalOut += tx.value\n      out.txCount += 1\n    }\n    if (inp) {\n      inp.totalIn += tx.value\n      inp.txCount += 1\n    }\n  }\n\n  const nodes: GraphNode[] = [...addresses].map(addr =>\n    GraphNode.parse({\n      id: addr,\n      entityType: 'unknown',\n      riskLevel: 'unknown',\n      totalIn: totals[addr]?.totalIn ?? 0,\n      totalOut: totals[addr]?.totalOut ?? 0,\n      txCount: totals[addr]?.txCount ?? 0,\n    })\n  )\n\n  return { nodes, edges }\n}\n\n/**\n * Handles two input formats:\n * 1. Full GraphData object (has nodes + edges) — parse with Zod\n * 2. Array of {from, to, value, ...} transaction objects — auto-derive nodes\n *\n * Throws \"Invalid transaction data\" for any other input.\n */\nexport function extractGraphFromJson(input: unknown): GraphData {\n  if (isGraphDataLike(input)) {\n    // Full GraphData: parse and validate through Zod\n    return GraphData.parse(input)\n  }\n\n  if (Array.isArray(input)) {\n    const simpleTxs: SimpleTx[] = []\n    for (const item of input) {\n      if (isSimpleTx(item)) {\n        simpleTxs.push(item)\n      } else {\n        simpleTxs.push(...compactEvidenceToSimpleTxs(item))\n      }\n    }\n    const { nodes, edges } = buildGraphFromSimpleTxs(simpleTxs)\n    return GraphData.parse({\n      nodes,\n      edges,\n      metadata: {\n        title: 'Money Flow',\n        generatedAt: new Date().toISOString(),\n      },\n    })\n  }\n\n  throw new Error(\n    'Invalid transaction data. The input file must contain a JSON array of transaction objects with `from`, `to`, and `value` fields.'\n  )\n}\n\n/**\n * Merges two sets of nodes, deduplicating by id.\n * For duplicate nodes, sums totalIn, totalOut, txCount; keeps earliest firstSeen, latest lastSeen.\n */\nfunction mergeNodes(existing: GraphNode[], incoming: GraphNode[]): GraphNode[] {\n  const map = new Map<string, GraphNode>()\n  for (const node of existing) {\n    map.set(node.id, { ...node })\n  }\n  for (const node of incoming) {\n    const prev = map.get(node.id)\n    if (prev) {\n      map.set(node.id, {\n        ...prev,\n        totalIn: prev.totalIn + node.totalIn,\n        totalOut: prev.totalOut + node.totalOut,\n        txCount: prev.txCount + node.txCount,\n        firstSeen: pickEarlier(prev.firstSeen, node.firstSeen),\n        lastSeen: pickLater(prev.lastSeen, node.lastSeen),\n      })\n    } else {\n      map.set(node.id, { ...node })\n    }\n  }\n  return [...map.values()]\n}\n\nfunction pickEarlier(a: string | undefined, b: string | undefined): string | undefined {\n  if (!a) return b\n  if (!b) return a\n  return a < b ? a : b\n}\n\nfunction pickLater(a: string | undefined, b: string | undefined): string | undefined {\n  if (!a) return b\n  if (!b) return a\n  return a > b ? a : b\n}\n\n/**\n * Aggregates edges: for duplicate (source, target) pairs, sums value; keeps last txHash/timestamp.\n */\nfunction aggregateEdges(edges: GraphEdge[]): GraphEdge[] {\n  const map = new Map<string, GraphEdge>()\n  for (const edge of edges) {\n    const key = `${edge.source}::${edge.target}`\n    const prev = map.get(key)\n    if (prev) {\n      map.set(key, { ...edge, value: prev.value + edge.value })\n    } else {\n      map.set(key, { ...edge })\n    }\n  }\n  return [...map.values()]\n}\n\n/**\n * Reads evidence files from a case directory, extracts JSON transaction data\n * from markdown code blocks, enriches nodes with entity types from dossiers,\n * and returns a merged GraphData.\n */\nexport const DataExtractor = {\n  extractGraphFromJson,\n  parseEvidenceJson,\n}\n","export { GraphData, GraphNode, GraphEdge, EntityType, RiskLevel, truncateGraph } from './graph-model.js'\nexport type { GraphData as GraphDataType, GraphNode as GraphNodeType, GraphEdge as GraphEdgeType } from './graph-model.js'\nexport { generateHtml, writeVizHtml, transformToGraphHtml } from './html-generator.js'\nexport { DataExtractor, extractGraphFromJson } from './data-extractor.js'\n\nimport { readFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport { truncateGraph } from './graph-model.js'\nimport { extractGraphFromJson } from './data-extractor.js'\nimport { generateHtml, writeVizHtml } from './html-generator.js'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\n\nfunction sanitizeSourceId(sourceId: string): string {\n  if (!/^[A-Za-z0-9._-]+$/.test(sourceId) || sourceId.includes('..')) {\n    throw new Error(`Invalid visualization source ID: ${sourceId}`)\n  }\n  return sourceId\n}\n\nexport async function generateVisualization(opts: {\n  sourceId?: string\n  dataFile?: string\n}): Promise<{ vizId: string; htmlPath: string }> {\n  let rawData: unknown\n  let vizId: string\n  let title: string\n\n  if (opts.dataFile) {\n    const content = await readFile(opts.dataFile, 'utf-8')\n    let parsed: unknown\n    try {\n      parsed = JSON.parse(content)\n    } catch {\n      throw new Error('Invalid transaction data. The input file must contain a JSON array of transaction objects with `from`, `to`, and `value` fields.')\n    }\n    rawData = extractGraphFromJson(parsed)\n    vizId = `adhoc_${Date.now()}`\n    title = 'Ad-hoc Visualization'\n  } else if (opts.sourceId) {\n    const sourceId = sanitizeSourceId(opts.sourceId)\n    const paths = workspaceOutputPaths()\n    const graphPath = path.join(paths.reportGraphsRoot, `${sourceId}.graph.json`)\n    const content = await readFile(graphPath, 'utf-8')\n    rawData = JSON.parse(content) as unknown\n    vizId = sourceId\n    title = `${sourceId} - Workspace Graph`\n  } else {\n    throw new Error('Provide either a visualization source ID or --data <file.json>')\n  }\n\n  const data = truncateGraph(rawData as Parameters<typeof truncateGraph>[0])\n  const html = generateHtml(data, title)\n  const htmlPath = await writeVizHtml(vizId, html)\n\n  return { vizId, htmlPath }\n}\n"],"mappings":"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"}