chain-insights 0.24.2 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -3
- package/dist/{capabilities-CjpGnrVg.cjs → capabilities-B-M8vAYp.cjs} +8 -16
- package/dist/{capabilities-CY3Ok5ls.mjs → capabilities-ByKP5l0G.mjs} +9 -17
- package/dist/capabilities-ByKP5l0G.mjs.map +1 -0
- package/dist/cli.cjs +124 -23
- package/dist/cli.mjs +124 -23
- package/dist/cli.mjs.map +1 -1
- package/dist/mcp-proxy.cjs +15 -12
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +15 -12
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{public-tools-CpOLkME5.mjs → public-tools-CMI_0y87.mjs} +28 -2
- package/dist/public-tools-CMI_0y87.mjs.map +1 -0
- package/dist/{public-tools-Bou25cki.cjs → public-tools-DDGHRazh.cjs} +28 -1
- package/dist/{tool-visibility-DA4pOuJt.cjs → tool-visibility-BBEbpKt4.cjs} +2 -1
- package/dist/{tool-visibility-wDMzmHU-.mjs → tool-visibility-CJqjmDF5.mjs} +3 -2
- package/dist/tool-visibility-CJqjmDF5.mjs.map +1 -0
- package/docs/graph-tools.md +16 -3
- package/docs/mcp-proxy.md +33 -2
- package/docs/stability.md +4 -0
- package/package.json +1 -1
- package/skills/chain-insights-address-risk/SKILL.md +6 -0
- package/dist/capabilities-CY3Ok5ls.mjs.map +0 -1
- package/dist/public-tools-CpOLkME5.mjs.map +0 -1
- package/dist/tool-visibility-wDMzmHU-.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-tools-CMI_0y87.mjs","names":["isRecord","numberValue"],"sources":["../src/federation/apply-merge.ts","../src/viz/graph-normalizer.ts","../src/investigation/risk-level.ts","../src/workspace/output-root.ts","../src/lib/usage-accumulator.ts","../src/investigation/public-tools.ts"],"sourcesContent":["/**\n * Wires `mergeShardRows` (federation/merge.ts) into the graph read path.\n *\n * graphrag-mcp's thin fan-out mode tags each row with `__shard` and merges\n * nothing (SPEC-2026-07-26-FED-THIN-FANOUT). Callers here derive merge\n * options — `aggregateKeys`, `orderBy`/`limit`, `orderKeyClass` — from the\n * query text they already hold, because those are caller assertions the\n * merge cannot infer by inspecting rows (spec A7/D3).\n *\n * The query-text parsing below is deliberately narrow: it handles a single\n * flat `RETURN a, b AS c, agg(x) AS y ... ORDER BY <key> [ASC|DESC] LIMIT n`\n * shape, which is what every caller in this codebase emits. It is not a\n * general Cypher/GQL parser. Anything it cannot confidently classify is\n * treated as non-aggregate / merge-affected — the safer default — rather\n * than guessed.\n */\n\nimport { mergeShardRows, type MergedResult, type MergeOptions } from './merge.js'\n\nconst SHARD_KEY = '__shard'\n\n// Node properties that are shard-invariant: identical for the same node\n// regardless of which shard produced the row, so sorting by them survives\n// merging exactly (SPEC D3).\nconst INVARIANT_ORDER_PROPERTIES = new Set([\n 'address',\n 'network',\n 'labels',\n 'is_exchange',\n 'risk_score',\n 'risk_level',\n 'identity_id',\n 'label_risk',\n])\n\nconst AGGREGATE_FN_RE = /^(count|sum|avg|min|max)\\s*\\(/i\n\ninterface ProjectionItem {\n expr: string\n alias: string\n}\n\n/** Splits `input` on `sep` at paren-depth 0 only, so `sum(a, b)` is not\n * split on its internal comma. */\nfunction splitTopLevel(input: string, sep: string): string[] {\n const parts: string[] = []\n let depth = 0\n let current = ''\n for (const ch of input) {\n if (ch === '(') depth += 1\n if (ch === ')') depth -= 1\n if (ch === sep && depth === 0) {\n parts.push(current)\n current = ''\n } else {\n current += ch\n }\n }\n parts.push(current)\n return parts\n}\n\n/** Extracts the text between `\\bKEYWORD\\b` and the earliest of `stopWords`\n * (or end of string). Returns null if `keyword` is not present. Matching is\n * case-insensitive and keyword-boundary based, deliberately not aware of\n * string literals — query builders in this codebase never put ORDER BY /\n * LIMIT / RETURN text inside a quoted string literal. */\nfunction extractClause(query: string, keyword: string, stopWords: string[]): string | null {\n const startRe = new RegExp(`\\\\b${keyword}\\\\b`, 'i')\n const match = startRe.exec(query)\n if (!match) return null\n const rest = query.slice(match.index + match[0].length)\n let end = rest.length\n for (const stop of stopWords) {\n const stopMatch = new RegExp(`\\\\b${stop}\\\\b`, 'i').exec(rest)\n if (stopMatch && stopMatch.index < end) end = stopMatch.index\n }\n return rest.slice(0, end).trim()\n}\n\nfunction parseProjection(query: string): ProjectionItem[] {\n const clause = extractClause(query, 'RETURN', ['ORDER BY', 'LIMIT', 'SKIP'])\n if (!clause) return []\n return splitTopLevel(clause, ',')\n .map((raw) => raw.trim())\n .filter((item) => item.length > 0)\n .map((item) => {\n const asMatch = /^(.*)\\bAS\\b\\s*(\\S+)$/i.exec(item)\n if (asMatch) return { expr: asMatch[1].trim(), alias: asMatch[2].trim() }\n return { expr: item, alias: item }\n })\n}\n\nfunction bareProperty(expr: string): string {\n const trimmed = expr.trim()\n const dot = trimmed.lastIndexOf('.')\n return dot >= 0 ? trimmed.slice(dot + 1) : trimmed\n}\n\n/**\n * Derives `MergeOptions` from a single query's text: aggregate aliases (so\n * they land in `perShard` instead of being summed as plain row values),\n * `orderBy`/`limit` from ORDER BY / LIMIT, and `orderKeyClass` from whether\n * the sort key is a shard-invariant node property.\n */\nexport function deriveMergeOptionsFromQuery(query: string): MergeOptions {\n const projections = parseProjection(query)\n const aggregateKeys = projections.filter((p) => AGGREGATE_FN_RE.test(p.expr)).map((p) => p.alias)\n\n const options: MergeOptions = {}\n if (aggregateKeys.length > 0) options.aggregateKeys = aggregateKeys\n\n const orderClause = extractClause(query, 'ORDER BY', ['LIMIT', 'SKIP'])\n if (orderClause) {\n // Narrow parsing: only the first sort key of a (possibly multi-key)\n // ORDER BY is honored — mergeShardRows itself only supports one.\n const firstKeyRaw = splitTopLevel(orderClause, ',')[0]?.trim() ?? ''\n const desc = /\\bDESC\\b/i.test(firstKeyRaw)\n const exprText = firstKeyRaw.replace(/\\b(ASC|DESC)\\b/gi, '').trim()\n const matched = projections.find((p) => p.expr === exprText || p.alias === exprText)\n const resolvedKey = matched ? matched.alias : exprText\n // If the sort key IS an aggregate alias, it will not exist on merged\n // rows (mergeShardRows lifts aggregate columns into perShard) — sorting\n // by it would be sorting by `undefined`. Skip orderBy entirely rather\n // than guess: this yields `ordering: 'unordered'`, which is honest.\n if (resolvedKey && !aggregateKeys.includes(resolvedKey)) {\n options.orderBy = { key: resolvedKey, desc }\n const bareKey = bareProperty(matched ? matched.expr : exprText)\n options.orderKeyClass = INVARIANT_ORDER_PROPERTIES.has(bareKey)\n ? 'invariant'\n : 'merge-affected'\n }\n }\n\n const limitMatch = /\\bLIMIT\\s+(\\d+)/i.exec(query)\n if (limitMatch) options.limit = Number(limitMatch[1])\n\n return options\n}\n\nexport interface GraphRowMergeResult extends MergedResult {\n /** False when no row carried `__shard` — the response was already a\n * single-shard / non-federated result. `rows` is then the SAME array\n * reference the caller passed in, unmodified: a true no-op. */\n merged: boolean\n}\n\n/**\n * Merges `rows` via `mergeShardRows` if (and only if) at least one row\n * carries `__shard`. This is the flag-off no-op guard: a non-federated\n * response passes through byte-identical, `perShard` empty, `ordering`\n * `'unordered'`.\n */\nexport function mergeGraphRows(\n rows: Array<Record<string, unknown>>,\n query: string\n): GraphRowMergeResult {\n const isSharded = rows.some((row) => typeof row[SHARD_KEY] === 'string')\n if (!isSharded) {\n return { rows, perShard: {}, ordering: 'unordered', merged: false }\n }\n const options = deriveMergeOptionsFromQuery(query)\n const result = mergeShardRows(rows, options)\n return { ...result, merged: true }\n}\n\nexport interface BatchQueryEntry {\n id?: string\n ok?: boolean\n results?: Array<Record<string, unknown>>\n error?: string\n perShard?: Record<string, Array<Record<string, unknown>>>\n ordering?: 'exact' | 'approximate' | 'unordered'\n}\n\n/**\n * Mutates a parsed `graph_query_batch` response's `facts.queries` entries in\n * place: merges shard-tagged `results`, strips `__shard`, and attaches\n * `perShard`/`ordering` onto the entry so a caller that reads the raw batch\n * entry can see them (SPEC D2/D4 — the client merges, the client never\n * filters). `queries` is the ORIGINAL (pre-`USE topology` prefix) query list\n * sent for this batch, keyed by id, used to derive merge options; text\n * before/after the `USE` prefix does not affect RETURN/ORDER BY/LIMIT\n * parsing, so either form works.\n */\nexport function applyShardMergeToBatchEntries(\n entries: BatchQueryEntry[] | undefined,\n queries: Array<{ id: string; query: string }>\n): void {\n if (!entries || entries.length === 0) return\n const queryById = new Map(queries.map((q) => [q.id, q.query]))\n for (const entry of entries) {\n if (!entry || entry.ok === false || !entry.results || entry.results.length === 0) continue\n const queryText = entry.id !== undefined ? queryById.get(entry.id) : undefined\n if (queryText === undefined) continue\n const merged = mergeGraphRows(entry.results, queryText)\n if (!merged.merged) continue\n entry.results = merged.rows\n if (Object.keys(merged.perShard).length > 0) entry.perShard = merged.perShard\n entry.ordering = merged.ordering\n }\n}\n","const GRAPH_TYPE_LABELS = new Set([\n 'Address',\n 'Exchange',\n 'Miner',\n 'Validator',\n 'Hotkey',\n 'Subnet',\n 'IPAddress',\n])\nconst FLOW_EDGE_TYPES = new Set(['flows_to', 'transfer', 'transfer_to', 'transfers_to'])\n\ntype GraphRecord = Record<string, unknown>\n\nexport type NormalizedGraphPayload = {\n schema: 'chain-insights.graph.v1'\n nodes: GraphRecord[]\n edges: GraphRecord[]\n flows: unknown[]\n edge_anchors: unknown[]\n [key: string]: unknown\n}\n\nfunction isRecord(value: unknown): value is GraphRecord {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction stringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.map(String).filter(Boolean) : []\n}\n\nfunction unique(values: string[]): string[] {\n return [...new Set(values)]\n}\n\nfunction lowerSnake(value: string): string {\n return value\n .trim()\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/[^A-Za-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase()\n}\n\nfunction isGraphTypeLabel(label: string): boolean {\n return GRAPH_TYPE_LABELS.has(label)\n}\n\nfunction displayLabels(node: GraphRecord): string[] {\n return unique(stringArray(node['labels'])).filter((label) => !isGraphTypeLabel(label))\n}\n\nfunction systemLabels(node: GraphRecord): string[] {\n return unique([\n ...stringArray(node['system_labels']),\n ...stringArray(node['graph_labels']),\n ...stringArray(node['raw_labels']),\n ...stringArray(node['labels']).filter(isGraphTypeLabel),\n ])\n}\n\nfunction normalizeRoles(node: GraphRecord): string[] {\n const roles = stringArray(node['roles'])\n const role = typeof node['role'] === 'string' ? node['role'] : ''\n if (role === 'source_exchange') roles.push('exchange')\n else if (role) roles.push(role)\n\n const display = stringArray(node['labels']).map(lowerSnake)\n if (\n systemLabels(node).some((label) => lowerSnake(label) === 'exchange') ||\n display.includes('exchange')\n ) {\n roles.push('exchange')\n }\n\n return unique(roles.map(lowerSnake).filter(Boolean))\n}\n\nfunction normalizeNodeType(node: GraphRecord): string {\n if (typeof node['node_type'] === 'string' && node['node_type'].trim()) {\n return lowerSnake(node['node_type'])\n }\n if (typeof node['address'] === 'string' || typeof node['id'] === 'string') return 'address'\n const labels = systemLabels(node)\n if (labels.length > 0) return lowerSnake(labels[0]!)\n return 'unknown'\n}\n\nfunction normalizeNode(node: unknown): GraphRecord {\n if (!isRecord(node)) return {}\n\n const normalized: GraphRecord = {}\n for (const [key, value] of Object.entries(node)) {\n if (\n [\n 'address_subtypes',\n 'entity_kind',\n 'graph_labels',\n 'labels',\n 'node_type',\n 'pattern_flags',\n 'raw_labels',\n 'role',\n 'roles',\n 'system_labels',\n 'type',\n ].includes(key)\n ) {\n continue\n }\n normalized[key] = value\n }\n\n const id =\n typeof node['id'] === 'string'\n ? node['id']\n : typeof node['address'] === 'string'\n ? node['address']\n : undefined\n if (id) normalized['id'] = id\n\n const address = typeof node['address'] === 'string' ? node['address'] : id\n if (address) normalized['address'] = address\n\n normalized['node_type'] = normalizeNodeType(node)\n normalized['labels'] = displayLabels(node)\n\n const addressSubtypes = unique(stringArray(node['address_subtypes']))\n if (addressSubtypes.length > 0) normalized['address_subtypes'] = addressSubtypes\n\n const roles = normalizeRoles(node)\n if (roles.length > 0) normalized['roles'] = roles\n\n if (\n !Array.isArray(node['flags']) &&\n Array.isArray(node['pattern_flags']) &&\n node['pattern_flags'].length > 0\n ) {\n normalized['flags'] = node['pattern_flags'].map(String)\n }\n\n return normalized\n}\n\nfunction normalizeEdgeType(edge: GraphRecord): string {\n const rawType =\n typeof edge['edge_type'] === 'string' && edge['edge_type'].trim()\n ? edge['edge_type']\n : typeof edge['type'] === 'string' && edge['type'].trim()\n ? edge['type']\n : typeof edge['relationship_type'] === 'string' && edge['relationship_type'].trim()\n ? edge['relationship_type']\n : 'related_to'\n const edgeType = lowerSnake(rawType)\n return FLOW_EDGE_TYPES.has(edgeType) ? 'flows_to' : edgeType\n}\n\nfunction normalizeEdge(edge: unknown): GraphRecord {\n if (!isRecord(edge)) return {}\n\n const normalized: GraphRecord = {}\n for (const [key, value] of Object.entries(edge)) {\n if (['edge_type', 'from_address', 'relationship_type', 'to_address', 'type'].includes(key))\n continue\n normalized[key] = value\n }\n\n if (typeof normalized['source'] !== 'string' && typeof edge['from_address'] === 'string') {\n normalized['source'] = edge['from_address']\n }\n if (typeof normalized['target'] !== 'string' && typeof edge['to_address'] === 'string') {\n normalized['target'] = edge['to_address']\n }\n normalized['edge_type'] = normalizeEdgeType(edge)\n\n return normalized\n}\n\nexport function normalizeGraphPayload(payload: unknown): NormalizedGraphPayload {\n if (!isRecord(payload) || payload['schema'] !== 'chain-insights.graph.v1') {\n throw new Error('Unsupported graph payload schema')\n }\n\n return {\n ...payload,\n schema: 'chain-insights.graph.v1',\n nodes: Array.isArray(payload['nodes']) ? payload['nodes'].map(normalizeNode) : [],\n edges: Array.isArray(payload['edges']) ? payload['edges'].map(normalizeEdge) : [],\n flows: Array.isArray(payload['flows']) ? payload['flows'] : [],\n edge_anchors: Array.isArray(payload['edge_anchors']) ? payload['edge_anchors'] : [],\n }\n}\n","/**\n * Risk-level normalization shared by the AML tools and graph payload builders.\n *\n * The Chain Insights Graph serves the ML verdict verbatim, which historically\n * mixed cases (HIGH vs high) and — since the calibrated-scoring release —\n * includes an explicit abstention band: risk_level = UNSCORED means the model\n * had too little labeled graph context to stand behind a band. UNSCORED is a\n * \"no stance\" state and must never be rendered or reasoned about as low risk.\n */\n\nexport const RISK_SEVERITIES = ['low', 'medium', 'high', 'critical'] as const\nexport const UNSCORED_RISK_LEVEL = 'unscored'\n\n/** Lowercased, trimmed risk level; undefined for empty/unrecognized values. */\nexport function normalizeRiskLevel(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const normalized = value.trim().toLowerCase()\n if (!normalized) return undefined\n if (normalized === UNSCORED_RISK_LEVEL) return UNSCORED_RISK_LEVEL\n if ((RISK_SEVERITIES as readonly string[]).includes(normalized)) return normalized\n return undefined\n}\n\n/** True when the ML verdict explicitly abstained (no stance, not low). */\nexport function isUnscoredRiskLevel(value: unknown): boolean {\n return normalizeRiskLevel(value) === UNSCORED_RISK_LEVEL\n}\n\n/**\n * Severity ordering for comparisons: low(0) < medium(1) < high(2) <\n * critical(3). Unknown/unscored values rank -1 — they never win a\n * severity comparison, they abstain from it.\n */\nexport function riskSeverityRank(value: unknown): number {\n const normalized = normalizeRiskLevel(value)\n if (!normalized || normalized === UNSCORED_RISK_LEVEL) return -1\n return (RISK_SEVERITIES as readonly string[]).indexOf(normalized)\n}\n","import path from 'node:path'\nimport { findActiveWorkspace } from './active.js'\n\nexport const NO_WORKSPACE_ERROR = 'No Chain Insights workspace found. Run: cia init .'\n\nexport interface WorkspaceOutputPaths {\n root: string\n metadataDir: string\n schemaDir: string\n runtimeDir: string\n artifactsRoot: string\n entitiesRoot: string\n sessionsRoot: string\n publishedRoot: string\n reportsRoot: string\n reportGraphsRoot: string\n reportTablesRoot: string\n logsRoot: string\n}\n\nexport function requireWorkspaceRoot(startDir = process.cwd()): string {\n const workspace = findActiveWorkspace(startDir)\n if (!workspace) throw new Error(NO_WORKSPACE_ERROR)\n return workspace.root\n}\n\nexport function workspaceOutputPaths(workspaceRoot = requireWorkspaceRoot()): WorkspaceOutputPaths {\n const root = path.resolve(workspaceRoot)\n return {\n root,\n metadataDir: path.join(root, '.chain-insights'),\n schemaDir: path.join(root, '.chain-insights', 'schema'),\n runtimeDir: path.join(root, '.chain-insights', 'runtime'),\n artifactsRoot: path.join(root, 'artifacts'),\n entitiesRoot: path.join(root, 'entities'),\n sessionsRoot: path.join(root, 'sessions'),\n publishedRoot: path.join(root, 'published'),\n reportsRoot: path.join(root, 'reports'),\n reportGraphsRoot: path.join(root, 'reports', 'graphs'),\n reportTablesRoot: path.join(root, 'reports', 'tables'),\n logsRoot: path.join(root, '.chain-insights', 'runtime', 'logs'),\n }\n}\n","/**\n * Per-workflow billable-units accounting for the aml_* tools (internal feature:\n * usage-units-propagation). The `aml_*` workflow tools make MANY internal\n * graph_query_batch round trips to answer one high-level question; this\n * module totals what those round trips billed so the workflow response can\n * report a single `usage` block instead of exposing nothing at all.\n *\n * Capture point: aml_address_risk (and internal graph_query_batch helper\n * calls feeding its enrichment) wraps its `remoteClient` once via\n * `wrapClientForUsageTracking` before making any internal calls, then reads\n * the shared `UsageTotals` back out when building its response. Every\n * internal helper (callGraphBatch, probeSeedAddresses, ...) keeps calling\n * `client.callTool(...)` exactly as before -- the wrapper observes\n * graph_query_batch/graph_query responses in transit and never changes their\n * return value.\n *\n * Defensive by design: a backend that does not yet emit billing fields (or\n * an unparsable response) contributes 0 units but the call still counts\n * toward `query_count` -- this must never throw and never block a workflow.\n */\n\ninterface CallToolLike {\n callTool(...args: unknown[]): Promise<unknown>\n}\n\nexport interface UsageTotals {\n billable_units: number\n query_count: number\n truncated_queries: number\n}\n\nexport function createUsageAccumulator(): UsageTotals {\n return { billable_units: 0, query_count: 0, truncated_queries: 0 }\n}\n\n// Snapshot as a plain object for embedding in a response -- callers should\n// not hand out the live mutable accumulator.\nexport function usageBlock(totals: UsageTotals): UsageTotals {\n return {\n billable_units: totals.billable_units,\n query_count: totals.query_count,\n truncated_queries: totals.truncated_queries,\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction numberValue(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim()) {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : undefined\n }\n return undefined\n}\n\nfunction textOfToolResult(result: unknown): string {\n if (!isRecord(result)) return ''\n const content = result['content']\n if (!Array.isArray(content)) return ''\n return content\n .filter(\n (item): item is { type: 'text'; text: string } =>\n isRecord(item) && item['type'] === 'text' && typeof item['text'] === 'string'\n )\n .map((item) => item.text)\n .join('\\n')\n}\n\n// facts.query.* is the single-query shape (graph_query). facts.batch.* /\n// facts.queries[] is the batch shape (graph_query_batch). Both are read\n// defensively -- missing/malformed fields never throw, they just contribute\n// nothing to the total.\nfunction accumulateFacts(totals: UsageTotals, facts: unknown): void {\n if (!isRecord(facts)) return\n\n const query = facts['query']\n if (isRecord(query)) {\n const units = numberValue(query['billable_units'])\n if (units !== undefined) totals.billable_units += units\n if (query['truncated'] === true) totals.truncated_queries += 1\n }\n\n const batch = facts['batch']\n const batchUnits = isRecord(batch) ? numberValue(batch['billable_units']) : undefined\n const queries = Array.isArray(facts['queries'])\n ? (facts['queries'] as unknown[]).filter(isRecord)\n : undefined\n\n if (batchUnits !== undefined) {\n totals.billable_units += batchUnits\n } else if (queries) {\n // No batch-level total (older/mocked backend): fall back to summing\n // whatever per-query billable_units are present. Entries with no field\n // contribute 0, same as the single-query defensive path.\n for (const entry of queries) {\n const units = numberValue(entry['billable_units'])\n if (units !== undefined) totals.billable_units += units\n }\n }\n\n if (queries) {\n for (const entry of queries) {\n if (entry['truncated'] === true) totals.truncated_queries += 1\n }\n } else if (isRecord(batch) && batch['truncated'] === true) {\n totals.truncated_queries += 1\n }\n}\n\n// One graph_query_batch round trip is one query_count unit per Cypher\n// statement it carried (facts.queries.length) -- that is what \"one internal\n// graph query\" means for this tool. A round trip whose response could not be\n// attributed to individual statements (parse failure, missing facts.queries)\n// still represents one internal graph query having been issued, so it counts\n// as 1 rather than 0.\nfunction recordGraphQueryBatchResult(totals: UsageTotals, rawResult: unknown): void {\n let facts: unknown\n try {\n const text = textOfToolResult(rawResult).trim()\n if (text) facts = (JSON.parse(text) as { facts?: unknown }).facts\n else if (\n isRecord(rawResult) &&\n isRecord((rawResult as Record<string, unknown>)['structuredContent'])\n ) {\n facts = (\n (rawResult as Record<string, unknown>)['structuredContent'] as Record<string, unknown>\n )['facts']\n }\n } catch {\n facts = undefined\n }\n\n const queries =\n isRecord(facts) && Array.isArray(facts['queries']) ? (facts['queries'] as unknown[]) : undefined\n totals.query_count += queries ? queries.length : 1\n accumulateFacts(totals, facts)\n}\n\n// One graph_query round trip is exactly one internal graph query.\nfunction recordGraphQueryResult(totals: UsageTotals, rawResult: unknown): void {\n totals.query_count += 1\n let facts: unknown\n try {\n if (\n isRecord(rawResult) &&\n isRecord((rawResult as Record<string, unknown>)['structuredContent'])\n ) {\n facts = (\n (rawResult as Record<string, unknown>)['structuredContent'] as Record<string, unknown>\n )['facts']\n } else {\n const text = textOfToolResult(rawResult).trim()\n if (text) facts = (JSON.parse(text) as { facts?: unknown }).facts\n }\n } catch {\n facts = undefined\n }\n accumulateFacts(totals, facts)\n}\n\n// Wraps a Client-shaped object (real MCP SDK Client or a test double) so\n// every graph_query_batch/graph_query call it makes is observed and totaled\n// into `totals`, without changing what the call returns. Any other method\n// or property passes through untouched.\nexport function wrapClientForUsageTracking<T extends CallToolLike>(\n client: T,\n totals: UsageTotals\n): T {\n return new Proxy(client as object, {\n get(target, prop, receiver) {\n if (prop === 'callTool') {\n return async (...args: unknown[]) => {\n const result = await (target as CallToolLike).callTool(...args)\n const request = args[0]\n const toolName =\n isRecord(request) && typeof request['name'] === 'string' ? request['name'] : undefined\n if (toolName === 'graph_query_batch') {\n recordGraphQueryBatchResult(totals, result)\n } else if (toolName === 'graph_query') {\n recordGraphQueryResult(totals, result)\n }\n return result\n }\n }\n const value = Reflect.get(target, prop, target)\n return typeof value === 'function' ? value.bind(target) : value\n },\n }) as T\n}\n","import { createHash } from 'node:crypto'\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport type { ContentBlock } from '@modelcontextprotocol/sdk/types.js'\nimport type { InvestigatorConfig } from '../config/schema.js'\nimport { applyShardMergeToBatchEntries } from '../federation/apply-merge.js'\nimport { normalizeGraphPayload } from '../viz/graph-normalizer.js'\nimport { isUnscoredRiskLevel, normalizeRiskLevel, riskSeverityRank } from './risk-level.js'\nimport { workspaceOutputPaths } from '../workspace/output-root.js'\nimport {\n createUsageAccumulator,\n usageBlock,\n wrapClientForUsageTracking,\n type UsageTotals,\n} from '../lib/usage-accumulator.js'\n\nexport { buildTruthProposal, resolveTruthIngressTlsPaths } from './truth-proposal.js'\nexport type { TruthProposalCommand } from './truth-proposal.js'\n\ntype RemoteToolResult = {\n content?: ContentBlock[]\n isError?: boolean\n}\n\ninterface ParsedGraphBatch {\n facts?: {\n queries?: Array<{\n id?: string\n ok?: boolean\n results?: Array<Record<string, unknown>>\n error?: string\n perShard?: Record<string, Array<Record<string, unknown>>>\n ordering?: 'exact' | 'approximate' | 'unordered'\n }>\n }\n}\n\ntype QueryFailure = {\n id: string\n error: string\n}\n\nconst GRAPH_QUERY_BATCH_TIMEOUT_SECONDS = 10\nconst GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS = 5 * 60 * 1000\n\nexport interface AddressRiskOptions {\n address: string\n network: string\n compareAddress?: string\n writeArtifacts?: boolean\n}\n\nconst AML_ADDRESS_RISK_SUPPORTED_VERSIONS = ['v1'] as const\nexport type AmlAddressRiskVersion = (typeof AML_ADDRESS_RISK_SUPPORTED_VERSIONS)[number]\nexport const LATEST_AML_ADDRESS_RISK_VERSION: AmlAddressRiskVersion = 'v1'\n\nexport function resolveAmlAddressRiskVersion(requested?: string): AmlAddressRiskVersion {\n const normalized = requested?.trim().toLowerCase()\n if (!normalized) return LATEST_AML_ADDRESS_RISK_VERSION\n if (\n AML_ADDRESS_RISK_SUPPORTED_VERSIONS.includes(\n normalized as (typeof AML_ADDRESS_RISK_SUPPORTED_VERSIONS)[number]\n )\n ) {\n return normalized as AmlAddressRiskVersion\n }\n throw new Error(\n `Unsupported aml_address_risk version \"${requested?.trim() ?? ''}\". Supported versions: ${AML_ADDRESS_RISK_SUPPORTED_VERSIONS.join(', ')}.`\n )\n}\n\nfunction escapeCypherString(value: string): string {\n return value.replaceAll('\\\\', '\\\\\\\\').replaceAll('\"', '\\\\\"')\n}\n\nfunction textFromToolResult(result: RemoteToolResult): string {\n return (result.content ?? [])\n .filter((item): item is Extract<ContentBlock, { type: 'text' }> => item.type === 'text')\n .map((item) => item.text)\n .join('\\n')\n}\n\nfunction parseGraphBatchResult(result: RemoteToolResult): ParsedGraphBatch {\n const text = textFromToolResult(result).trim()\n if (!text) throw new Error('graph_query_batch returned no text content')\n const parsed = JSON.parse(text) as ParsedGraphBatch\n if (!parsed.facts?.queries)\n throw new Error('graph_query_batch response did not include facts.queries')\n return parsed\n}\n\nfunction topologyGraphQuery(query: string): string {\n const trimmed = query.trim()\n if (/^USE\\s+/i.test(trimmed)) return trimmed\n return `USE topology ${trimmed}`\n}\n\nfunction collectQueryFailure(\n failures: QueryFailure[],\n id: string,\n error: string | undefined\n): void {\n failures.push({ id, error: error || 'unknown error' })\n}\n\nfunction optionalResultsFor(\n batch: ParsedGraphBatch,\n id: string,\n failures: QueryFailure[]\n): Array<Record<string, unknown>> {\n const query = batch.facts?.queries?.find((entry) => entry.id === id)\n if (!query) return []\n if (query.ok === false) {\n collectQueryFailure(failures, id, query.error)\n return []\n }\n return query.results ?? []\n}\n\nfunction optionalResultsWithPrefix(\n batch: ParsedGraphBatch,\n prefix: string,\n failures: QueryFailure[]\n): Array<Record<string, unknown>> {\n return (batch.facts?.queries ?? [])\n .filter((entry) => entry.id?.startsWith(prefix))\n .flatMap((entry) => {\n if (entry.ok === false) {\n collectQueryFailure(failures, entry.id ?? prefix, entry.error)\n return []\n }\n return entry.results ?? []\n })\n}\n\nasync function callGraphBatch(\n remoteClient: Client,\n network: string,\n queries: Array<{ id: string; query: string }>\n): Promise<ParsedGraphBatch> {\n const result = (await remoteClient.callTool(\n {\n name: 'graph_query_batch',\n arguments: {\n network,\n queries: queries.map((query) => ({\n ...query,\n query: topologyGraphQuery(query.query),\n })),\n per_query_timeout_seconds: GRAPH_QUERY_BATCH_TIMEOUT_SECONDS,\n },\n },\n undefined,\n {\n timeout: GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS,\n maxTotalTimeout: GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS,\n }\n )) as RemoteToolResult\n if (result.isError) throw new Error(textFromToolResult(result) || 'graph_query_batch failed')\n const parsed = parseGraphBatchResult(result)\n applyShardMergeToBatchEntries(parsed.facts?.queries, queries)\n return parsed\n}\n\nfunction parseAddressList(value: string | string[] | undefined): string[] {\n const raw = Array.isArray(value) ? value.join(',') : (value ?? '')\n return raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n}\n\nfunction graphArray(\n graphData: Record<string, unknown>,\n key: string\n): Array<Record<string, unknown>> {\n const value = graphData[key]\n return Array.isArray(value)\n ? value.filter(\n (item): item is Record<string, unknown> =>\n typeof item === 'object' && item !== null && !Array.isArray(item)\n )\n : []\n}\n\nfunction addressProfileQuery(address: string): { id: string; query: string } {\n // risk_score/risk_level are always projected: topology is now the only ML\n // risk source (the retired facts_risk_scores_view never had a risk_level\n // column, so UNSCORED abstention was invisible until this move).\n const riskFields = ', a.risk_score AS live_risk_score, a.risk_level AS live_risk_level'\n // label_risk is the per-label risk overlay graphsync now materializes\n // directly on the node (P2b′): a list of {label, risk_level,\n // updated_timestamp} maps, one per current label row. Missing/empty is \"no\n // label-risk signal,\" never an error -- the retired facts_address_labels_view\n // read is gone; this replaces it entirely.\n const labelRiskField = ', a.label_risk AS label_risk'\n return {\n id: 'address_profile',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})`,\n `RETURN a.address AS address, a.network AS network, a.labels AS display_labels, a.labels AS system_labels, a.is_exchange AS is_exchange${riskFields}${labelRiskField}`,\n 'LIMIT 1',\n ].join(' '),\n }\n}\n\n// Lifetime address features from federated USE topology node-metric\n// projections (was: USE facts HAS_FEATURE -> facts_address_features_view,\n// the view's last reader — internal epic). The federation typed-AST planner\n// (internal epic) re-derives every projected metric EXACTLY across shards:\n// additive props summed over disjoint shard windows, degrees as distinct\n// counterparty set unions, first/last activity as min/max of per-shard baked\n// endpoints, span from the combined endpoints — oracle-verified.\nfunction addressFeatureQuery(address: string): { id: string; query: string } {\n return {\n id: 'address_feature',\n query: [\n 'USE topology',\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})`,\n 'RETURN a.degree_in AS degree_in, a.degree_out AS degree_out, a.degree_total AS degree_total, a.tx_in_count AS tx_in_count, a.tx_out_count AS tx_out_count, a.tx_total_count AS tx_total_count, a.total_volume_usd AS total_volume_usd, a.total_in_usd AS total_in_usd, a.total_out_usd AS total_out_usd, a.net_flow_usd AS net_flow_usd, a.first_activity_timestamp AS first_activity_timestamp, a.last_activity_timestamp AS last_activity_timestamp, a.activity_span_days AS activity_span_days',\n 'LIMIT 1',\n ].join(' '),\n }\n}\n\nfunction flowEdgeMap(variableName: string): string {\n return `{amount_usd_sum: ${variableName}.amount_usd_sum, tx_count: ${variableName}.tx_count, first_seen_timestamp: ${variableName}.first_seen_timestamp, last_seen_timestamp: ${variableName}.last_seen_timestamp, first_tx_id: ${variableName}.first_tx_id, last_tx_id: ${variableName}.last_tx_id}`\n}\n\nfunction pathNodeMap(variableName: string): string {\n // risk_score/risk_level are always projected: topology is now unconditionally\n // Memgraph-backed, so the :Address slim risk verdict is always available.\n const riskFields = `, risk_score: ${variableName}.risk_score, risk_level: ${variableName}.risk_level`\n return `{address: ${variableName}.address, network: ${variableName}.network, labels: ${variableName}.labels, system_labels: ${variableName}.labels, is_exchange: ${variableName}.is_exchange${riskFields}}`\n}\n\n// money-trail enrichment: incident MONEY_TRAIL edges on the subject address\n// (either direction), and the TRAIL_ENDS_AT fan-out of whichever seed\n// generated the trail -- both are optional-results reads, so a graph with no\n// money-trail layer (or a failed query) never fails aml_address_risk itself.\nexport function moneyTrailIncidentQuery(address: string): string {\n return [\n 'USE topology',\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[r:MONEY_TRAIL]-()`,\n 'RETURN r.edge_class AS edge_class, r.value AS value, r.min_hop AS min_hop, r.seed_count AS seed_count, r.primary_seed AS primary_seed, r.generation AS generation, r.network AS network, r.first_ts AS first_ts, r.last_ts AS last_ts',\n 'LIMIT 200',\n ].join(' ')\n}\n\nexport function moneyTrailEndsQuery(seed: string): string {\n return [\n 'USE topology',\n `MATCH (s:Address {address: \"${escapeCypherString(seed)}\"})-[r:TRAIL_ENDS_AT]->(t:Address)`,\n 'RETURN t.address AS address, r.fact_type AS fact_type, r.direction AS direction, r.terminal_role AS terminal_role, r.hop AS hop, r.value AS value, r.generation AS generation',\n 'LIMIT 200',\n ].join(' ')\n}\n\n// Task 2 (trace fast path): a resolved trace target IS a money-trail seed\n// when it has outgoing TRAIL_ENDS_AT edges -- same shape as\n// moneyTrailEndsQuery, reused here under a probe-specific name so the\n// trace-tool call site reads as \"check whether this address is a seed\"\n// rather than \"fetch the ends fan of a known seed\" (they happen to be the\n// same read).\nexport function moneyTrailSeedProbeQuery(address: string): string {\n return moneyTrailEndsQuery(address)\n}\n\n// Precomputed MONEY_TRAIL corridor from a confirmed seed: bounded to 6 hops\n// (min_hop is a property baked onto the edge by the walk engine, not a\n// variable-length path depth) and excludes peripheral-only continuation --\n// peripheral is the \"touched funds\" floor (see MONEY_TRAIL_CLASS_RANK), not\n// a real trail hop, so it never extends the fast-path corridor.\nexport function moneyTrailCorridorQuery(seed: string): string {\n return [\n 'USE topology',\n `MATCH (s:Address {address: \"${escapeCypherString(seed)}\"})-[r:MONEY_TRAIL]->(t:Address)`,\n 'WHERE r.min_hop <= 6 AND r.edge_class <> \"peripheral\"',\n 'RETURN t.address AS address, r.edge_class AS edge_class, r.value AS value, r.min_hop AS min_hop, r.seed_count AS seed_count, r.primary_seed AS primary_seed, r.generation AS generation, r.network AS network',\n 'ORDER BY r.min_hop ASC',\n 'LIMIT 200',\n ].join(' ')\n}\n\nexport interface MoneyTrailBlock {\n on_trail: true\n class: string\n min_hop: number\n primary_seed: string\n generation: number\n nearest_trail_end?: { address: string; fact_type: string; value: string }\n}\n\nconst MONEY_TRAIL_CLASS_RANK: Record<string, number> = { transport: 3, holding: 2, peripheral: 1 }\n\nexport function buildMoneyTrailBlock(\n incidentRows: Array<Record<string, unknown>>,\n endRows: Array<Record<string, unknown>>\n): MoneyTrailBlock | undefined {\n if (incidentRows.length === 0) return undefined\n\n const winningClassRank = Math.max(\n ...incidentRows.map((row) => MONEY_TRAIL_CLASS_RANK[firstString(row['edge_class']) ?? ''] ?? 0)\n )\n const winningClass =\n Object.entries(MONEY_TRAIL_CLASS_RANK).find(([, rank]) => rank === winningClassRank)?.[0] ??\n firstString(incidentRows[0]?.['edge_class']) ??\n 'peripheral'\n const rowsOfWinningClass = incidentRows.filter(\n (row) => firstString(row['edge_class']) === winningClass\n )\n const bestRow = rowsOfWinningClass.reduce((best, row) => {\n const rowHop = numberValue(row['min_hop']) ?? Number.POSITIVE_INFINITY\n const bestHop = numberValue(best['min_hop']) ?? Number.POSITIVE_INFINITY\n return rowHop < bestHop ? row : best\n }, rowsOfWinningClass[0]!)\n\n const nearestEnd = endRows.reduce<Record<string, unknown> | undefined>((best, row) => {\n const rowValue = numberValue(row['value']) ?? Number.NEGATIVE_INFINITY\n const bestValue = best\n ? (numberValue(best['value']) ?? Number.NEGATIVE_INFINITY)\n : Number.NEGATIVE_INFINITY\n return !best || rowValue > bestValue ? row : best\n }, undefined)\n\n return {\n on_trail: true,\n class: winningClass,\n min_hop: numberValue(bestRow['min_hop']) ?? 0,\n primary_seed: firstString(bestRow['primary_seed']) ?? '',\n generation: numberValue(bestRow['generation']) ?? 0,\n ...(nearestEnd\n ? {\n nearest_trail_end: {\n address: firstString(nearestEnd['address']) ?? '',\n fact_type: firstString(nearestEnd['fact_type']) ?? '',\n value: String(nearestEnd['value'] ?? ''),\n },\n }\n : {}),\n }\n}\n\nexport function moneyTrailSummarySentence(block: MoneyTrailBlock): string {\n return block.class === 'peripheral'\n ? `This address touched money-trail funds (${block.class}, min hop ${block.min_hop}).`\n : `This address sits on a money trail (${block.class}, min hop ${block.min_hop}).`\n}\n\nfunction exchangeOutflowQueries(address: string): Array<{ id: string; query: string }> {\n return Array.from({ length: 3 }, (_, index) => exchangeOutflowQueryAtDepth(address, index + 1))\n}\n\nfunction exchangeOutflowQueryAtDepth(\n address: string,\n depth: number\n): { id: string; query: string } {\n const intermediateVariables = Array.from(\n { length: Math.max(depth - 1, 0) },\n (_, index) => `n${index + 1}`\n )\n const nodeVariables = ['a', ...intermediateVariables, 'exchange']\n const edgeVariables = Array.from({ length: depth }, (_, index) => `r${index + 1}`)\n const relationshipChain = edgeVariables\n .map((edgeVariable, index) => {\n const targetVariable =\n index === edgeVariables.length - 1 ? 'exchange' : intermediateVariables[index]!\n return `-[${edgeVariable}:FLOWS_TO]->(${targetVariable}:Address)`\n })\n .join('')\n const intermediatePredicates = intermediateVariables.map(\n (nodeVariable) => `${nodeVariable}.is_exchange IS NULL`\n )\n const depositVariable = nodeVariables[nodeVariables.length - 2]!\n const terminalEdgeVariable = edgeVariables[edgeVariables.length - 1]!\n return {\n id: `exchange_outflows_${depth}`,\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})${relationshipChain}`,\n `WHERE a.address <> exchange.address AND exchange.is_exchange IS NOT NULL${intermediatePredicates.length > 0 ? ` AND ${intermediatePredicates.join(' AND ')}` : ''}`,\n `RETURN \"outflow\" AS direction, exchange.address AS exchange_address, exchange.labels AS exchange_display_labels, exchange.labels AS exchange_system_labels, ${depositVariable}.address AS deposit_address, ${depth} AS hops, ${terminalEdgeVariable}.amount_usd_sum AS amount_usd_sum, ${terminalEdgeVariable}.tx_count AS tx_count, ${terminalEdgeVariable}.first_seen_timestamp AS first_seen_timestamp, ${terminalEdgeVariable}.last_seen_timestamp AS last_seen_timestamp, [${nodeVariables.map((nodeVariable) => `${nodeVariable}.address`).join(', ')}] AS addresses, [${nodeVariables.map((nodeVariable) => pathNodeMap(nodeVariable)).join(', ')}] AS path_nodes, [${edgeVariables.map(flowEdgeMap).join(', ')}] AS edge_props`,\n 'ORDER BY hops ASC',\n 'LIMIT 200',\n ].join(' '),\n }\n}\n\nfunction exchangeInflowQueries(address: string): Array<{ id: string; query: string }> {\n return Array.from({ length: 3 }, (_, index) => exchangeInflowQueryAtDepth(address, index + 1))\n}\n\nfunction exchangeInflowQueryAtDepth(address: string, depth: number): { id: string; query: string } {\n const intermediateVariables = Array.from(\n { length: Math.max(depth - 1, 0) },\n (_, index) => `n${index + 1}`\n )\n const nodeVariables = ['exchange', ...intermediateVariables, 'a']\n const edgeVariables = Array.from({ length: depth }, (_, index) => `r${index + 1}`)\n const relationshipChain = edgeVariables\n .map((edgeVariable, index) => {\n const targetVariable =\n index === edgeVariables.length - 1 ? 'a' : intermediateVariables[index]!\n return `-[${edgeVariable}:FLOWS_TO]->(${targetVariable}:Address)`\n })\n .join('')\n const intermediatePredicates = intermediateVariables.map(\n (nodeVariable) => `${nodeVariable}.is_exchange IS NULL`\n )\n const withdrawalVariable = nodeVariables[1]!\n const terminalEdgeVariable = edgeVariables[edgeVariables.length - 1]!\n return {\n id: `exchange_inflows_${depth}`,\n query: [\n `MATCH (exchange:Address)${relationshipChain}`,\n `WHERE a.address = \"${escapeCypherString(address)}\" AND a.address <> exchange.address AND exchange.is_exchange IS NOT NULL${intermediatePredicates.length > 0 ? ` AND ${intermediatePredicates.join(' AND ')}` : ''}`,\n `RETURN \"inflow\" AS direction, exchange.address AS exchange_address, exchange.labels AS exchange_display_labels, exchange.labels AS exchange_system_labels, ${withdrawalVariable}.address AS withdrawal_address, ${depth} AS hops, ${terminalEdgeVariable}.amount_usd_sum AS amount_usd_sum, ${terminalEdgeVariable}.tx_count AS tx_count, ${terminalEdgeVariable}.first_seen_timestamp AS first_seen_timestamp, ${terminalEdgeVariable}.last_seen_timestamp AS last_seen_timestamp, [${nodeVariables.map((nodeVariable) => `${nodeVariable}.address`).join(', ')}] AS addresses, [${nodeVariables.map((nodeVariable) => pathNodeMap(nodeVariable)).join(', ')}] AS path_nodes, [${edgeVariables.map(flowEdgeMap).join(', ')}] AS edge_props`,\n 'ORDER BY hops ASC',\n 'LIMIT 200',\n ].join(' '),\n }\n}\n\nfunction connectionProbeQuery(\n address: string,\n compareAddress: string\n): { id: string; query: string } {\n return {\n id: 'connection_probe',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[r:FLOWS_TO]-(b:Address {address: \"${escapeCypherString(compareAddress)}\"})`,\n 'RETURN [a.address, b.address] AS addresses, 1 AS hops',\n 'LIMIT 5',\n ].join(' '),\n }\n}\n\n// Address-grain has no separate identity-resolution lookup: existence of the\n// compare_address is checked directly against :Address, distinct from the\n// primary address_profile query so both existence checks can run in one batch.\nfunction compareAddressExistsQuery(address: string): { id: string; query: string } {\n return {\n id: 'compare_address_exists',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})`,\n 'RETURN a.address AS address',\n 'LIMIT 1',\n ].join(' '),\n }\n}\n\n// R2/R3 seed pre-flight (address grain): before a multi-address trace runs,\n// every seed is probed with a cheap indexed :Address existence lookup so a\n// made-up or inactive address is REPORTED as unresolved instead of silently\n// traced into an empty result. This preserves the pre-revert identity-\n// resolution contract's unresolved-seeds surface (structuredContent.unresolved\n// + summary.unresolved_count + the Unresolved summary line) with the address\n// itself as the graph key -- there is no canonical-key rewrite step anymore.\nfunction seedAddressExistsQuery(id: string, address: string): { id: string; query: string } {\n return {\n id,\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})`,\n 'RETURN a.address AS address',\n 'LIMIT 1',\n ].join(' '),\n }\n}\n\nasync function probeSeedAddresses(\n remoteClient: Client,\n network: string,\n inputs: string[]\n): Promise<Set<string>> {\n if (inputs.length === 0) return new Set()\n const queries = inputs.map((input, index) => ({\n input,\n ...seedAddressExistsQuery(`seed_address_exists_${index + 1}`, input),\n }))\n const batch = await callGraphBatch(\n remoteClient,\n network,\n queries.map(({ id, query }) => ({ id, query }))\n )\n const failures: QueryFailure[] = []\n const existing = new Set<string>()\n for (const { input, id } of queries) {\n const rows = optionalResultsFor(batch, id, failures)\n // A failed probe (ok:false) treats the seed as unresolved rather than\n // silently tracing it -- same conservative posture the pre-revert\n // identity resolution took on lookup failure.\n if (firstString(rows[0]?.['address'])) existing.add(input)\n }\n return existing\n}\n\n// AC11: FLOWS_TO reachability UNIONed over one -[:LINKED]- hop, so an\n// investigator surveying an address's exposure also sees exposure carried by\n// an address that is only ownership-LINKED to it (LINKED is undirected).\nfunction linkedExposureQueries(address: string): Array<{ id: string; query: string }> {\n return [\n {\n id: 'linked_exposure_direct',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[r:FLOWS_TO]-(b:Address)`,\n 'WHERE a.address <> b.address',\n `RETURN a.address AS subject_address, b.address AS counterparty_address, b.network AS counterparty_network, \"direct\" AS exposure_basis, r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count`,\n 'LIMIT 200',\n ].join(' '),\n },\n {\n id: 'linked_exposure_via_linked',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[l:LINKED]-(owned:Address)-[r:FLOWS_TO]-(b:Address)`,\n 'WHERE owned.address <> b.address AND a.address <> b.address',\n `RETURN a.address AS subject_address, b.address AS counterparty_address, b.network AS counterparty_network, \"via_linked\" AS exposure_basis, owned.address AS linked_via_address, l.basis AS link_basis, l.confidence AS link_confidence, r.amount_usd_sum AS amount_usd_sum, r.tx_count AS tx_count`,\n 'LIMIT 200',\n ].join(' '),\n },\n ]\n}\n\n// AC5: LINKED ownership-overlay probe. On the single public robinhood graph,\n// :Address.network is a node PROPERTY (robinhood), never a second query\n// network: LINKED is the ownership edge within that one space, served on the\n// topology graph only, with no network switch.\nfunction crossSpaceLinkedQuery(address: string): { id: string; query: string } {\n return {\n id: 'cross_space_linked',\n query: [\n `MATCH (a:Address {address: \"${escapeCypherString(address)}\"})-[l:LINKED]-(b:Address)`,\n 'WHERE a.network <> b.network',\n 'RETURN a.address AS address, a.network AS network, b.address AS linked_address, b.network AS linked_network, l.basis AS basis, l.confidence AS confidence, l.source_event AS source_event, l.declared_owner AS declared_owner',\n 'LIMIT 50',\n ].join(' '),\n }\n}\n\n// ── Pairwise route evidence ──\n// Directed shortest route between two KNOWN identity endpoints. The topology\n// graph is native Memgraph Cypher, so the route uses native `*BFS 1..N`\n// (BFS = fewest-hop directed route within the depth bound) between both\n// anchored endpoints. Exchange intermediates on a returned route are DISCLOSED\n// in the evidence, never silently filtered out.\n\nexport const CONNECTION_ROUTE_DEPTH_BOUND = 4\n\nexport function shouldIncludeRouteQueries(compareAddress: string | undefined): boolean {\n // Native traversal (*BFS) always fires when a compare address is given:\n // topology is unconditionally Memgraph-native.\n return Boolean(compareAddress)\n}\n\nexport function connectionRouteQueries(\n address: string,\n compareAddress: string\n): Array<{ id: string; query: string }> {\n const routeQuery = (fromAddress: string, toAddress: string): string =>\n [\n `MATCH p = (src:Address {address: \"${escapeCypherString(fromAddress)}\"})`,\n `-[:FLOWS_TO *BFS 1..${CONNECTION_ROUTE_DEPTH_BOUND}]->`,\n `(dst:Address {address: \"${escapeCypherString(toAddress)}\"}) RETURN p LIMIT 1`,\n ].join('')\n return [\n { id: 'connection_route_outbound', query: routeQuery(address, compareAddress) },\n { id: 'connection_route_inbound', query: routeQuery(compareAddress, address) },\n ]\n}\n\nexport interface RouteEvidenceSide {\n hops: number\n identities: string[]\n exchange_intermediates: string[]\n amount_usd_sum_total: number\n}\n\nfunction collectOrdered(\n value: unknown,\n matches: (candidate: Record<string, unknown>) => boolean\n): Array<Record<string, unknown>> {\n const collected: Array<Record<string, unknown>> = []\n const walk = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const entry of node) walk(entry)\n return\n }\n if (node && typeof node === 'object') {\n const candidate = node as Record<string, unknown>\n if (matches(candidate)) collected.push(candidate)\n for (const nested of Object.values(candidate)) walk(nested)\n }\n }\n walk(value)\n return collected\n}\n\n// is_exchange on topology nodes is a MARKER, not a boolean: it usually\n// carries the exchange name (e.g. \"binance\"). Treat explicit falsy\n// encodings as not-exchange (a bare non-null check would falsely disclose\n// nodes serialized as false/\"false\"/0), and any other non-empty value as\n// exchange. isExchangeFlag() is NOT reused here — it is boolean-only and\n// would miss name-valued markers.\nexport function isExchangeMarker(value: unknown): boolean {\n if (value === null || value === undefined || value === false) return false\n if (typeof value === 'number') return value !== 0\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase()\n return (\n normalized !== '' && normalized !== 'false' && normalized !== '0' && normalized !== 'null'\n )\n }\n return value === true\n}\n\n// Shape-tolerant hydrated-path reader: walks the returned path value for\n// ordered address nodes and FLOWS_TO edge amounts without pinning the\n// backend's exact path envelope.\nexport function routeFromPathValue(value: unknown): RouteEvidenceSide | null {\n if (!value || typeof value !== 'object') return null\n const nodes = collectOrdered(value, (candidate) => typeof candidate['address'] === 'string')\n if (nodes.length < 2) return null\n const edges = collectOrdered(\n value,\n (candidate) => typeof candidate['amount_usd_sum'] === 'number' && !('address' in candidate)\n )\n const identities = nodes.map((node) => String(node['address']))\n const exchangeIntermediates = nodes\n .slice(1, -1)\n .filter((node) => isExchangeMarker(node['is_exchange']))\n .map((node) => String(node['address']))\n return {\n // Hop count is a structural property of the node sequence — never\n // derived from edge-object detection, which keys on numeric\n // amount_usd_sum and would under-report on partially hydrated edges.\n hops: nodes.length - 1,\n identities,\n exchange_intermediates: exchangeIntermediates,\n amount_usd_sum_total: edges.reduce((total, edge) => total + Number(edge['amount_usd_sum']), 0),\n }\n}\n\nexport interface RouteEvidence {\n search_strategy: 'any_shortest'\n route_rank_basis: 'hop_count'\n depth_bound: number\n route_found: boolean\n outbound: RouteEvidenceSide | null\n inbound: RouteEvidenceSide | null\n}\n\nexport function buildRouteEvidence(\n outboundRows: Array<Record<string, unknown>>,\n inboundRows: Array<Record<string, unknown>>\n): RouteEvidence {\n const sideFrom = (rows: Array<Record<string, unknown>>): RouteEvidenceSide | null => {\n const first = rows[0]\n if (!first) return null\n const pathValue = 'p' in first ? first['p'] : Object.values(first)[0]\n return routeFromPathValue(pathValue)\n }\n const outbound = sideFrom(outboundRows)\n const inbound = sideFrom(inboundRows)\n return {\n search_strategy: 'any_shortest',\n route_rank_basis: 'hop_count',\n depth_bound: CONNECTION_ROUTE_DEPTH_BOUND,\n route_found: outbound !== null || inbound !== null,\n outbound,\n inbound,\n }\n}\n\nfunction formatExchangeRows(rows: Array<Record<string, unknown>>): string[] {\n return rows.map((row) => {\n const direction = String(row['direction'] ?? 'flow')\n const exchange = String(row['exchange_address'] ?? '')\n const amount = row['amount_usd_sum'] ?? ''\n const hops = row['hops'] ?? ''\n return `- ${direction}: ${exchange} (${hops} hop(s), amount_usd_sum ${amount})`\n })\n}\n\nfunction numberValue(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string' && value.trim()) {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : undefined\n }\n return undefined\n}\n\nfunction isExchangeFlag(value: unknown): boolean {\n if (value === true) return true\n if (value === false || value === null || value === undefined) return false\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase()\n return normalized === 'true' || normalized === '1'\n }\n if (typeof value === 'number') return value === 1\n return false\n}\n\nfunction hasExactExchangeLabel(labels: string[] | undefined): boolean {\n return (labels ?? []).some((label) => label.trim().toLowerCase() === 'exchange')\n}\n\nfunction firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n const parsed = numberValue(value)\n if (parsed !== undefined) return parsed\n }\n return undefined\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === 'string' && value.trim()) return value.trim()\n }\n return undefined\n}\n\nfunction riskLevelFromScore(score: number): string {\n if (score >= 0.85) return 'critical'\n if (score >= 0.7) return 'high'\n if (score >= 0.4) return 'medium'\n return 'low'\n}\n\nfunction riskRecommendation(level: string): string {\n if (level === 'critical' || level === 'high') return 'Escalate for manual review.'\n if (level === 'medium') return 'Review exchange exposure and counterparties before clearing.'\n return 'No stored risk signal found; continue with normal monitoring.'\n}\n\nfunction riskDrivers(\n profile: Record<string, unknown>,\n labelRows: Array<Record<string, unknown>>,\n exchangeRows: Array<Record<string, unknown>>\n): string[] {\n const drivers: string[] = []\n\n const riskLabels = labelRows\n .map((row) => firstString(row['label']))\n .filter((label): label is string => Boolean(label))\n if (riskLabels.length > 0) drivers.push(`Labels: ${[...new Set(riskLabels)].join('; ')}`)\n\n const outflowCount = exchangeRows.filter((row) => row['direction'] === 'outflow').length\n const inflowCount = exchangeRows.filter((row) => row['direction'] === 'inflow').length\n if (outflowCount > 0)\n drivers.push(`Forward bounded search reached ${outflowCount} exchange path(s).`)\n if (inflowCount > 0)\n drivers.push(`Backward bounded search found ${inflowCount} source exchange path(s).`)\n\n return [...new Set(drivers)]\n}\n\n// Derives the deterministic labelRows subset from the profile row's\n// a.label_risk property (topology, P2b′), reproducing the retired\n// addressLabelRiskQuery's `ORDER BY label.updated_timestamp DESC LIMIT 10`\n// subset byte-for-byte: same sort, same cap. Each entry already carries\n// {label, risk_level, updated_timestamp} -- no separate name/level pairing\n// is needed. Missing/empty property -> empty array, same as \"no labels\"\n// behaved under the old facts read.\nfunction deriveLabelRows(profile: Record<string, unknown>): Array<Record<string, unknown>> {\n const raw = profile['label_risk']\n const rows = Array.isArray(raw)\n ? raw.filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null && !Array.isArray(row)\n )\n : []\n return [...rows]\n .sort(\n (a, b) =>\n (numberValue(b['updated_timestamp']) ?? 0) - (numberValue(a['updated_timestamp']) ?? 0)\n )\n .slice(0, 10)\n}\n\nconst RISK_LEVEL_ORDER = ['critical', 'high', 'medium', 'low'] as const\n\nfunction strongestLabelRiskLevel(labelRows: Array<Record<string, unknown>>): string | undefined {\n const levels = labelRows\n .map((row) => firstString(row['risk_level'])?.toLowerCase())\n .filter((level): level is string =>\n Boolean(level && (RISK_LEVEL_ORDER as readonly string[]).includes(level))\n )\n if (levels.length === 0) return undefined\n return RISK_LEVEL_ORDER.find((candidate) => levels.includes(candidate))\n}\n\nfunction riskScoreSources(\n profile: Record<string, unknown>,\n labelRows: Array<Record<string, unknown>>\n): Array<Record<string, unknown>> {\n const sources: Array<Record<string, unknown>> = []\n if (numberValue(profile['live_risk_score']) !== undefined) {\n sources.push({\n family: 'ml_risk_score',\n layer: 'topology',\n source: 'address_node',\n fields: ['risk_score', 'risk_level'],\n })\n }\n if (labelRows.length > 0) {\n sources.push({\n family: 'label_risk',\n layer: 'topology',\n source: 'address_node',\n labels: labelRows.map((row) => ({\n label: row['label'],\n risk_level: row['risk_level'],\n })),\n })\n }\n return sources\n}\n\nfunction terminalEdgeProperties(row: Record<string, unknown>): Record<string, unknown> | undefined {\n const edgeProps = Array.isArray(row['edge_props'])\n ? (row['edge_props'] as Array<Record<string, unknown>>)\n : []\n return edgeProps[edgeProps.length - 1]\n}\n\nfunction enrichExchangeRows(rows: Array<Record<string, unknown>>): Array<Record<string, unknown>> {\n return rows.map((row) => {\n const terminal = terminalEdgeProperties(row)\n if (!terminal) return row\n return {\n ...row,\n amount_usd_sum: row['amount_usd_sum'] ?? terminal['amount_usd_sum'],\n tx_count: row['tx_count'] ?? terminal['tx_count'],\n first_seen_timestamp: row['first_seen_timestamp'] ?? terminal['first_seen_timestamp'],\n last_seen_timestamp: row['last_seen_timestamp'] ?? terminal['last_seen_timestamp'],\n first_tx_id: row['first_tx_id'] ?? terminal['first_tx_id'],\n last_tx_id: row['last_tx_id'] ?? terminal['last_tx_id'],\n }\n })\n}\n\nconst FALLBACK_SHARED_TX_COUNT = 1000\nconst FALLBACK_SHARED_USD_SUM = 5_000_000\nconst FALLBACK_SHARED_DAMPING = 0.1\nconst FALLBACK_VALUE_SATURATION_USD = 1_000_000\nconst FALLBACK_BASE_SCORE = 0.1\nconst FALLBACK_MAX_SCORE = 0.6\n\n/**\n * Score exchange exposure when no ML risk score exists. Topology-grain:\n * log-scaled USD volume across exchange rows, with shared/omnibus edges\n * (high tx_count or USD throughput) dampened, bounded in (0, 0.6] so a\n * fallback can never impersonate a high ML score band.\n */\nexport function exchangeExposureFallbackScore(\n exchangeRows: Array<Record<string, unknown>>\n): number {\n if (exchangeRows.length === 0) return 0\n let weightedUsd = 0\n for (const row of exchangeRows) {\n const usd = numberValue(row['amount_usd_sum']) ?? 0\n const txCount = numberValue(row['tx_count']) ?? 0\n const shared = txCount >= FALLBACK_SHARED_TX_COUNT || usd >= FALLBACK_SHARED_USD_SUM\n weightedUsd += shared ? usd * FALLBACK_SHARED_DAMPING : usd\n }\n const capped = Math.min(weightedUsd, FALLBACK_VALUE_SATURATION_USD)\n const factor = Math.log10(1 + capped) / Math.log10(1 + FALLBACK_VALUE_SATURATION_USD)\n return Math.min(\n FALLBACK_MAX_SCORE,\n FALLBACK_BASE_SCORE + (FALLBACK_MAX_SCORE - FALLBACK_BASE_SCORE) * factor\n )\n}\n\nexport function riskAssessment(\n profile: Record<string, unknown>,\n labelRows: Array<Record<string, unknown>>,\n exchangeRows: Array<Record<string, unknown>>\n): Record<string, unknown> {\n const mlRiskScore = firstNumber(profile['live_risk_score'])\n // UNSCORED is the model's explicit abstention (calibrated-scoring release):\n // the score exists for transparency but carries no stance — deriving a\n // severity band from it would silently launder an abstention into a\n // confident verdict (typically \"low\"). Fall back to labels/exchange\n // exposure and say so.\n const mlAbstained = isUnscoredRiskLevel(profile['live_risk_level'])\n const labelRiskLevel = strongestLabelRiskLevel(labelRows)\n const usableMlScore = mlAbstained ? undefined : mlRiskScore\n const score = usableMlScore ?? exchangeExposureFallbackScore(exchangeRows)\n let level =\n labelRiskLevel ??\n (mlAbstained && exchangeRows.length === 0 ? 'unscored' : riskLevelFromScore(score))\n const drivers = riskDrivers(profile, labelRows, exchangeRows)\n if (mlAbstained)\n drivers.push(\n 'ml_abstained: ML verdict is UNSCORED (insufficient labeled graph context); level derived from labels/exchange exposure only'\n )\n // Labels are curated truth and stay first — but a lower-severity label\n // must never SUPPRESS a more severe usable ML band. Failing toward\n // \"looks safe\" is the one direction an AML triage tool may not fail.\n const usableMlBand = usableMlScore !== undefined ? riskLevelFromScore(usableMlScore) : undefined\n if (\n labelRiskLevel &&\n usableMlBand &&\n riskSeverityRank(usableMlBand) > riskSeverityRank(labelRiskLevel)\n ) {\n level = usableMlBand\n drivers.push(\n `ml_label_divergence: usable ML band ${usableMlBand} exceeds strongest label level ${labelRiskLevel}; reporting the more severe band — review the label`\n )\n }\n return {\n level,\n score,\n ...(mlRiskScore !== undefined ? { ml_risk_score: mlRiskScore } : {}),\n ...(mlAbstained ? { ml_verdict: 'unscored' } : {}),\n confidence:\n usableMlScore !== undefined || labelRiskLevel\n ? 'high'\n : exchangeRows.length > 0\n ? 'medium'\n : 'low',\n recommendation:\n level === 'unscored'\n ? 'Model abstained and no label/exchange signal found; gather more context before clearing.'\n : riskRecommendation(level),\n drivers,\n sources: riskScoreSources(profile, labelRows),\n }\n}\n\nfunction formatRiskScore(score: unknown): string {\n const parsed = numberValue(score)\n if (parsed === undefined) return String(score ?? 'unknown')\n return Number.isInteger(parsed) ? parsed.toString() : parsed.toFixed(2)\n}\n\nfunction stringArrayValue(value: unknown): string[] | undefined {\n if (Array.isArray(value)) return value.map(String)\n if (typeof value === 'string' && value.trim()) return [value]\n return undefined\n}\n\nfunction restoreSystemLabels(\n graph: Record<string, unknown>,\n rawNodes: Array<Record<string, unknown>>\n): Record<string, unknown> {\n if (!Array.isArray(graph['nodes'])) return graph\n const labelsByAddress = new Map(\n rawNodes\n .map(\n (node) =>\n [\n typeof node['address'] === 'string'\n ? node['address']\n : typeof node['id'] === 'string'\n ? node['id']\n : '',\n stringArrayValue(node['system_labels']),\n ] as const\n )\n .filter(\n (entry): entry is readonly [string, string[]] =>\n Boolean(entry[0]) && Array.isArray(entry[1]) && entry[1].length > 0\n )\n )\n return {\n ...graph,\n nodes: graph['nodes'].map((node) => {\n if (typeof node !== 'object' || node === null || Array.isArray(node)) return node\n const record = node as Record<string, unknown>\n const address =\n typeof record['address'] === 'string'\n ? record['address']\n : typeof record['id'] === 'string'\n ? record['id']\n : ''\n const systemLabels = labelsByAddress.get(address)\n return systemLabels ? { ...record, system_labels: systemLabels } : record\n }),\n }\n}\n\nfunction buildRiskGraph(\n address: string,\n profile: Record<string, unknown>,\n rows: Array<Record<string, unknown>>,\n network: string\n): Record<string, unknown> {\n const nodes = new Map<string, Record<string, unknown>>()\n nodes.set(address, {\n id: address,\n address,\n node_type: 'address',\n labels: stringArrayValue(profile['display_labels']) ?? [],\n ...(stringArrayValue(profile['system_labels'])\n ? { system_labels: stringArrayValue(profile['system_labels']) }\n : {}),\n ...(stringArrayValue(profile['member_addresses'])?.length\n ? { member_addresses: stringArrayValue(profile['member_addresses']) }\n : {}),\n ...(numberValue(profile['live_risk_score']) !== undefined\n ? { risk_score: numberValue(profile['live_risk_score']) }\n : {}),\n ...(firstString(profile['live_risk_level'])\n ? { risk_level: firstString(profile['live_risk_level']) }\n : {}),\n roles: ['subject'],\n })\n const edges: Array<Record<string, unknown>> = []\n const mergeNode = (entry: string, metadata?: Record<string, unknown>) => {\n const existing = nodes.get(entry) ?? {\n id: entry,\n address: entry,\n node_type: 'address',\n labels: [],\n }\n const labels = stringArrayValue(metadata?.['labels']) ?? existing['labels']\n const systemLabels = stringArrayValue(metadata?.['system_labels']) ?? existing['system_labels']\n const memberAddresses =\n stringArrayValue(metadata?.['member_addresses']) ?? existing['member_addresses']\n const riskScore = numberValue(metadata?.['risk_score']) ?? existing['risk_score']\n const riskLevel =\n normalizeRiskLevel(firstString(metadata?.['risk_level'])) ?? existing['risk_level']\n nodes.set(entry, {\n ...existing,\n labels,\n ...(systemLabels ? { system_labels: systemLabels } : {}),\n ...(Array.isArray(memberAddresses) && memberAddresses.length > 0\n ? { member_addresses: memberAddresses }\n : {}),\n ...(riskScore !== undefined ? { risk_score: riskScore } : {}),\n ...(riskLevel ? { risk_level: riskLevel } : {}),\n })\n }\n for (const row of rows) {\n const rawPath = Array.isArray(row['path']) ? row['path'] : row['addresses']\n const path = Array.isArray(rawPath) ? rawPath.map(String) : []\n const pathNodes = Array.isArray(row['path_nodes'])\n ? (row['path_nodes'] as Array<Record<string, unknown>>)\n : []\n for (let index = 0; index < path.length; index += 1) {\n const entry = path[index]!\n mergeNode(entry, pathNodes[index])\n }\n const exchange = typeof row['exchange_address'] === 'string' ? row['exchange_address'] : ''\n if (exchange) {\n const displayLabels = stringArrayValue(row['exchange_display_labels']) ?? []\n const systemLabels =\n stringArrayValue(row['exchange_system_labels']) ??\n stringArrayValue(row['exchange_labels']) ??\n []\n nodes.set(exchange, {\n id: exchange,\n address: exchange,\n node_type: 'address',\n labels: displayLabels,\n ...(systemLabels.length > 0 ? { system_labels: systemLabels } : {}),\n roles: ['exchange'],\n })\n }\n for (let index = 0; index < path.length - 1; index += 1) {\n const edgeProps = Array.isArray(row['edge_props'])\n ? (row['edge_props'] as Array<Record<string, unknown>>)\n : []\n const edge = edgeProps[index] ?? row\n edges.push({\n source: path[index],\n target: path[index + 1],\n edge_type: 'flows_to',\n usd_amount: edge['amount_usd_sum'] ?? 0,\n amount_usd_sum: edge['amount_usd_sum'] ?? 0,\n tx_count: edge['tx_count'] ?? 0,\n first_tx_id: edge['first_tx_id'],\n last_tx_id: edge['last_tx_id'],\n direction: row['direction'],\n })\n }\n }\n const rawNodes = [...nodes.values()]\n return restoreSystemLabels(\n normalizeGraphPayload({\n schema: 'chain-insights.graph.v1',\n nodes: rawNodes,\n edges,\n flows: [],\n edge_anchors: [],\n metadata: { address, network, generated_at: new Date().toISOString() },\n }),\n rawNodes\n )\n}\n\nexport async function addressRisk(\n remoteClient: Client,\n options: AddressRiskOptions\n): Promise<{\n summaryText: string\n structuredContent: Record<string, unknown>\n graphData: Record<string, unknown>\n}> {\n const address = options.address.trim()\n const network = options.network.trim()\n const compareInput = options.compareAddress?.trim() ?? ''\n if (!address) throw new Error('address is required')\n if (!network) throw new Error('network is required')\n\n // Every internal graph_query_batch round trip this workflow makes is\n // observed here and totaled into a usage block on the response (never\n // blocks/throws on a backend that doesn't emit billing fields yet).\n const usage = createUsageAccumulator()\n const trackedClient = wrapClientForUsageTracking(remoteClient, usage)\n\n // Address-grain has no separate resolution step for the SUBJECT address:\n // the input address IS the graph node key. Its existence is inferred\n // post-hoc from the address_profile row (below) instead of the pre-revert\n // pre-flight lookup -- deliberate: it saves a round trip on the hot path.\n // The COMPARE address keeps a pre-flight existence probe, because the\n // connection probe and the *BFS route queries must be SUPPRESSED (not just\n // ignored) when the compare address does not exist -- pre-revert behavior\n // never issued route probes for an unresolved compare input.\n let compareUnresolved = false\n if (compareInput) {\n const compareBatch = await callGraphBatch(trackedClient, network, [\n compareAddressExistsQuery(compareInput),\n ])\n const compareRows = optionalResultsFor(compareBatch, 'compare_address_exists', [])\n compareUnresolved = !firstString(compareRows[0]?.['address'])\n }\n const compareAddress = compareInput && !compareUnresolved ? compareInput : ''\n\n const queries = [\n addressProfileQuery(address),\n addressFeatureQuery(address),\n { id: 'money_trail_incident', query: moneyTrailIncidentQuery(address) },\n ...exchangeOutflowQueries(address),\n ...exchangeInflowQueries(address),\n ...(compareAddress\n ? [connectionProbeQuery(address, compareAddress)]\n : [\n {\n id: 'connection_probe',\n query:\n 'MATCH (n:Address {address: \"__chain_insights_noop__\"}) RETURN n.address AS noop LIMIT 0',\n },\n ]),\n // Route evidence is additive: native *BFS traversal always fires for a\n // compare address whose existence probe succeeded (the 1-hop probe above\n // always runs).\n ...(shouldIncludeRouteQueries(compareAddress)\n ? connectionRouteQueries(address, compareAddress)\n : []),\n ]\n const batch = await callGraphBatch(trackedClient, network, queries)\n const partialQueryFailures: QueryFailure[] = []\n // Deliberate post-hoc existence inference (address grain): an empty\n // address_profile result means the subject :Address does not exist ->\n // report unresolved; this replaces the pre-revert identity pre-flight.\n const addressProfileRows = optionalResultsFor(batch, 'address_profile', partialQueryFailures)\n if (addressProfileRows.length === 0) {\n return {\n summaryText: `Address risk for ${network}:${address}\\n\\nUnresolved: no Address found for \"${address}\". The address may not have on-chain activity in this network.`,\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: 'aml_address_risk',\n facts: {\n subject: { network, addresses: [address] },\n unresolved: [address],\n usage: usageBlock(usage),\n },\n },\n graphData: {\n schema: 'chain-insights.graph.v1',\n nodes: [],\n edges: [],\n flows: [],\n edge_anchors: [],\n metadata: { address, network, generated_at: new Date().toISOString() },\n },\n }\n }\n const profile: Record<string, unknown> = {\n address,\n ...(addressProfileRows[0] ?? {}),\n ...(optionalResultsFor(batch, 'address_feature', partialQueryFailures)[0] ?? {}),\n }\n const labelRows = deriveLabelRows(profile)\n const outflows = enrichExchangeRows(\n optionalResultsWithPrefix(batch, 'exchange_outflows_', partialQueryFailures)\n )\n const inflows = enrichExchangeRows(\n optionalResultsWithPrefix(batch, 'exchange_inflows_', partialQueryFailures)\n )\n const connections = compareAddress\n ? optionalResultsFor(batch, 'connection_probe', partialQueryFailures)\n : []\n const routeEvidence = shouldIncludeRouteQueries(compareAddress)\n ? buildRouteEvidence(\n optionalResultsFor(batch, 'connection_route_outbound', partialQueryFailures),\n optionalResultsFor(batch, 'connection_route_inbound', partialQueryFailures)\n )\n : undefined\n const exchangeRows = [...outflows, ...inflows]\n // A hop-depth exchange_outflows_N/exchange_inflows_N query can fail\n // independently (e.g. the archive-tier query-memory limit on a deep\n // multi-hop search) while other hop depths succeed with zero rows. An\n // empty exchangeRows result must then read as \"search incomplete,\" not\n // \"no exchange exposure\" -- the two are not the same claim, and an AML\n // tool that silently reports a false-clean verdict on a partial search\n // failure is a real risk (found during MoA review, 2026-07-05).\n const exchangeSearchFailures = partialQueryFailures.filter(\n (failure) =>\n failure.id.startsWith('exchange_outflows_') || failure.id.startsWith('exchange_inflows_')\n )\n const exchangeSearchComplete = exchangeSearchFailures.length === 0\n // money-trail enrichment (optional): a preliminary block resolves the\n // primary_seed off the incident-only rows, then a second batch call fans\n // out TRAIL_ENDS_AT for that seed so the block can pick the highest-value\n // terminal fact. Both reads are optional-results -- a graph with no\n // money-trail layer never fails the tool.\n const moneyTrailIncidentRows = optionalResultsFor(\n batch,\n 'money_trail_incident',\n partialQueryFailures\n )\n const preliminaryMoneyTrail = buildMoneyTrailBlock(moneyTrailIncidentRows, [])\n let moneyTrailEndRows: Array<Record<string, unknown>> = []\n if (preliminaryMoneyTrail?.primary_seed) {\n try {\n const endsBatch = await callGraphBatch(trackedClient, network, [\n { id: 'money_trail_ends', query: moneyTrailEndsQuery(preliminaryMoneyTrail.primary_seed) },\n ])\n moneyTrailEndRows = optionalResultsFor(endsBatch, 'money_trail_ends', partialQueryFailures)\n } catch (error) {\n // Degrade, don't fail: a transport/parse error on the second batch call\n // (unlike a query-level ok:false, which optionalResultsFor already\n // absorbs) must not blow up the whole tool -- the incident rows are\n // already in hand, so the block still reports on_trail without\n // nearest_trail_end.\n collectQueryFailure(\n partialQueryFailures,\n 'money_trail_ends',\n error instanceof Error ? error.message : String(error)\n )\n }\n }\n const moneyTrail = buildMoneyTrailBlock(moneyTrailIncidentRows, moneyTrailEndRows)\n const graphData = buildRiskGraph(address, profile, exchangeRows, network)\n const risk = riskAssessment(profile, labelRows, exchangeRows)\n const liveRiskScore = numberValue(profile['live_risk_score'])\n const liveRiskLevel = firstString(profile['live_risk_level'])\n const liveNodeVerdict =\n liveRiskScore !== undefined || liveRiskLevel\n ? {\n ...(liveRiskScore !== undefined ? { risk_score: liveRiskScore } : {}),\n ...(liveRiskLevel ? { risk_level: liveRiskLevel } : {}),\n source: 'topology_node',\n }\n : undefined\n\n const lines = [\n `Address risk for ${network}:${address}`,\n '',\n `Risk: ${risk['level']} (${formatRiskScore(risk['score'])})`,\n `Confidence: ${risk['confidence']}`,\n `Recommendation: ${risk['recommendation']}`,\n ...(liveNodeVerdict\n ? [`Live node triage: ${liveRiskLevel ?? 'unknown'} (${formatRiskScore(liveRiskScore)})`]\n : []),\n `Graph degree: in ${profile['degree_in'] ?? 'unknown'}, out ${profile['degree_out'] ?? 'unknown'}.`,\n '',\n 'Exchange behavior',\n ...(exchangeRows.length > 0\n ? [\n formatExchangeRows(exchangeRows).join('\\n'),\n ...(exchangeSearchComplete\n ? []\n : [\n `(incomplete: ${exchangeSearchFailures.length} other hop-depth quer${exchangeSearchFailures.length === 1 ? 'y' : 'ies'} failed -- there may be more exchange exposure than shown here)`,\n ]),\n ]\n : [\n exchangeSearchComplete\n ? '- No exchange inflow/outflow paths found in bounded search.'\n : `- Exchange search incomplete: ${exchangeSearchFailures.length} hop-depth quer${exchangeSearchFailures.length === 1 ? 'y' : 'ies'} failed before returning a result. This is NOT a clean finding -- retry or narrow the search (see Partial query failures below).`,\n ]),\n ]\n if (Array.isArray(risk['drivers']) && risk['drivers'].length > 0) {\n lines.push('', 'Risk drivers', risk['drivers'].map((driver) => `- ${driver}`).join('\\n'))\n }\n if (moneyTrail) {\n lines.push('', moneyTrailSummarySentence(moneyTrail))\n }\n if (compareAddress) {\n lines.push(\n '',\n `Connection compare target: ${compareAddress}`,\n connections.length > 0\n ? `Connection paths found: ${connections.length}`\n : 'Connection paths found: 0'\n )\n }\n if (compareUnresolved) {\n lines.push(\n '',\n `Unresolved compare_address: no Address found for \"${compareInput}\"; comparison skipped.`\n )\n }\n if (partialQueryFailures.length > 0) {\n lines.push(\n '',\n 'Partial query failures',\n partialQueryFailures.map((failure) => `- ${failure.id}: ${failure.error}`).join('\\n')\n )\n }\n const summaryText = lines.join('\\n')\n const structuredContent = {\n schema: 'chain-insights.result.v1',\n tool: 'aml_address_risk',\n facts: {\n subject: {\n network,\n addresses: compareAddress ? [address, compareAddress] : [address],\n },\n risk: {\n ...risk,\n ...(liveNodeVerdict ? { live_node: liveNodeVerdict } : {}),\n },\n exchange_behavior: {\n outflows,\n inflows,\n search_status: exchangeSearchComplete ? 'complete' : 'incomplete',\n ...(exchangeSearchComplete\n ? {}\n : { failed_query_ids: exchangeSearchFailures.map((failure) => failure.id) }),\n },\n connection: compareAddress\n ? {\n compare_address: compareAddress,\n paths: connections,\n ...(routeEvidence ? { route_evidence: routeEvidence } : {}),\n }\n : undefined,\n money_trail: moneyTrail,\n unresolved: compareUnresolved ? [compareInput] : undefined,\n partial_query_errors: partialQueryFailures.length > 0 ? partialQueryFailures : undefined,\n usage: usageBlock(usage),\n },\n }\n const artifacts = options.writeArtifacts\n ? await writeAddressRiskArtifacts(\n network,\n address,\n compareAddress,\n graphData,\n exchangeRows,\n summaryText\n )\n : statelessArtifacts()\n const evidence = artifactEvidence(artifacts)\n\n return {\n summaryText,\n structuredContent: {\n ...structuredContent,\n artifacts,\n evidence: [\n ...evidence,\n {\n evidence_type: 'tool_summary',\n summary: `aml_address_risk ${address} completed for ${network}`,\n },\n ],\n },\n graphData,\n }\n}\n\n/**\n * Routes the public AML tool contract to the requested implementation. An\n * omitted version always follows the latest supported contract.\n */\nexport async function runAmlAddressRisk(\n remoteClient: Client,\n options: AddressRiskOptions,\n requestedVersion?: string\n) {\n const version = resolveAmlAddressRiskVersion(requestedVersion)\n switch (version) {\n case 'v1': {\n const result = await addressRisk(remoteClient, options)\n return {\n ...result,\n structuredContent: {\n ...result.structuredContent,\n tool_version: version,\n },\n }\n }\n }\n}\n\nfunction toCsvValue(value: unknown): string {\n return value === undefined || value === null ? '' : String(value)\n}\n\nfunction subjectNodeForExchangeRow(row: Record<string, unknown>, fallbackAddress: string): string {\n return String(\n row['direction'] === 'inflow'\n ? (row['withdrawal_address'] ?? row['deposit_address'] ?? fallbackAddress)\n : (row['deposit_address'] ?? row['withdrawal_address'] ?? fallbackAddress)\n )\n}\n\nfunction buildAddressRiskTableHtml(\n tool: string,\n network: string,\n rows: Array<Record<string, unknown>>,\n subject: string\n): string {\n const headers = [\n 'direction',\n 'exchange_address',\n 'subject_path_node',\n 'hops',\n 'amount_usd_sum',\n 'tx_count',\n ] as const\n const body = rows\n .map((row) => {\n const exchangeAddress = String(row['exchange_address'] ?? '')\n const subjectNode = subjectNodeForExchangeRow(row, subject)\n const rowValues = [\n row['direction'] ?? '',\n exchangeAddress,\n subjectNode,\n row['hops'] ?? '',\n row['amount_usd_sum'] ?? '',\n row['tx_count'] ?? '',\n ]\n return `<tr>${rowValues.map((value) => `<td>${htmlEscape(toCsvValue(value))}</td>`).join('')}</tr>`\n })\n .join('\\n')\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${htmlEscape(tool)} Risk Table</title>\n<style>\n :root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui; background: #0b0d12; color: #f4f2ea; }\n body { margin: 0; background: #0b0d12; color: #f4f2ea; }\n main { padding: 24px; }\n h1 { font-size: 20px; margin: 0 0 8px; font-weight: 650; }\n table { border-collapse: collapse; width: 100%; min-width: 900px; }\n th, td { border-bottom: 1px solid rgba(255,255,255,.08); padding: 8px 10px; text-align: left; }\n th { position: sticky; top: 0; background: #161a24; color: #f2dda6; font-weight: 600; }\n</style>\n</head>\n<body>\n<main>\n <h1>${htmlEscape(tool)} Table</h1>\n <div>Network: <strong>${htmlEscape(network)}</strong></div>\n <div>Generated: <strong>${htmlEscape(new Date().toISOString())}</strong></div>\n <table>\n <thead><tr>${headers.map((header) => `<th>${htmlEscape(header)}</th>`).join('')}</tr></thead>\n <tbody>${body}</tbody>\n </table>\n</main>\n</body>\n</html>\n`\n}\n\nasync function writeAddressRiskArtifacts(\n network: string,\n address: string,\n compareAddress: string | undefined,\n graphData: Record<string, unknown>,\n exchangeRows: Array<Record<string, unknown>>,\n summaryText: string\n): Promise<Record<string, string>> {\n const paths = workspaceOutputPaths()\n await Promise.all([\n mkdir(paths.reportsRoot, { recursive: true }),\n mkdir(paths.reportGraphsRoot, { recursive: true }),\n mkdir(paths.reportTablesRoot, { recursive: true }),\n ])\n const safeNetwork = network.replace(/[^A-Za-z0-9._-]+/g, '_')\n const safeAddress = address.replace(/[^A-Za-z0-9._-]+/g, '_')\n const slug = `${new Date()\n .toISOString()\n .replace(/[-:]/g, '')\n .replace(/\\.\\d{3}Z$/, 'Z')}_aml_address_risk_${safeNetwork}_${safeAddress}`\n const graphPath = path.join(paths.reportGraphsRoot, `${slug}.graph.json`)\n const tableJsonPath = path.join(paths.reportTablesRoot, `${slug}.compact-evidence.json`)\n const csvPath = path.join(paths.reportTablesRoot, `${slug}.flows.csv`)\n const tableHtmlPath = path.join(paths.reportsRoot, `${slug}.table.html`)\n const reportPath = path.join(paths.reportsRoot, `${slug}.aml-address-report.md`)\n const graphHtmlPath = path.join(paths.reportsRoot, `${slug}.graph.html`)\n const { generateInlineGraphHtml } = await import('../viz/html-generator.js')\n const header = [\n 'direction',\n 'exchange_address',\n 'subject_path_node',\n 'hops',\n 'amount_usd_sum',\n 'tx_count',\n ]\n const csv =\n [\n header.join(','),\n ...exchangeRows.map((row) => {\n const exchangeAddress = String(row['exchange_address'] ?? '')\n const subjectPathNode = subjectNodeForExchangeRow(row, address)\n return [\n row['direction'] ?? '',\n exchangeAddress,\n subjectPathNode,\n row['hops'] ?? '',\n row['amount_usd_sum'] ?? '',\n row['tx_count'] ?? '',\n ]\n .map((value) => JSON.stringify(String(value)))\n .join(',')\n }),\n ].join('\\n') + '\\n'\n const evidence = {\n schema: 'chain-insights.trace.v1',\n tool: 'aml_address_risk',\n network,\n input: {\n address,\n ...(compareAddress ? { compare_address: compareAddress } : {}),\n },\n profile: {\n exchange_rows: exchangeRows.map((row) => ({\n direction: row['direction'],\n exchange_address: row['exchange_address'],\n subject_node: subjectNodeForExchangeRow(row, address),\n hops: row['hops'],\n amount_usd_sum: row['amount_usd_sum'],\n tx_count: row['tx_count'],\n })),\n report_summary: summaryText,\n },\n }\n await writeFile(graphPath, JSON.stringify(graphData, null, 2) + '\\n', { mode: 0o600 })\n await writeFile(tableJsonPath, JSON.stringify(evidence, null, 2) + '\\n', { mode: 0o600 })\n await writeFile(csvPath, csv, { mode: 0o600 })\n await writeFile(\n tableHtmlPath,\n buildAddressRiskTableHtml('aml_address_risk', network, exchangeRows, address),\n { mode: 0o600 }\n )\n await writeFile(graphHtmlPath, generateInlineGraphHtml(graphData), { mode: 0o600 })\n await writeFile(\n reportPath,\n [\n `# Address Risk Report (${network}:${address})`,\n `- Graph JSON: ${graphPath}`,\n `- Table JSON: ${tableJsonPath}`,\n `- CSV: ${csvPath}`,\n `- Report HTML: ${tableHtmlPath}`,\n `- Graph HTML: ${graphHtmlPath}`,\n '',\n summaryText,\n ].join('\\n'),\n { mode: 0o600 }\n )\n\n return {\n graph_json: graphPath,\n graph_html: graphHtmlPath,\n table_json: tableJsonPath,\n flows_csv: csvPath,\n table_html: tableHtmlPath,\n report_md: reportPath,\n }\n}\n\nfunction statelessArtifacts(): Record<string, unknown> {\n return {\n artifacts_written: false,\n artifact_mode: 'stateless',\n }\n}\n\nfunction artifactEvidence(artifacts: Record<string, unknown>): Array<Record<string, unknown>> {\n return Object.entries(artifacts)\n .filter(\n (entry): entry is [string, string] =>\n entry[0] !== 'artifact_mode' && typeof entry[1] === 'string' && entry[1].length > 0\n )\n .map(([kind, filePath]) => ({\n evidence_type: 'artifact_pointer',\n path: filePath,\n summary: `${kind} artifact`,\n }))\n}\n\nfunction htmlEscape(value: unknown): string {\n return String(value ?? '')\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll(\"'\", ''')\n}\n\n// Deterministic query-builder surface for the committed corpus and\n// query-with-a-documented-call-contract tests. Reflects the retained\n// aml_address_risk/graph query builders after the aml_trace_* cut.\nexport const queryBuilderContract = {\n topologyGraphQuery,\n addressProfileQuery,\n compareAddressExistsQuery,\n addressFeatureQuery,\n exchangeOutflowQueries,\n exchangeInflowQueries,\n connectionProbeQuery,\n connectionRouteQueries,\n linkedExposureQueries,\n crossSpaceLinkedQuery,\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmBA,MAAM,YAAY;AAKlB,MAAM,6CAA6B,IAAI,IAAI;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAM,kBAAkB;;;AASxB,SAAS,cAAc,OAAe,KAAuB;CAC3D,MAAM,QAAkB,CAAC;CACzB,IAAI,QAAQ;CACZ,IAAI,UAAU;CACd,KAAK,MAAM,MAAM,OAAO;EACtB,IAAI,OAAO,KAAK,SAAS;EACzB,IAAI,OAAO,KAAK,SAAS;EACzB,IAAI,OAAO,OAAO,UAAU,GAAG;GAC7B,MAAM,KAAK,OAAO;GAClB,UAAU;EACZ,OACE,WAAW;CAEf;CACA,MAAM,KAAK,OAAO;CAClB,OAAO;AACT;;;;;;AAOA,SAAS,cAAc,OAAe,SAAiB,WAAoC;CAEzF,MAAM,QAAQ,IADM,OAAO,MAAM,QAAQ,MAAM,GAC3B,CAAC,CAAC,KAAK,KAAK;CAChC,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,OAAO,MAAM,MAAM,MAAM,QAAQ,MAAM,EAAE,CAAC,MAAM;CACtD,IAAI,MAAM,KAAK;CACf,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,YAAY,IAAI,OAAO,MAAM,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI;EAC5D,IAAI,aAAa,UAAU,QAAQ,KAAK,MAAM,UAAU;CAC1D;CACA,OAAO,KAAK,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK;AACjC;AAEA,SAAS,gBAAgB,OAAiC;CACxD,MAAM,SAAS,cAAc,OAAO,UAAU;EAAC;EAAY;EAAS;CAAM,CAAC;CAC3E,IAAI,CAAC,QAAQ,OAAO,CAAC;CACrB,OAAO,cAAc,QAAQ,GAAG,CAAC,CAC9B,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,CACxB,QAAQ,SAAS,KAAK,SAAS,CAAC,CAAC,CACjC,KAAK,SAAS;EACb,MAAM,UAAU,wBAAwB,KAAK,IAAI;EACjD,IAAI,SAAS,OAAO;GAAE,MAAM,QAAQ,EAAE,CAAC,KAAK;GAAG,OAAO,QAAQ,EAAE,CAAC,KAAK;EAAE;EACxE,OAAO;GAAE,MAAM;GAAM,OAAO;EAAK;CACnC,CAAC;AACL;AAEA,SAAS,aAAa,MAAsB;CAC1C,MAAM,UAAU,KAAK,KAAK;CAC1B,MAAM,MAAM,QAAQ,YAAY,GAAG;CACnC,OAAO,OAAO,IAAI,QAAQ,MAAM,MAAM,CAAC,IAAI;AAC7C;;;;;;;AAQA,SAAgB,4BAA4B,OAA6B;CACvE,MAAM,cAAc,gBAAgB,KAAK;CACzC,MAAM,gBAAgB,YAAY,QAAQ,MAAM,gBAAgB,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK;CAEhG,MAAM,UAAwB,CAAC;CAC/B,IAAI,cAAc,SAAS,GAAG,QAAQ,gBAAgB;CAEtD,MAAM,cAAc,cAAc,OAAO,YAAY,CAAC,SAAS,MAAM,CAAC;CACtE,IAAI,aAAa;EAGf,MAAM,cAAc,cAAc,aAAa,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,KAAK;EAClE,MAAM,OAAO,YAAY,KAAK,WAAW;EACzC,MAAM,WAAW,YAAY,QAAQ,oBAAoB,EAAE,CAAC,CAAC,KAAK;EAClE,MAAM,UAAU,YAAY,MAAM,MAAM,EAAE,SAAS,YAAY,EAAE,UAAU,QAAQ;EACnF,MAAM,cAAc,UAAU,QAAQ,QAAQ;EAK9C,IAAI,eAAe,CAAC,cAAc,SAAS,WAAW,GAAG;GACvD,QAAQ,UAAU;IAAE,KAAK;IAAa;GAAK;GAC3C,MAAM,UAAU,aAAa,UAAU,QAAQ,OAAO,QAAQ;GAC9D,QAAQ,gBAAgB,2BAA2B,IAAI,OAAO,IAC1D,cACA;EACN;CACF;CAEA,MAAM,aAAa,mBAAmB,KAAK,KAAK;CAChD,IAAI,YAAY,QAAQ,QAAQ,OAAO,WAAW,EAAE;CAEpD,OAAO;AACT;;;;;;;AAeA,SAAgB,eACd,MACA,OACqB;CAErB,IAAI,CADc,KAAK,MAAM,QAAQ,OAAO,IAAI,eAAe,QAClD,GACX,OAAO;EAAE;EAAM,UAAU,CAAC;EAAG,UAAU;EAAa,QAAQ;CAAM;CAEpE,MAAM,UAAU,4BAA4B,KAAK;CAEjD,OAAO;EAAE,GADM,eAAe,MAAM,OACnB;EAAG,QAAQ;CAAK;AACnC;;;;;;;;;;;AAqBA,SAAgB,8BACd,SACA,SACM;CACN,IAAI,CAAC,WAAW,QAAQ,WAAW,GAAG;CACtC,MAAM,YAAY,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;CAC7D,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,CAAC,SAAS,MAAM,OAAO,SAAS,CAAC,MAAM,WAAW,MAAM,QAAQ,WAAW,GAAG;EAClF,MAAM,YAAY,MAAM,OAAO,KAAA,IAAY,UAAU,IAAI,MAAM,EAAE,IAAI,KAAA;EACrE,IAAI,cAAc,KAAA,GAAW;EAC7B,MAAM,SAAS,eAAe,MAAM,SAAS,SAAS;EACtD,IAAI,CAAC,OAAO,QAAQ;EACpB,MAAM,UAAU,OAAO;EACvB,IAAI,OAAO,KAAK,OAAO,QAAQ,CAAC,CAAC,SAAS,GAAG,MAAM,WAAW,OAAO;EACrE,MAAM,WAAW,OAAO;CAC1B;AACF;;;ACzMA,MAAM,oCAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,kCAAkB,IAAI,IAAI;CAAC;CAAY;CAAY;CAAe;AAAc,CAAC;AAavF,SAASA,WAAS,OAAsC;CACtD,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,YAAY,OAA0B;CAC7C,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,IAAI,MAAM,CAAC,CAAC,OAAO,OAAO,IAAI,CAAC;AACrE;AAEA,SAAS,OAAO,QAA4B;CAC1C,OAAO,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAC5B;AAEA,SAAS,WAAW,OAAuB;CACzC,OAAO,MACJ,KAAK,CAAC,CACN,QAAQ,sBAAsB,OAAO,CAAC,CACtC,QAAQ,kBAAkB,GAAG,CAAC,CAC9B,QAAQ,YAAY,EAAE,CAAC,CACvB,YAAY;AACjB;AAEA,SAAS,iBAAiB,OAAwB;CAChD,OAAO,kBAAkB,IAAI,KAAK;AACpC;AAEA,SAAS,cAAc,MAA6B;CAClD,OAAO,OAAO,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,UAAU,CAAC,iBAAiB,KAAK,CAAC;AACvF;AAEA,SAAS,aAAa,MAA6B;CACjD,OAAO,OAAO;EACZ,GAAG,YAAY,KAAK,gBAAgB;EACpC,GAAG,YAAY,KAAK,eAAe;EACnC,GAAG,YAAY,KAAK,aAAa;EACjC,GAAG,YAAY,KAAK,SAAS,CAAC,CAAC,OAAO,gBAAgB;CACxD,CAAC;AACH;AAEA,SAAS,eAAe,MAA6B;CACnD,MAAM,QAAQ,YAAY,KAAK,QAAQ;CACvC,MAAM,OAAO,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU;CAC/D,IAAI,SAAS,mBAAmB,MAAM,KAAK,UAAU;MAChD,IAAI,MAAM,MAAM,KAAK,IAAI;CAE9B,MAAM,UAAU,YAAY,KAAK,SAAS,CAAC,CAAC,IAAI,UAAU;CAC1D,IACE,aAAa,IAAI,CAAC,CAAC,MAAM,UAAU,WAAW,KAAK,MAAM,UAAU,KACnE,QAAQ,SAAS,UAAU,GAE3B,MAAM,KAAK,UAAU;CAGvB,OAAO,OAAO,MAAM,IAAI,UAAU,CAAC,CAAC,OAAO,OAAO,CAAC;AACrD;AAEA,SAAS,kBAAkB,MAA2B;CACpD,IAAI,OAAO,KAAK,iBAAiB,YAAY,KAAK,YAAY,CAAC,KAAK,GAClE,OAAO,WAAW,KAAK,YAAY;CAErC,IAAI,OAAO,KAAK,eAAe,YAAY,OAAO,KAAK,UAAU,UAAU,OAAO;CAClF,MAAM,SAAS,aAAa,IAAI;CAChC,IAAI,OAAO,SAAS,GAAG,OAAO,WAAW,OAAO,EAAG;CACnD,OAAO;AACT;AAEA,SAAS,cAAc,MAA4B;CACjD,IAAI,CAACA,WAAS,IAAI,GAAG,OAAO,CAAC;CAE7B,MAAM,aAA0B,CAAC;CACjC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAAG;EAC/C,IACE;GACE;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF,CAAC,CAAC,SAAS,GAAG,GAEd;EAEF,WAAW,OAAO;CACpB;CAEA,MAAM,KACJ,OAAO,KAAK,UAAU,WAClB,KAAK,QACL,OAAO,KAAK,eAAe,WACzB,KAAK,aACL,KAAA;CACR,IAAI,IAAI,WAAW,QAAQ;CAE3B,MAAM,UAAU,OAAO,KAAK,eAAe,WAAW,KAAK,aAAa;CACxE,IAAI,SAAS,WAAW,aAAa;CAErC,WAAW,eAAe,kBAAkB,IAAI;CAChD,WAAW,YAAY,cAAc,IAAI;CAEzC,MAAM,kBAAkB,OAAO,YAAY,KAAK,mBAAmB,CAAC;CACpE,IAAI,gBAAgB,SAAS,GAAG,WAAW,sBAAsB;CAEjE,MAAM,QAAQ,eAAe,IAAI;CACjC,IAAI,MAAM,SAAS,GAAG,WAAW,WAAW;CAE5C,IACE,CAAC,MAAM,QAAQ,KAAK,QAAQ,KAC5B,MAAM,QAAQ,KAAK,gBAAgB,KACnC,KAAK,gBAAgB,CAAC,SAAS,GAE/B,WAAW,WAAW,KAAK,gBAAgB,CAAC,IAAI,MAAM;CAGxD,OAAO;AACT;AAEA,SAAS,kBAAkB,MAA2B;CASpD,MAAM,WAAW,WAPf,OAAO,KAAK,iBAAiB,YAAY,KAAK,YAAY,CAAC,KAAK,IAC5D,KAAK,eACL,OAAO,KAAK,YAAY,YAAY,KAAK,OAAO,CAAC,KAAK,IACpD,KAAK,UACL,OAAO,KAAK,yBAAyB,YAAY,KAAK,oBAAoB,CAAC,KAAK,IAC9E,KAAK,uBACL,YACyB;CACnC,OAAO,gBAAgB,IAAI,QAAQ,IAAI,aAAa;AACtD;AAEA,SAAS,cAAc,MAA4B;CACjD,IAAI,CAACA,WAAS,IAAI,GAAG,OAAO,CAAC;CAE7B,MAAM,aAA0B,CAAC;CACjC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,GAAG;EAC/C,IAAI;GAAC;GAAa;GAAgB;GAAqB;GAAc;EAAM,CAAC,CAAC,SAAS,GAAG,GACvF;EACF,WAAW,OAAO;CACpB;CAEA,IAAI,OAAO,WAAW,cAAc,YAAY,OAAO,KAAK,oBAAoB,UAC9E,WAAW,YAAY,KAAK;CAE9B,IAAI,OAAO,WAAW,cAAc,YAAY,OAAO,KAAK,kBAAkB,UAC5E,WAAW,YAAY,KAAK;CAE9B,WAAW,eAAe,kBAAkB,IAAI;CAEhD,OAAO;AACT;AAEA,SAAgB,sBAAsB,SAA0C;CAC9E,IAAI,CAACA,WAAS,OAAO,KAAK,QAAQ,cAAc,2BAC9C,MAAM,IAAI,MAAM,kCAAkC;CAGpD,OAAO;EACL,GAAG;EACH,QAAQ;EACR,OAAO,MAAM,QAAQ,QAAQ,QAAQ,IAAI,QAAQ,QAAQ,CAAC,IAAI,aAAa,IAAI,CAAC;EAChF,OAAO,MAAM,QAAQ,QAAQ,QAAQ,IAAI,QAAQ,QAAQ,CAAC,IAAI,aAAa,IAAI,CAAC;EAChF,OAAO,MAAM,QAAQ,QAAQ,QAAQ,IAAI,QAAQ,WAAW,CAAC;EAC7D,cAAc,MAAM,QAAQ,QAAQ,eAAe,IAAI,QAAQ,kBAAkB,CAAC;CACpF;AACF;;;;;;;;;;;;ACpLA,MAAa,kBAAkB;CAAC;CAAO;CAAU;CAAQ;AAAU;AACnE,MAAa,sBAAsB;;AAGnC,SAAgB,mBAAmB,OAAoC;CACrE,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,aAAa,MAAM,KAAK,CAAC,CAAC,YAAY;CAC5C,IAAI,CAAC,YAAY,OAAO,KAAA;CACxB,IAAI,eAAA,YAAoC,OAAO;CAC/C,IAAK,gBAAsC,SAAS,UAAU,GAAG,OAAO;AAE1E;;AAGA,SAAgB,oBAAoB,OAAyB;CAC3D,OAAO,mBAAmB,KAAK,MAAM;AACvC;;;;;;AAOA,SAAgB,iBAAiB,OAAwB;CACvD,MAAM,aAAa,mBAAmB,KAAK;CAC3C,IAAI,CAAC,cAAc,eAAA,YAAoC,OAAO;CAC9D,OAAQ,gBAAsC,QAAQ,UAAU;AAClE;;;AClCA,MAAa,qBAAqB;AAiBlC,SAAgB,qBAAqB,WAAW,QAAQ,IAAI,GAAW;CACrE,MAAM,YAAY,oBAAoB,QAAQ;CAC9C,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,kBAAkB;CAClD,OAAO,UAAU;AACnB;AAEA,SAAgB,qBAAqB,gBAAgB,qBAAqB,GAAyB;CACjG,MAAM,OAAO,KAAK,QAAQ,aAAa;CACvC,OAAO;EACL;EACA,aAAa,KAAK,KAAK,MAAM,iBAAiB;EAC9C,WAAW,KAAK,KAAK,MAAM,mBAAmB,QAAQ;EACtD,YAAY,KAAK,KAAK,MAAM,mBAAmB,SAAS;EACxD,eAAe,KAAK,KAAK,MAAM,WAAW;EAC1C,cAAc,KAAK,KAAK,MAAM,UAAU;EACxC,cAAc,KAAK,KAAK,MAAM,UAAU;EACxC,eAAe,KAAK,KAAK,MAAM,WAAW;EAC1C,aAAa,KAAK,KAAK,MAAM,SAAS;EACtC,kBAAkB,KAAK,KAAK,MAAM,WAAW,QAAQ;EACrD,kBAAkB,KAAK,KAAK,MAAM,WAAW,QAAQ;EACrD,UAAU,KAAK,KAAK,MAAM,mBAAmB,WAAW,MAAM;CAChE;AACF;;;ACXA,SAAgB,yBAAsC;CACpD,OAAO;EAAE,gBAAgB;EAAG,aAAa;EAAG,mBAAmB;CAAE;AACnE;AAIA,SAAgB,WAAW,QAAkC;CAC3D,OAAO;EACL,gBAAgB,OAAO;EACvB,aAAa,OAAO;EACpB,mBAAmB,OAAO;CAC5B;AACF;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAASC,cAAY,OAAoC;CACvD,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG,OAAO;CAChE,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,GAAG;EAC7C,MAAM,SAAS,OAAO,KAAK;EAC3B,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS,KAAA;CAC5C;AAEF;AAEA,SAAS,iBAAiB,QAAyB;CACjD,IAAI,CAAC,SAAS,MAAM,GAAG,OAAO;CAC9B,MAAM,UAAU,OAAO;CACvB,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;CACpC,OAAO,QACJ,QACE,SACC,SAAS,IAAI,KAAK,KAAK,YAAY,UAAU,OAAO,KAAK,YAAY,QACzE,CAAC,CACA,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,IAAI;AACd;AAMA,SAAS,gBAAgB,QAAqB,OAAsB;CAClE,IAAI,CAAC,SAAS,KAAK,GAAG;CAEtB,MAAM,QAAQ,MAAM;CACpB,IAAI,SAAS,KAAK,GAAG;EACnB,MAAM,QAAQA,cAAY,MAAM,iBAAiB;EACjD,IAAI,UAAU,KAAA,GAAW,OAAO,kBAAkB;EAClD,IAAI,MAAM,iBAAiB,MAAM,OAAO,qBAAqB;CAC/D;CAEA,MAAM,QAAQ,MAAM;CACpB,MAAM,aAAa,SAAS,KAAK,IAAIA,cAAY,MAAM,iBAAiB,IAAI,KAAA;CAC5E,MAAM,UAAU,MAAM,QAAQ,MAAM,UAAU,IACzC,MAAM,UAAU,CAAe,OAAO,QAAQ,IAC/C,KAAA;CAEJ,IAAI,eAAe,KAAA,GACjB,OAAO,kBAAkB;MACpB,IAAI,SAIT,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,QAAQA,cAAY,MAAM,iBAAiB;EACjD,IAAI,UAAU,KAAA,GAAW,OAAO,kBAAkB;CACpD;CAGF,IAAI,SACG;OAAA,MAAM,SAAS,SAClB,IAAI,MAAM,iBAAiB,MAAM,OAAO,qBAAqB;CAAA,OAE1D,IAAI,SAAS,KAAK,KAAK,MAAM,iBAAiB,MACnD,OAAO,qBAAqB;AAEhC;AAQA,SAAS,4BAA4B,QAAqB,WAA0B;CAClF,IAAI;CACJ,IAAI;EACF,MAAM,OAAO,iBAAiB,SAAS,CAAC,CAAC,KAAK;EAC9C,IAAI,MAAM,QAAS,KAAK,MAAM,IAAI,CAAC,CAAyB;OACvD,IACH,SAAS,SAAS,KAClB,SAAU,UAAsC,oBAAoB,GAEpE,QACG,UAAsC,oBAAoB,CAC3D;CAEN,QAAQ;EACN,QAAQ,KAAA;CACV;CAEA,MAAM,UACJ,SAAS,KAAK,KAAK,MAAM,QAAQ,MAAM,UAAU,IAAK,MAAM,aAA2B,KAAA;CACzF,OAAO,eAAe,UAAU,QAAQ,SAAS;CACjD,gBAAgB,QAAQ,KAAK;AAC/B;AAGA,SAAS,uBAAuB,QAAqB,WAA0B;CAC7E,OAAO,eAAe;CACtB,IAAI;CACJ,IAAI;EACF,IACE,SAAS,SAAS,KAClB,SAAU,UAAsC,oBAAoB,GAEpE,QACG,UAAsC,oBAAoB,CAC3D;OACG;GACL,MAAM,OAAO,iBAAiB,SAAS,CAAC,CAAC,KAAK;GAC9C,IAAI,MAAM,QAAS,KAAK,MAAM,IAAI,CAAC,CAAyB;EAC9D;CACF,QAAQ;EACN,QAAQ,KAAA;CACV;CACA,gBAAgB,QAAQ,KAAK;AAC/B;AAMA,SAAgB,2BACd,QACA,QACG;CACH,OAAO,IAAI,MAAM,QAAkB,EACjC,IAAI,QAAQ,MAAM,UAAU;EAC1B,IAAI,SAAS,YACX,OAAO,OAAO,GAAG,SAAoB;GACnC,MAAM,SAAS,MAAO,OAAwB,SAAS,GAAG,IAAI;GAC9D,MAAM,UAAU,KAAK;GACrB,MAAM,WACJ,SAAS,OAAO,KAAK,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU,KAAA;GAC/E,IAAI,aAAa,qBACf,4BAA4B,QAAQ,MAAM;QACrC,IAAI,aAAa,eACtB,uBAAuB,QAAQ,MAAM;GAEvC,OAAO;EACT;EAEF,MAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,MAAM;EAC9C,OAAO,OAAO,UAAU,aAAa,MAAM,KAAK,MAAM,IAAI;CAC5D,EACF,CAAC;AACH;;;ACpJA,MAAM,oCAAoC;AAC1C,MAAM,uCAAuC;AAS7C,MAAM,sCAAsC,CAAC,IAAI;AAIjD,SAAgB,6BAA6B,WAA2C;CACtF,MAAM,aAAa,WAAW,KAAK,CAAC,CAAC,YAAY;CACjD,IAAI,CAAC,YAAY,OAAA;CACjB,IACE,oCAAoC,SAClC,UACF,GAEA,OAAO;CAET,MAAM,IAAI,MACR,yCAAyC,WAAW,KAAK,KAAK,GAAG,yBAAyB,oCAAoC,KAAK,IAAI,EAAE,EAC3I;AACF;AAEA,SAAS,mBAAmB,OAAuB;CACjD,OAAO,MAAM,WAAW,MAAM,MAAM,CAAC,CAAC,WAAW,MAAK,MAAK;AAC7D;AAEA,SAAS,mBAAmB,QAAkC;CAC5D,QAAQ,OAAO,WAAW,CAAC,EAAA,CACxB,QAAQ,SAA0D,KAAK,SAAS,MAAM,CAAC,CACvF,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,IAAI;AACd;AAEA,SAAS,sBAAsB,QAA4C;CACzE,MAAM,OAAO,mBAAmB,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,4CAA4C;CACvE,MAAM,SAAS,KAAK,MAAM,IAAI;CAC9B,IAAI,CAAC,OAAO,OAAO,SACjB,MAAM,IAAI,MAAM,0DAA0D;CAC5E,OAAO;AACT;AAEA,SAAS,mBAAmB,OAAuB;CACjD,MAAM,UAAU,MAAM,KAAK;CAC3B,IAAI,WAAW,KAAK,OAAO,GAAG,OAAO;CACrC,OAAO,gBAAgB;AACzB;AAEA,SAAS,oBACP,UACA,IACA,OACM;CACN,SAAS,KAAK;EAAE;EAAI,OAAO,SAAS;CAAgB,CAAC;AACvD;AAEA,SAAS,mBACP,OACA,IACA,UACgC;CAChC,MAAM,QAAQ,MAAM,OAAO,SAAS,MAAM,UAAU,MAAM,OAAO,EAAE;CACnE,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,IAAI,MAAM,OAAO,OAAO;EACtB,oBAAoB,UAAU,IAAI,MAAM,KAAK;EAC7C,OAAO,CAAC;CACV;CACA,OAAO,MAAM,WAAW,CAAC;AAC3B;AAEA,SAAS,0BACP,OACA,QACA,UACgC;CAChC,QAAQ,MAAM,OAAO,WAAW,CAAC,EAAA,CAC9B,QAAQ,UAAU,MAAM,IAAI,WAAW,MAAM,CAAC,CAAC,CAC/C,SAAS,UAAU;EAClB,IAAI,MAAM,OAAO,OAAO;GACtB,oBAAoB,UAAU,MAAM,MAAM,QAAQ,MAAM,KAAK;GAC7D,OAAO,CAAC;EACV;EACA,OAAO,MAAM,WAAW,CAAC;CAC3B,CAAC;AACL;AAEA,eAAe,eACb,cACA,SACA,SAC2B;CAC3B,MAAM,SAAU,MAAM,aAAa,SACjC;EACE,MAAM;EACN,WAAW;GACT;GACA,SAAS,QAAQ,KAAK,WAAW;IAC/B,GAAG;IACH,OAAO,mBAAmB,MAAM,KAAK;GACvC,EAAE;GACF,2BAA2B;EAC7B;CACF,GACA,KAAA,GACA;EACE,SAAS;EACT,iBAAiB;CACnB,CACF;CACA,IAAI,OAAO,SAAS,MAAM,IAAI,MAAM,mBAAmB,MAAM,KAAK,0BAA0B;CAC5F,MAAM,SAAS,sBAAsB,MAAM;CAC3C,8BAA8B,OAAO,OAAO,SAAS,OAAO;CAC5D,OAAO;AACT;AAuBA,SAAS,oBAAoB,SAAgD;CAW3E,OAAO;EACL,IAAI;EACJ,OAAO;GACL,+BAA+B,mBAAmB,OAAO,EAAE;GAC3D;GACA;EACF,CAAC,CAAC,KAAK,GAAG;CACZ;AACF;AASA,SAAS,oBAAoB,SAAgD;CAC3E,OAAO;EACL,IAAI;EACJ,OAAO;GACL;GACA,+BAA+B,mBAAmB,OAAO,EAAE;GAC3D;GACA;EACF,CAAC,CAAC,KAAK,GAAG;CACZ;AACF;AAEA,SAAS,YAAY,cAA8B;CACjD,OAAO,oBAAoB,aAAa,6BAA6B,aAAa,mCAAmC,aAAa,8CAA8C,aAAa,qCAAqC,aAAa,4BAA4B,aAAa;AAC1R;AAEA,SAAS,YAAY,cAA8B;CAIjD,OAAO,aAAa,aAAa,qBAAqB,aAAa,oBAAoB,aAAa,0BAA0B,aAAa,wBAAwB,aAAa,cAAc,iBAD1J,aAAa,2BAA2B,aAAa,aACgH;AAC3M;AAMA,SAAgB,wBAAwB,SAAyB;CAC/D,OAAO;EACL;EACA,+BAA+B,mBAAmB,OAAO,EAAE;EAC3D;EACA;CACF,CAAC,CAAC,KAAK,GAAG;AACZ;AAEA,SAAgB,oBAAoB,MAAsB;CACxD,OAAO;EACL;EACA,+BAA+B,mBAAmB,IAAI,EAAE;EACxD;EACA;CACF,CAAC,CAAC,KAAK,GAAG;AACZ;AAqCA,MAAM,yBAAiD;CAAE,WAAW;CAAG,SAAS;CAAG,YAAY;AAAE;AAEjG,SAAgB,qBACd,cACA,SAC6B;CAC7B,IAAI,aAAa,WAAW,GAAG,OAAO,KAAA;CAEtC,MAAM,mBAAmB,KAAK,IAC5B,GAAG,aAAa,KAAK,QAAQ,uBAAuB,YAAY,IAAI,aAAa,KAAK,OAAO,CAAC,CAChG;CACA,MAAM,eACJ,OAAO,QAAQ,sBAAsB,CAAC,CAAC,MAAM,GAAG,UAAU,SAAS,gBAAgB,CAAC,GAAG,MACvF,YAAY,aAAa,EAAE,GAAG,aAAa,KAC3C;CACF,MAAM,qBAAqB,aAAa,QACrC,QAAQ,YAAY,IAAI,aAAa,MAAM,YAC9C;CACA,MAAM,UAAU,mBAAmB,QAAQ,MAAM,QAAQ;EAGvD,QAFe,YAAY,IAAI,UAAU,KAAK,OAAO,sBACrC,YAAY,KAAK,UAAU,KAAK,OAAO,qBAC7B,MAAM;CAClC,GAAG,mBAAmB,EAAG;CAEzB,MAAM,aAAa,QAAQ,QAA6C,MAAM,QAAQ;EACpF,MAAM,WAAW,YAAY,IAAI,QAAQ,KAAK,OAAO;EACrD,MAAM,YAAY,OACb,YAAY,KAAK,QAAQ,KAAK,OAAO,oBACtC,OAAO;EACX,OAAO,CAAC,QAAQ,WAAW,YAAY,MAAM;CAC/C,GAAG,KAAA,CAAS;CAEZ,OAAO;EACL,UAAU;EACV,OAAO;EACP,SAAS,YAAY,QAAQ,UAAU,KAAK;EAC5C,cAAc,YAAY,QAAQ,eAAe,KAAK;EACtD,YAAY,YAAY,QAAQ,aAAa,KAAK;EAClD,GAAI,aACA,EACE,mBAAmB;GACjB,SAAS,YAAY,WAAW,UAAU,KAAK;GAC/C,WAAW,YAAY,WAAW,YAAY,KAAK;GACnD,OAAO,OAAO,WAAW,YAAY,EAAE;EACzC,EACF,IACA,CAAC;CACP;AACF;AAEA,SAAgB,0BAA0B,OAAgC;CACxE,OAAO,MAAM,UAAU,eACnB,2CAA2C,MAAM,MAAM,YAAY,MAAM,QAAQ,MACjF,uCAAuC,MAAM,MAAM,YAAY,MAAM,QAAQ;AACnF;AAEA,SAAS,uBAAuB,SAAuD;CACrF,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,UAAU,4BAA4B,SAAS,QAAQ,CAAC,CAAC;AAChG;AAEA,SAAS,4BACP,SACA,OAC+B;CAC/B,MAAM,wBAAwB,MAAM,KAClC,EAAE,QAAQ,KAAK,IAAI,QAAQ,GAAG,CAAC,EAAE,IAChC,GAAG,UAAU,IAAI,QAAQ,GAC5B;CACA,MAAM,gBAAgB;EAAC;EAAK,GAAG;EAAuB;CAAU;CAChE,MAAM,gBAAgB,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,UAAU,IAAI,QAAQ,GAAG;CACjF,MAAM,oBAAoB,cACvB,KAAK,cAAc,UAAU;EAG5B,OAAO,KAAK,aAAa,eADvB,UAAU,cAAc,SAAS,IAAI,aAAa,sBAAsB,OACnB;CACzD,CAAC,CAAC,CACD,KAAK,EAAE;CACV,MAAM,yBAAyB,sBAAsB,KAClD,iBAAiB,GAAG,aAAa,qBACpC;CACA,MAAM,kBAAkB,cAAc,cAAc,SAAS;CAC7D,MAAM,uBAAuB,cAAc,cAAc,SAAS;CAClE,OAAO;EACL,IAAI,qBAAqB;EACzB,OAAO;GACL,+BAA+B,mBAAmB,OAAO,EAAE,KAAK;GAChE,2EAA2E,uBAAuB,SAAS,IAAI,QAAQ,uBAAuB,KAAK,OAAO,MAAM;GAChK,+JAA+J,gBAAgB,+BAA+B,MAAM,YAAY,qBAAqB,qCAAqC,qBAAqB,yBAAyB,qBAAqB,iDAAiD,qBAAqB,gDAAgD,cAAc,KAAK,iBAAiB,GAAG,aAAa,SAAS,CAAC,CAAC,KAAK,IAAI,EAAE,mBAAmB,cAAc,KAAK,iBAAiB,YAAY,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,oBAAoB,cAAc,IAAI,WAAW,CAAC,CAAC,KAAK,IAAI,EAAE;GACxrB;GACA;EACF,CAAC,CAAC,KAAK,GAAG;CACZ;AACF;AAEA,SAAS,sBAAsB,SAAuD;CACpF,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,UAAU,2BAA2B,SAAS,QAAQ,CAAC,CAAC;AAC/F;AAEA,SAAS,2BAA2B,SAAiB,OAA8C;CACjG,MAAM,wBAAwB,MAAM,KAClC,EAAE,QAAQ,KAAK,IAAI,QAAQ,GAAG,CAAC,EAAE,IAChC,GAAG,UAAU,IAAI,QAAQ,GAC5B;CACA,MAAM,gBAAgB;EAAC;EAAY,GAAG;EAAuB;CAAG;CAChE,MAAM,gBAAgB,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,UAAU,IAAI,QAAQ,GAAG;CACjF,MAAM,oBAAoB,cACvB,KAAK,cAAc,UAAU;EAG5B,OAAO,KAAK,aAAa,eADvB,UAAU,cAAc,SAAS,IAAI,MAAM,sBAAsB,OACZ;CACzD,CAAC,CAAC,CACD,KAAK,EAAE;CACV,MAAM,yBAAyB,sBAAsB,KAClD,iBAAiB,GAAG,aAAa,qBACpC;CACA,MAAM,qBAAqB,cAAc;CACzC,MAAM,uBAAuB,cAAc,cAAc,SAAS;CAClE,OAAO;EACL,IAAI,oBAAoB;EACxB,OAAO;GACL,2BAA2B;GAC3B,sBAAsB,mBAAmB,OAAO,EAAE,0EAA0E,uBAAuB,SAAS,IAAI,QAAQ,uBAAuB,KAAK,OAAO,MAAM;GACjN,8JAA8J,mBAAmB,kCAAkC,MAAM,YAAY,qBAAqB,qCAAqC,qBAAqB,yBAAyB,qBAAqB,iDAAiD,qBAAqB,gDAAgD,cAAc,KAAK,iBAAiB,GAAG,aAAa,SAAS,CAAC,CAAC,KAAK,IAAI,EAAE,mBAAmB,cAAc,KAAK,iBAAiB,YAAY,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,oBAAoB,cAAc,IAAI,WAAW,CAAC,CAAC,KAAK,IAAI,EAAE;GAC7rB;GACA;EACF,CAAC,CAAC,KAAK,GAAG;CACZ;AACF;AAEA,SAAS,qBACP,SACA,gBAC+B;CAC/B,OAAO;EACL,IAAI;EACJ,OAAO;GACL,+BAA+B,mBAAmB,OAAO,EAAE,yCAAyC,mBAAmB,cAAc,EAAE;GACvI;GACA;EACF,CAAC,CAAC,KAAK,GAAG;CACZ;AACF;AAKA,SAAS,0BAA0B,SAAgD;CACjF,OAAO;EACL,IAAI;EACJ,OAAO;GACL,+BAA+B,mBAAmB,OAAO,EAAE;GAC3D;GACA;EACF,CAAC,CAAC,KAAK,GAAG;CACZ;AACF;AAkGA,SAAgB,0BAA0B,gBAA6C;CAGrF,OAAO,QAAQ,cAAc;AAC/B;AAEA,SAAgB,uBACd,SACA,gBACsC;CACtC,MAAM,cAAc,aAAqB,cACvC;EACE,qCAAqC,mBAAmB,WAAW,EAAE;EACrE;EACA,2BAA2B,mBAAmB,SAAS,EAAE;CAC3D,CAAC,CAAC,KAAK,EAAE;CACX,OAAO,CACL;EAAE,IAAI;EAA6B,OAAO,WAAW,SAAS,cAAc;CAAE,GAC9E;EAAE,IAAI;EAA4B,OAAO,WAAW,gBAAgB,OAAO;CAAE,CAC/E;AACF;AASA,SAAS,eACP,OACA,SACgC;CAChC,MAAM,YAA4C,CAAC;CACnD,MAAM,QAAQ,SAAwB;EACpC,IAAI,MAAM,QAAQ,IAAI,GAAG;GACvB,KAAK,MAAM,SAAS,MAAM,KAAK,KAAK;GACpC;EACF;EACA,IAAI,QAAQ,OAAO,SAAS,UAAU;GACpC,MAAM,YAAY;GAClB,IAAI,QAAQ,SAAS,GAAG,UAAU,KAAK,SAAS;GAChD,KAAK,MAAM,UAAU,OAAO,OAAO,SAAS,GAAG,KAAK,MAAM;EAC5D;CACF;CACA,KAAK,KAAK;CACV,OAAO;AACT;AAQA,SAAgB,iBAAiB,OAAyB;CACxD,IAAI,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,OAAO,OAAO;CACrE,IAAI,OAAO,UAAU,UAAU,OAAO,UAAU;CAChD,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,aAAa,MAAM,KAAK,CAAC,CAAC,YAAY;EAC5C,OACE,eAAe,MAAM,eAAe,WAAW,eAAe,OAAO,eAAe;CAExF;CACA,OAAO,UAAU;AACnB;AAKA,SAAgB,mBAAmB,OAA0C;CAC3E,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAChD,MAAM,QAAQ,eAAe,QAAQ,cAAc,OAAO,UAAU,eAAe,QAAQ;CAC3F,IAAI,MAAM,SAAS,GAAG,OAAO;CAC7B,MAAM,QAAQ,eACZ,QACC,cAAc,OAAO,UAAU,sBAAsB,YAAY,EAAE,aAAa,UACnF;CACA,MAAM,aAAa,MAAM,KAAK,SAAS,OAAO,KAAK,UAAU,CAAC;CAC9D,MAAM,wBAAwB,MAC3B,MAAM,GAAG,EAAE,CAAC,CACZ,QAAQ,SAAS,iBAAiB,KAAK,cAAc,CAAC,CAAC,CACvD,KAAK,SAAS,OAAO,KAAK,UAAU,CAAC;CACxC,OAAO;EAIL,MAAM,MAAM,SAAS;EACrB;EACA,wBAAwB;EACxB,sBAAsB,MAAM,QAAQ,OAAO,SAAS,QAAQ,OAAO,KAAK,iBAAiB,GAAG,CAAC;CAC/F;AACF;AAWA,SAAgB,mBACd,cACA,aACe;CACf,MAAM,YAAY,SAAmE;EACnF,MAAM,QAAQ,KAAK;EACnB,IAAI,CAAC,OAAO,OAAO;EAEnB,OAAO,mBADW,OAAO,QAAQ,MAAM,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC,EAChC;CACrC;CACA,MAAM,WAAW,SAAS,YAAY;CACtC,MAAM,UAAU,SAAS,WAAW;CACpC,OAAO;EACL,iBAAiB;EACjB,kBAAkB;EAClB,aAAA;EACA,aAAa,aAAa,QAAQ,YAAY;EAC9C;EACA;CACF;AACF;AAEA,SAAS,mBAAmB,MAAgD;CAC1E,OAAO,KAAK,KAAK,QAAQ;EACvB,MAAM,YAAY,OAAO,IAAI,gBAAgB,MAAM;EACnD,MAAM,WAAW,OAAO,IAAI,uBAAuB,EAAE;EACrD,MAAM,SAAS,IAAI,qBAAqB;EAExC,OAAO,KAAK,UAAU,IAAI,SAAS,IADtB,IAAI,WAAW,GACgB,0BAA0B,OAAO;CAC/E,CAAC;AACH;AAEA,SAAS,YAAY,OAAoC;CACvD,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG,OAAO;CAChE,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,GAAG;EAC7C,MAAM,SAAS,OAAO,KAAK;EAC3B,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS,KAAA;CAC5C;AAEF;AAiBA,SAAS,YAAY,GAAG,QAAuC;CAC7D,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,SAAS,YAAY,KAAK;EAChC,IAAI,WAAW,KAAA,GAAW,OAAO;CACnC;AAEF;AAEA,SAAS,YAAY,GAAG,QAAuC;CAC7D,KAAK,MAAM,SAAS,QAClB,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,GAAG,OAAO,MAAM,KAAK;AAGrE;AAEA,SAAS,mBAAmB,OAAuB;CACjD,IAAI,SAAS,KAAM,OAAO;CAC1B,IAAI,SAAS,IAAK,OAAO;CACzB,IAAI,SAAS,IAAK,OAAO;CACzB,OAAO;AACT;AAEA,SAAS,mBAAmB,OAAuB;CACjD,IAAI,UAAU,cAAc,UAAU,QAAQ,OAAO;CACrD,IAAI,UAAU,UAAU,OAAO;CAC/B,OAAO;AACT;AAEA,SAAS,YACP,SACA,WACA,cACU;CACV,MAAM,UAAoB,CAAC;CAE3B,MAAM,aAAa,UAChB,KAAK,QAAQ,YAAY,IAAI,QAAQ,CAAC,CAAC,CACvC,QAAQ,UAA2B,QAAQ,KAAK,CAAC;CACpD,IAAI,WAAW,SAAS,GAAG,QAAQ,KAAK,WAAW,CAAC,GAAG,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG;CAExF,MAAM,eAAe,aAAa,QAAQ,QAAQ,IAAI,iBAAiB,SAAS,CAAC,CAAC;CAClF,MAAM,cAAc,aAAa,QAAQ,QAAQ,IAAI,iBAAiB,QAAQ,CAAC,CAAC;CAChF,IAAI,eAAe,GACjB,QAAQ,KAAK,kCAAkC,aAAa,mBAAmB;CACjF,IAAI,cAAc,GAChB,QAAQ,KAAK,iCAAiC,YAAY,0BAA0B;CAEtF,OAAO,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC;AAC7B;AASA,SAAS,gBAAgB,SAAkE;CACzF,MAAM,MAAM,QAAQ;CAOpB,OAAO,CAAC,GANK,MAAM,QAAQ,GAAG,IAC1B,IAAI,QACD,QACC,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,GAAG,CACjE,IACA,CAAC,CACU,CAAC,CACb,MACE,GAAG,OACD,YAAY,EAAE,oBAAoB,KAAK,MAAM,YAAY,EAAE,oBAAoB,KAAK,EACzF,CAAC,CACA,MAAM,GAAG,EAAE;AAChB;AAEA,MAAM,mBAAmB;CAAC;CAAY;CAAQ;CAAU;AAAK;AAE7D,SAAS,wBAAwB,WAA+D;CAC9F,MAAM,SAAS,UACZ,KAAK,QAAQ,YAAY,IAAI,aAAa,CAAC,EAAE,YAAY,CAAC,CAAC,CAC3D,QAAQ,UACP,QAAQ,SAAU,iBAAuC,SAAS,KAAK,CAAC,CAC1E;CACF,IAAI,OAAO,WAAW,GAAG,OAAO,KAAA;CAChC,OAAO,iBAAiB,MAAM,cAAc,OAAO,SAAS,SAAS,CAAC;AACxE;AAEA,SAAS,iBACP,SACA,WACgC;CAChC,MAAM,UAA0C,CAAC;CACjD,IAAI,YAAY,QAAQ,kBAAkB,MAAM,KAAA,GAC9C,QAAQ,KAAK;EACX,QAAQ;EACR,OAAO;EACP,QAAQ;EACR,QAAQ,CAAC,cAAc,YAAY;CACrC,CAAC;CAEH,IAAI,UAAU,SAAS,GACrB,QAAQ,KAAK;EACX,QAAQ;EACR,OAAO;EACP,QAAQ;EACR,QAAQ,UAAU,KAAK,SAAS;GAC9B,OAAO,IAAI;GACX,YAAY,IAAI;EAClB,EAAE;CACJ,CAAC;CAEH,OAAO;AACT;AAEA,SAAS,uBAAuB,KAAmE;CACjG,MAAM,YAAY,MAAM,QAAQ,IAAI,aAAa,IAC5C,IAAI,gBACL,CAAC;CACL,OAAO,UAAU,UAAU,SAAS;AACtC;AAEA,SAAS,mBAAmB,MAAsE;CAChG,OAAO,KAAK,KAAK,QAAQ;EACvB,MAAM,WAAW,uBAAuB,GAAG;EAC3C,IAAI,CAAC,UAAU,OAAO;EACtB,OAAO;GACL,GAAG;GACH,gBAAgB,IAAI,qBAAqB,SAAS;GAClD,UAAU,IAAI,eAAe,SAAS;GACtC,sBAAsB,IAAI,2BAA2B,SAAS;GAC9D,qBAAqB,IAAI,0BAA0B,SAAS;GAC5D,aAAa,IAAI,kBAAkB,SAAS;GAC5C,YAAY,IAAI,iBAAiB,SAAS;EAC5C;CACF,CAAC;AACH;AAEA,MAAM,2BAA2B;AACjC,MAAM,0BAA0B;AAChC,MAAM,0BAA0B;AAChC,MAAM,gCAAgC;AACtC,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;;;;;;;AAQ3B,SAAgB,8BACd,cACQ;CACR,IAAI,aAAa,WAAW,GAAG,OAAO;CACtC,IAAI,cAAc;CAClB,KAAK,MAAM,OAAO,cAAc;EAC9B,MAAM,MAAM,YAAY,IAAI,iBAAiB,KAAK;EAElD,MAAM,UADU,YAAY,IAAI,WAAW,KAAK,MACtB,4BAA4B,OAAO;EAC7D,eAAe,SAAS,MAAM,0BAA0B;CAC1D;CAEA,MAAM,SAAS,KAAK,MAAM,IADX,KAAK,IAAI,aAAa,6BACF,CAAC,IAAI,KAAK,MAAM,OAAiC;CACpF,OAAO,KAAK,IACV,oBACA,sBAAuB,KAA4C,MACrE;AACF;AAEA,SAAgB,eACd,SACA,WACA,cACyB;CACzB,MAAM,cAAc,YAAY,QAAQ,kBAAkB;CAM1D,MAAM,cAAc,oBAAoB,QAAQ,kBAAkB;CAClE,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,gBAAgB,cAAc,KAAA,IAAY;CAChD,MAAM,QAAQ,iBAAiB,8BAA8B,YAAY;CACzE,IAAI,QACF,mBACC,eAAe,aAAa,WAAW,IAAI,aAAa,mBAAmB,KAAK;CACnF,MAAM,UAAU,YAAY,SAAS,WAAW,YAAY;CAC5D,IAAI,aACF,QAAQ,KACN,6HACF;CAIF,MAAM,eAAe,kBAAkB,KAAA,IAAY,mBAAmB,aAAa,IAAI,KAAA;CACvF,IACE,kBACA,gBACA,iBAAiB,YAAY,IAAI,iBAAiB,cAAc,GAChE;EACA,QAAQ;EACR,QAAQ,KACN,uCAAuC,aAAa,iCAAiC,eAAe,oDACtG;CACF;CACA,OAAO;EACL;EACA;EACA,GAAI,gBAAgB,KAAA,IAAY,EAAE,eAAe,YAAY,IAAI,CAAC;EAClE,GAAI,cAAc,EAAE,YAAY,WAAW,IAAI,CAAC;EAChD,YACE,kBAAkB,KAAA,KAAa,iBAC3B,SACA,aAAa,SAAS,IACpB,WACA;EACR,gBACE,UAAU,aACN,6FACA,mBAAmB,KAAK;EAC9B;EACA,SAAS,iBAAiB,SAAS,SAAS;CAC9C;AACF;AAEA,SAAS,gBAAgB,OAAwB;CAC/C,MAAM,SAAS,YAAY,KAAK;CAChC,IAAI,WAAW,KAAA,GAAW,OAAO,OAAO,SAAS,SAAS;CAC1D,OAAO,OAAO,UAAU,MAAM,IAAI,OAAO,SAAS,IAAI,OAAO,QAAQ,CAAC;AACxE;AAEA,SAAS,iBAAiB,OAAsC;CAC9D,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,MAAM,IAAI,MAAM;CACjD,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK;AAE9D;AAEA,SAAS,oBACP,OACA,UACyB;CACzB,IAAI,CAAC,MAAM,QAAQ,MAAM,QAAQ,GAAG,OAAO;CAC3C,MAAM,kBAAkB,IAAI,IAC1B,SACG,KACE,SACC,CACE,OAAO,KAAK,eAAe,WACvB,KAAK,aACL,OAAO,KAAK,UAAU,WACpB,KAAK,QACL,IACN,iBAAiB,KAAK,gBAAgB,CACxC,CACJ,CAAC,CACA,QACE,UACC,QAAQ,MAAM,EAAE,KAAK,MAAM,QAAQ,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC,SAAS,CACtE,CACJ;CACA,OAAO;EACL,GAAG;EACH,OAAO,MAAM,QAAQ,CAAC,KAAK,SAAS;GAClC,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG,OAAO;GAC7E,MAAM,SAAS;GACf,MAAM,UACJ,OAAO,OAAO,eAAe,WACzB,OAAO,aACP,OAAO,OAAO,UAAU,WACtB,OAAO,QACP;GACR,MAAM,eAAe,gBAAgB,IAAI,OAAO;GAChD,OAAO,eAAe;IAAE,GAAG;IAAQ,eAAe;GAAa,IAAI;EACrE,CAAC;CACH;AACF;AAEA,SAAS,eACP,SACA,SACA,MACA,SACyB;CACzB,MAAM,wBAAQ,IAAI,IAAqC;CACvD,MAAM,IAAI,SAAS;EACjB,IAAI;EACJ;EACA,WAAW;EACX,QAAQ,iBAAiB,QAAQ,iBAAiB,KAAK,CAAC;EACxD,GAAI,iBAAiB,QAAQ,gBAAgB,IACzC,EAAE,eAAe,iBAAiB,QAAQ,gBAAgB,EAAE,IAC5D,CAAC;EACL,GAAI,iBAAiB,QAAQ,mBAAmB,CAAC,EAAE,SAC/C,EAAE,kBAAkB,iBAAiB,QAAQ,mBAAmB,EAAE,IAClE,CAAC;EACL,GAAI,YAAY,QAAQ,kBAAkB,MAAM,KAAA,IAC5C,EAAE,YAAY,YAAY,QAAQ,kBAAkB,EAAE,IACtD,CAAC;EACL,GAAI,YAAY,QAAQ,kBAAkB,IACtC,EAAE,YAAY,YAAY,QAAQ,kBAAkB,EAAE,IACtD,CAAC;EACL,OAAO,CAAC,SAAS;CACnB,CAAC;CACD,MAAM,QAAwC,CAAC;CAC/C,MAAM,aAAa,OAAe,aAAuC;EACvE,MAAM,WAAW,MAAM,IAAI,KAAK,KAAK;GACnC,IAAI;GACJ,SAAS;GACT,WAAW;GACX,QAAQ,CAAC;EACX;EACA,MAAM,SAAS,iBAAiB,WAAW,SAAS,KAAK,SAAS;EAClE,MAAM,eAAe,iBAAiB,WAAW,gBAAgB,KAAK,SAAS;EAC/E,MAAM,kBACJ,iBAAiB,WAAW,mBAAmB,KAAK,SAAS;EAC/D,MAAM,YAAY,YAAY,WAAW,aAAa,KAAK,SAAS;EACpE,MAAM,YACJ,mBAAmB,YAAY,WAAW,aAAa,CAAC,KAAK,SAAS;EACxE,MAAM,IAAI,OAAO;GACf,GAAG;GACH;GACA,GAAI,eAAe,EAAE,eAAe,aAAa,IAAI,CAAC;GACtD,GAAI,MAAM,QAAQ,eAAe,KAAK,gBAAgB,SAAS,IAC3D,EAAE,kBAAkB,gBAAgB,IACpC,CAAC;GACL,GAAI,cAAc,KAAA,IAAY,EAAE,YAAY,UAAU,IAAI,CAAC;GAC3D,GAAI,YAAY,EAAE,YAAY,UAAU,IAAI,CAAC;EAC/C,CAAC;CACH;CACA,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,UAAU,MAAM,QAAQ,IAAI,OAAO,IAAI,IAAI,UAAU,IAAI;EAC/D,MAAM,OAAO,MAAM,QAAQ,OAAO,IAAI,QAAQ,IAAI,MAAM,IAAI,CAAC;EAC7D,MAAM,YAAY,MAAM,QAAQ,IAAI,aAAa,IAC5C,IAAI,gBACL,CAAC;EACL,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;GACnD,MAAM,QAAQ,KAAK;GACnB,UAAU,OAAO,UAAU,MAAM;EACnC;EACA,MAAM,WAAW,OAAO,IAAI,wBAAwB,WAAW,IAAI,sBAAsB;EACzF,IAAI,UAAU;GACZ,MAAM,gBAAgB,iBAAiB,IAAI,0BAA0B,KAAK,CAAC;GAC3E,MAAM,eACJ,iBAAiB,IAAI,yBAAyB,KAC9C,iBAAiB,IAAI,kBAAkB,KACvC,CAAC;GACH,MAAM,IAAI,UAAU;IAClB,IAAI;IACJ,SAAS;IACT,WAAW;IACX,QAAQ;IACR,GAAI,aAAa,SAAS,IAAI,EAAE,eAAe,aAAa,IAAI,CAAC;IACjE,OAAO,CAAC,UAAU;GACpB,CAAC;EACH;EACA,KAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,SAAS,GAAG,SAAS,GAAG;GAIvD,MAAM,QAHY,MAAM,QAAQ,IAAI,aAAa,IAC5C,IAAI,gBACL,CAAC,EAAA,CACkB,UAAU;GACjC,MAAM,KAAK;IACT,QAAQ,KAAK;IACb,QAAQ,KAAK,QAAQ;IACrB,WAAW;IACX,YAAY,KAAK,qBAAqB;IACtC,gBAAgB,KAAK,qBAAqB;IAC1C,UAAU,KAAK,eAAe;IAC9B,aAAa,KAAK;IAClB,YAAY,KAAK;IACjB,WAAW,IAAI;GACjB,CAAC;EACH;CACF;CACA,MAAM,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC;CACnC,OAAO,oBACL,sBAAsB;EACpB,QAAQ;EACR,OAAO;EACP;EACA,OAAO,CAAC;EACR,cAAc,CAAC;EACf,UAAU;GAAE;GAAS;GAAS,+BAAc,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE;CACvE,CAAC,GACD,QACF;AACF;AAEA,eAAsB,YACpB,cACA,SAKC;CACD,MAAM,UAAU,QAAQ,QAAQ,KAAK;CACrC,MAAM,UAAU,QAAQ,QAAQ,KAAK;CACrC,MAAM,eAAe,QAAQ,gBAAgB,KAAK,KAAK;CACvD,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,qBAAqB;CACnD,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,qBAAqB;CAKnD,MAAM,QAAQ,uBAAuB;CACrC,MAAM,gBAAgB,2BAA2B,cAAc,KAAK;CAUpE,IAAI,oBAAoB;CACxB,IAAI,cAKF,oBAAoB,CAAC,YADD,mBAAmB,MAHZ,eAAe,eAAe,SAAS,CAChE,0BAA0B,YAAY,CACxC,CAAC,GACoD,0BAA0B,CAAC,CACrC,CAAC,CAAC,EAAE,GAAG,UAAU;CAE9D,MAAM,iBAAiB,gBAAgB,CAAC,oBAAoB,eAAe;CAwB3E,MAAM,QAAQ,MAAM,eAAe,eAAe,SAAS;EArBzD,oBAAoB,OAAO;EAC3B,oBAAoB,OAAO;EAC3B;GAAE,IAAI;GAAwB,OAAO,wBAAwB,OAAO;EAAE;EACtE,GAAG,uBAAuB,OAAO;EACjC,GAAG,sBAAsB,OAAO;EAChC,GAAI,iBACA,CAAC,qBAAqB,SAAS,cAAc,CAAC,IAC9C,CACE;GACE,IAAI;GACJ,OACE;EACJ,CACF;EAIJ,GAAI,0BAA0B,cAAc,IACxC,uBAAuB,SAAS,cAAc,IAC9C,CAAC;CAE0D,CAAC;CAClE,MAAM,uBAAuC,CAAC;CAI9C,MAAM,qBAAqB,mBAAmB,OAAO,mBAAmB,oBAAoB;CAC5F,IAAI,mBAAmB,WAAW,GAChC,OAAO;EACL,aAAa,oBAAoB,QAAQ,GAAG,QAAQ,wCAAwC,QAAQ;EACpG,mBAAmB;GACjB,QAAQ;GACR,MAAM;GACN,OAAO;IACL,SAAS;KAAE;KAAS,WAAW,CAAC,OAAO;IAAE;IACzC,YAAY,CAAC,OAAO;IACpB,OAAO,WAAW,KAAK;GACzB;EACF;EACA,WAAW;GACT,QAAQ;GACR,OAAO,CAAC;GACR,OAAO,CAAC;GACR,OAAO,CAAC;GACR,cAAc,CAAC;GACf,UAAU;IAAE;IAAS;IAAS,+BAAc,IAAI,KAAK,EAAA,CAAE,YAAY;GAAE;EACvE;CACF;CAEF,MAAM,UAAmC;EACvC;EACA,GAAI,mBAAmB,MAAM,CAAC;EAC9B,GAAI,mBAAmB,OAAO,mBAAmB,oBAAoB,CAAC,CAAC,MAAM,CAAC;CAChF;CACA,MAAM,YAAY,gBAAgB,OAAO;CACzC,MAAM,WAAW,mBACf,0BAA0B,OAAO,sBAAsB,oBAAoB,CAC7E;CACA,MAAM,UAAU,mBACd,0BAA0B,OAAO,qBAAqB,oBAAoB,CAC5E;CACA,MAAM,cAAc,iBAChB,mBAAmB,OAAO,oBAAoB,oBAAoB,IAClE,CAAC;CACL,MAAM,gBAAgB,0BAA0B,cAAc,IAC1D,mBACE,mBAAmB,OAAO,6BAA6B,oBAAoB,GAC3E,mBAAmB,OAAO,4BAA4B,oBAAoB,CAC5E,IACA,KAAA;CACJ,MAAM,eAAe,CAAC,GAAG,UAAU,GAAG,OAAO;CAQ7C,MAAM,yBAAyB,qBAAqB,QACjD,YACC,QAAQ,GAAG,WAAW,oBAAoB,KAAK,QAAQ,GAAG,WAAW,mBAAmB,CAC5F;CACA,MAAM,yBAAyB,uBAAuB,WAAW;CAMjE,MAAM,yBAAyB,mBAC7B,OACA,wBACA,oBACF;CACA,MAAM,wBAAwB,qBAAqB,wBAAwB,CAAC,CAAC;CAC7E,IAAI,oBAAoD,CAAC;CACzD,IAAI,uBAAuB,cACzB,IAAI;EAIF,oBAAoB,mBAAmB,MAHf,eAAe,eAAe,SAAS,CAC7D;GAAE,IAAI;GAAoB,OAAO,oBAAoB,sBAAsB,YAAY;EAAE,CAC3F,CAAC,GACiD,oBAAoB,oBAAoB;CAC5F,SAAS,OAAO;EAMd,oBACE,sBACA,oBACA,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CACvD;CACF;CAEF,MAAM,aAAa,qBAAqB,wBAAwB,iBAAiB;CACjF,MAAM,YAAY,eAAe,SAAS,SAAS,cAAc,OAAO;CACxE,MAAM,OAAO,eAAe,SAAS,WAAW,YAAY;CAC5D,MAAM,gBAAgB,YAAY,QAAQ,kBAAkB;CAC5D,MAAM,gBAAgB,YAAY,QAAQ,kBAAkB;CAC5D,MAAM,kBACJ,kBAAkB,KAAA,KAAa,gBAC3B;EACE,GAAI,kBAAkB,KAAA,IAAY,EAAE,YAAY,cAAc,IAAI,CAAC;EACnE,GAAI,gBAAgB,EAAE,YAAY,cAAc,IAAI,CAAC;EACrD,QAAQ;CACV,IACA,KAAA;CAEN,MAAM,QAAQ;EACZ,oBAAoB,QAAQ,GAAG;EAC/B;EACA,SAAS,KAAK,SAAS,IAAI,gBAAgB,KAAK,QAAQ,EAAE;EAC1D,eAAe,KAAK;EACpB,mBAAmB,KAAK;EACxB,GAAI,kBACA,CAAC,qBAAqB,iBAAiB,UAAU,IAAI,gBAAgB,aAAa,EAAE,EAAE,IACtF,CAAC;EACL,oBAAoB,QAAQ,gBAAgB,UAAU,QAAQ,QAAQ,iBAAiB,UAAU;EACjG;EACA;EACA,GAAI,aAAa,SAAS,IACtB,CACE,mBAAmB,YAAY,CAAC,CAAC,KAAK,IAAI,GAC1C,GAAI,yBACA,CAAC,IACD,CACE,gBAAgB,uBAAuB,OAAO,uBAAuB,uBAAuB,WAAW,IAAI,MAAM,MAAM,gEACzH,CACN,IACA,CACE,yBACI,gEACA,iCAAiC,uBAAuB,OAAO,iBAAiB,uBAAuB,WAAW,IAAI,MAAM,MAAM,iIACxI;CACN;CACA,IAAI,MAAM,QAAQ,KAAK,UAAU,KAAK,KAAK,UAAU,CAAC,SAAS,GAC7D,MAAM,KAAK,IAAI,gBAAgB,KAAK,UAAU,CAAC,KAAK,WAAW,KAAK,QAAQ,CAAC,CAAC,KAAK,IAAI,CAAC;CAE1F,IAAI,YACF,MAAM,KAAK,IAAI,0BAA0B,UAAU,CAAC;CAEtD,IAAI,gBACF,MAAM,KACJ,IACA,8BAA8B,kBAC9B,YAAY,SAAS,IACjB,2BAA2B,YAAY,WACvC,2BACN;CAEF,IAAI,mBACF,MAAM,KACJ,IACA,qDAAqD,aAAa,uBACpE;CAEF,IAAI,qBAAqB,SAAS,GAChC,MAAM,KACJ,IACA,0BACA,qBAAqB,KAAK,YAAY,KAAK,QAAQ,GAAG,IAAI,QAAQ,OAAO,CAAC,CAAC,KAAK,IAAI,CACtF;CAEF,MAAM,cAAc,MAAM,KAAK,IAAI;CACnC,MAAM,oBAAoB;EACxB,QAAQ;EACR,MAAM;EACN,OAAO;GACL,SAAS;IACP;IACA,WAAW,iBAAiB,CAAC,SAAS,cAAc,IAAI,CAAC,OAAO;GAClE;GACA,MAAM;IACJ,GAAG;IACH,GAAI,kBAAkB,EAAE,WAAW,gBAAgB,IAAI,CAAC;GAC1D;GACA,mBAAmB;IACjB;IACA;IACA,eAAe,yBAAyB,aAAa;IACrD,GAAI,yBACA,CAAC,IACD,EAAE,kBAAkB,uBAAuB,KAAK,YAAY,QAAQ,EAAE,EAAE;GAC9E;GACA,YAAY,iBACR;IACE,iBAAiB;IACjB,OAAO;IACP,GAAI,gBAAgB,EAAE,gBAAgB,cAAc,IAAI,CAAC;GAC3D,IACA,KAAA;GACJ,aAAa;GACb,YAAY,oBAAoB,CAAC,YAAY,IAAI,KAAA;GACjD,sBAAsB,qBAAqB,SAAS,IAAI,uBAAuB,KAAA;GAC/E,OAAO,WAAW,KAAK;EACzB;CACF;CACA,MAAM,YAAY,QAAQ,iBACtB,MAAM,0BACJ,SACA,SACA,gBACA,WACA,cACA,WACF,IACA,mBAAmB;CACvB,MAAM,WAAW,iBAAiB,SAAS;CAE3C,OAAO;EACL;EACA,mBAAmB;GACjB,GAAG;GACH;GACA,UAAU,CACR,GAAG,UACH;IACE,eAAe;IACf,SAAS,oBAAoB,QAAQ,iBAAiB;GACxD,CACF;EACF;EACA;CACF;AACF;;;;;AAMA,eAAsB,kBACpB,cACA,SACA,kBACA;CACA,MAAM,UAAU,6BAA6B,gBAAgB;CAC7D,QAAQ,SAAR;EACE,KAAK,MAAM;GACT,MAAM,SAAS,MAAM,YAAY,cAAc,OAAO;GACtD,OAAO;IACL,GAAG;IACH,mBAAmB;KACjB,GAAG,OAAO;KACV,cAAc;IAChB;GACF;EACF;CACF;AACF;AAEA,SAAS,WAAW,OAAwB;CAC1C,OAAO,UAAU,KAAA,KAAa,UAAU,OAAO,KAAK,OAAO,KAAK;AAClE;AAEA,SAAS,0BAA0B,KAA8B,iBAAiC;CAChG,OAAO,OACL,IAAI,iBAAiB,WAChB,IAAI,yBAAyB,IAAI,sBAAsB,kBACvD,IAAI,sBAAsB,IAAI,yBAAyB,eAC9D;AACF;AAEA,SAAS,0BACP,MACA,SACA,MACA,SACQ;CACR,MAAM,UAAU;EACd;EACA;EACA;EACA;EACA;EACA;CACF;CACA,MAAM,OAAO,KACV,KAAK,QAAQ;EACZ,MAAM,kBAAkB,OAAO,IAAI,uBAAuB,EAAE;EAC5D,MAAM,cAAc,0BAA0B,KAAK,OAAO;EAS1D,OAAO,OAAO;GAPZ,IAAI,gBAAgB;GACpB;GACA;GACA,IAAI,WAAW;GACf,IAAI,qBAAqB;GACzB,IAAI,eAAe;EAEC,CAAC,CAAC,KAAK,UAAU,OAAO,WAAW,WAAW,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;CAC/F,CAAC,CAAC,CACD,KAAK,IAAI;CACZ,OAAO;;;;;SAKA,WAAW,IAAI,EAAE;;;;;;;;;;;;;QAalB,WAAW,IAAI,EAAE;0BACC,WAAW,OAAO,EAAE;4BAClB,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC,EAAE;;iBAEhD,QAAQ,KAAK,WAAW,OAAO,WAAW,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;aACvE,KAAK;;;;;;AAMlB;AAEA,eAAe,0BACb,SACA,SACA,gBACA,WACA,cACA,aACiC;CACjC,MAAM,QAAQ,qBAAqB;CACnC,MAAM,QAAQ,IAAI;EAChB,MAAM,MAAM,aAAa,EAAE,WAAW,KAAK,CAAC;EAC5C,MAAM,MAAM,kBAAkB,EAAE,WAAW,KAAK,CAAC;EACjD,MAAM,MAAM,kBAAkB,EAAE,WAAW,KAAK,CAAC;CACnD,CAAC;CACD,MAAM,cAAc,QAAQ,QAAQ,qBAAqB,GAAG;CAC5D,MAAM,cAAc,QAAQ,QAAQ,qBAAqB,GAAG;CAC5D,MAAM,OAAO,oBAAG,IAAI,KAAK,EAAA,CACtB,YAAY,CAAC,CACb,QAAQ,SAAS,EAAE,CAAC,CACpB,QAAQ,aAAa,GAAG,EAAE,oBAAoB,YAAY,GAAG;CAChE,MAAM,YAAY,KAAK,KAAK,MAAM,kBAAkB,GAAG,KAAK,YAAY;CACxE,MAAM,gBAAgB,KAAK,KAAK,MAAM,kBAAkB,GAAG,KAAK,uBAAuB;CACvF,MAAM,UAAU,KAAK,KAAK,MAAM,kBAAkB,GAAG,KAAK,WAAW;CACrE,MAAM,gBAAgB,KAAK,KAAK,MAAM,aAAa,GAAG,KAAK,YAAY;CACvE,MAAM,aAAa,KAAK,KAAK,MAAM,aAAa,GAAG,KAAK,uBAAuB;CAC/E,MAAM,gBAAgB,KAAK,KAAK,MAAM,aAAa,GAAG,KAAK,YAAY;CACvE,MAAM,EAAE,4BAA4B,MAAM,OAAO;CASjD,MAAM,MACJ,CACE;EATF;EACA;EACA;EACA;EACA;EACA;CAIO,CAAC,CAAC,KAAK,GAAG,GACf,GAAG,aAAa,KAAK,QAAQ;EAC3B,MAAM,kBAAkB,OAAO,IAAI,uBAAuB,EAAE;EAC5D,MAAM,kBAAkB,0BAA0B,KAAK,OAAO;EAC9D,OAAO;GACL,IAAI,gBAAgB;GACpB;GACA;GACA,IAAI,WAAW;GACf,IAAI,qBAAqB;GACzB,IAAI,eAAe;EACrB,CAAC,CACE,KAAK,UAAU,KAAK,UAAU,OAAO,KAAK,CAAC,CAAC,CAAC,CAC7C,KAAK,GAAG;CACb,CAAC,CACH,CAAC,CAAC,KAAK,IAAI,IAAI;CACjB,MAAM,WAAW;EACf,QAAQ;EACR,MAAM;EACN;EACA,OAAO;GACL;GACA,GAAI,iBAAiB,EAAE,iBAAiB,eAAe,IAAI,CAAC;EAC9D;EACA,SAAS;GACP,eAAe,aAAa,KAAK,SAAS;IACxC,WAAW,IAAI;IACf,kBAAkB,IAAI;IACtB,cAAc,0BAA0B,KAAK,OAAO;IACpD,MAAM,IAAI;IACV,gBAAgB,IAAI;IACpB,UAAU,IAAI;GAChB,EAAE;GACF,gBAAgB;EAClB;CACF;CACA,MAAM,UAAU,WAAW,KAAK,UAAU,WAAW,MAAM,CAAC,IAAI,MAAM,EAAE,MAAM,IAAM,CAAC;CACrF,MAAM,UAAU,eAAe,KAAK,UAAU,UAAU,MAAM,CAAC,IAAI,MAAM,EAAE,MAAM,IAAM,CAAC;CACxF,MAAM,UAAU,SAAS,KAAK,EAAE,MAAM,IAAM,CAAC;CAC7C,MAAM,UACJ,eACA,0BAA0B,oBAAoB,SAAS,cAAc,OAAO,GAC5E,EAAE,MAAM,IAAM,CAChB;CACA,MAAM,UAAU,eAAe,wBAAwB,SAAS,GAAG,EAAE,MAAM,IAAM,CAAC;CAClF,MAAM,UACJ,YACA;EACE,0BAA0B,QAAQ,GAAG,QAAQ;EAC7C,iBAAiB;EACjB,iBAAiB;EACjB,UAAU;EACV,kBAAkB;EAClB,iBAAiB;EACjB;EACA;CACF,CAAC,CAAC,KAAK,IAAI,GACX,EAAE,MAAM,IAAM,CAChB;CAEA,OAAO;EACL,YAAY;EACZ,YAAY;EACZ,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,WAAW;CACb;AACF;AAEA,SAAS,qBAA8C;CACrD,OAAO;EACL,mBAAmB;EACnB,eAAe;CACjB;AACF;AAEA,SAAS,iBAAiB,WAAoE;CAC5F,OAAO,OAAO,QAAQ,SAAS,CAAC,CAC7B,QACE,UACC,MAAM,OAAO,mBAAmB,OAAO,MAAM,OAAO,YAAY,MAAM,EAAE,CAAC,SAAS,CACtF,CAAC,CACA,KAAK,CAAC,MAAM,eAAe;EAC1B,eAAe;EACf,MAAM;EACN,SAAS,GAAG,KAAK;CACnB,EAAE;AACN;AAEA,SAAS,WAAW,OAAwB;CAC1C,OAAO,OAAO,SAAS,EAAE,CAAC,CACvB,WAAW,KAAK,OAAO,CAAC,CACxB,WAAW,KAAK,MAAM,CAAC,CACvB,WAAW,KAAK,MAAM,CAAC,CACvB,WAAW,MAAK,QAAQ,CAAC,CACzB,WAAW,KAAK,OAAO;AAC5B"}
|
|
@@ -450,6 +450,13 @@ function wrapClientForUsageTracking(client, totals) {
|
|
|
450
450
|
//#region src/investigation/public-tools.ts
|
|
451
451
|
const GRAPH_QUERY_BATCH_TIMEOUT_SECONDS = 10;
|
|
452
452
|
const GRAPH_QUERY_BATCH_REQUEST_TIMEOUT_MS = 3e5;
|
|
453
|
+
const AML_ADDRESS_RISK_SUPPORTED_VERSIONS = ["v1"];
|
|
454
|
+
function resolveAmlAddressRiskVersion(requested) {
|
|
455
|
+
const normalized = requested?.trim().toLowerCase();
|
|
456
|
+
if (!normalized) return "v1";
|
|
457
|
+
if (AML_ADDRESS_RISK_SUPPORTED_VERSIONS.includes(normalized)) return normalized;
|
|
458
|
+
throw new Error(`Unsupported aml_address_risk version "${requested?.trim() ?? ""}". Supported versions: ${AML_ADDRESS_RISK_SUPPORTED_VERSIONS.join(", ")}.`);
|
|
459
|
+
}
|
|
453
460
|
function escapeCypherString(value) {
|
|
454
461
|
return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
455
462
|
}
|
|
@@ -1146,6 +1153,25 @@ async function addressRisk(remoteClient, options) {
|
|
|
1146
1153
|
graphData
|
|
1147
1154
|
};
|
|
1148
1155
|
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Routes the public AML tool contract to the requested implementation. An
|
|
1158
|
+
* omitted version always follows the latest supported contract.
|
|
1159
|
+
*/
|
|
1160
|
+
async function runAmlAddressRisk(remoteClient, options, requestedVersion) {
|
|
1161
|
+
const version = resolveAmlAddressRiskVersion(requestedVersion);
|
|
1162
|
+
switch (version) {
|
|
1163
|
+
case "v1": {
|
|
1164
|
+
const result = await addressRisk(remoteClient, options);
|
|
1165
|
+
return {
|
|
1166
|
+
...result,
|
|
1167
|
+
structuredContent: {
|
|
1168
|
+
...result.structuredContent,
|
|
1169
|
+
tool_version: version
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1149
1175
|
function toCsvValue(value) {
|
|
1150
1176
|
return value === void 0 || value === null ? "" : String(value);
|
|
1151
1177
|
}
|
|
@@ -1300,4 +1326,5 @@ function htmlEscape(value) {
|
|
|
1300
1326
|
return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
1301
1327
|
}
|
|
1302
1328
|
//#endregion
|
|
1303
|
-
exports.
|
|
1329
|
+
exports.resolveAmlAddressRiskVersion = resolveAmlAddressRiskVersion;
|
|
1330
|
+
exports.runAmlAddressRisk = runAmlAddressRisk;
|
|
@@ -37,7 +37,8 @@ const PUBLIC_MCP_TOOL_ALLOWED_ARGS = {
|
|
|
37
37
|
aml_address_risk: [
|
|
38
38
|
"address",
|
|
39
39
|
"network",
|
|
40
|
-
"compare_address"
|
|
40
|
+
"compare_address",
|
|
41
|
+
"version"
|
|
41
42
|
],
|
|
42
43
|
graph_query: [
|
|
43
44
|
"query",
|
|
@@ -76,4 +77,4 @@ function validatePublicMcpToolArguments(name, args) {
|
|
|
76
77
|
//#endregion
|
|
77
78
|
export { tool_visibility_exports as i, PUBLIC_MCP_TOOL_ALLOWED_ARGS as n, PUBLIC_MCP_TOOL_REQUIRED_ARGS as r, HIDDEN_REMOTE_TOOL_NAMES as t };
|
|
78
79
|
|
|
79
|
-
//# sourceMappingURL=tool-visibility-
|
|
80
|
+
//# sourceMappingURL=tool-visibility-CJqjmDF5.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-visibility-CJqjmDF5.mjs","names":[],"sources":["../src/mcp/tool-visibility.ts"],"sourcesContent":["export const HIDDEN_REMOTE_TOOL_NAMES = new Set([\n 'topup',\n 'address_risk',\n 'trace_victim_funds',\n 'trace_suspect_funds',\n 'trace_deposit_sources',\n 'trace_funds',\n 'track_funds',\n // Retired AML trace tools: never exposed publicly. Built by concatenation\n // so the retired names do not appear as literals in this file.\n ['aml_trace_victim', '_funds'].join(''),\n ['aml_trace_suspect', '_funds'].join(''),\n ['aml_trace_deposit', '_sources'].join(''),\n 'network_capabilities',\n 'usage_status',\n 'balance',\n 'help',\n 'money_flows_between_exchanges',\n 'address_connection_risk',\n])\n\nexport const PUBLIC_MCP_TOOL_REQUIRED_ARGS: Record<string, string[]> = {\n aml_address_risk: ['address', 'network'],\n graph_query: ['query', 'network'],\n graph_query_batch: ['network', 'queries'],\n}\n\n// normalizeRemoteToolArguments FILTERS pass-through arguments to this list, so\n// an argument that is missing here is silently STRIPPED and the caller gets a\n// default-bounded result with no error and no warning. `time_scope` shipped\n// broken for exactly this reason. Every tunable search bound below\n// (per_address_limit, row_limit) must therefore be listed the moment it\n// appears on a tool schema, and is covered end-to-end through the proxy in\n// tests/mcp-proxy.test.ts.\nexport const PUBLIC_MCP_TOOL_ALLOWED_ARGS: Record<string, string[]> = {\n aml_address_risk: ['address', 'network', 'compare_address', 'version'],\n // `time_scope` narrows a `USE topology` query to a temporal-shard subset\n graph_query: ['query', 'network', 'time_scope'],\n graph_query_batch: ['network', 'queries', 'per_query_timeout_seconds', 'time_scope'],\n}\n\nexport function isHiddenRemoteToolName(name: string): boolean {\n return HIDDEN_REMOTE_TOOL_NAMES.has(name)\n}\n\nexport function visibleRemoteTools<T extends { name: string }>(tools: T[]): T[] {\n return tools.filter((tool) => !isHiddenRemoteToolName(tool.name))\n}\n\nexport function assertPublicMcpToolName(name: string): void {\n if (!isHiddenRemoteToolName(name)) return\n const replacement =\n name === 'address_risk'\n ? ' Use aml_address_risk instead.'\n : name === 'network_capabilities'\n ? ' Use meta_network_capabilities instead.'\n : name === 'usage_status'\n ? ' Use meta_usage_status instead.'\n : name === 'balance'\n ? ' Use wallet_balance instead.'\n : name === 'help'\n ? ' Use meta_help instead.'\n : ''\n throw new Error(`MCP tool '${name}' is not exposed by Chain Insights.${replacement}`)\n}\n\nexport function formatMcpCallError(tool: string, err: unknown): string {\n const message = err instanceof Error ? err.message : String(err)\n if (/\\bunknown tool\\b|\\btool\\b.*\\bnot found\\b/i.test(message)) {\n return `Unknown MCP tool \"${tool}\". Run \\`cia mcp tools --refresh\\` to list available tools.`\n }\n return message\n}\n\nexport function validatePublicMcpToolArguments(name: string, args: Record<string, unknown>): void {\n const allowedArgs = PUBLIC_MCP_TOOL_ALLOWED_ARGS[name]\n if (!allowedArgs) return\n\n const unsupportedArgs = Object.keys(args).filter((argName) => !allowedArgs.includes(argName))\n if (unsupportedArgs.length === 0) return\n\n throw new Error(\n [\n `Unsupported argument${unsupportedArgs.length === 1 ? '' : 's'} for ${name}: ${unsupportedArgs.join(', ')}.`,\n `Allowed arguments: ${allowedArgs.join(', ')}.`,\n ].join(' ')\n )\n}\n"],"mappings":";;;;;;;;;;;;AAAA,MAAa,2CAA2B,IAAI,IAAI;CAC9C;CACA;CACA;CACA;CACA;CACA;CACA;CAGA,CAAC,oBAAoB,QAAQ,CAAC,CAAC,KAAK,EAAE;CACtC,CAAC,qBAAqB,QAAQ,CAAC,CAAC,KAAK,EAAE;CACvC,CAAC,qBAAqB,UAAU,CAAC,CAAC,KAAK,EAAE;CACzC;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,MAAa,gCAA0D;CACrE,kBAAkB,CAAC,WAAW,SAAS;CACvC,aAAa,CAAC,SAAS,SAAS;CAChC,mBAAmB,CAAC,WAAW,SAAS;AAC1C;AASA,MAAa,+BAAyD;CACpE,kBAAkB;EAAC;EAAW;EAAW;EAAmB;CAAS;CAErE,aAAa;EAAC;EAAS;EAAW;CAAY;CAC9C,mBAAmB;EAAC;EAAW;EAAW;EAA6B;CAAY;AACrF;AAEA,SAAgB,uBAAuB,MAAuB;CAC5D,OAAO,yBAAyB,IAAI,IAAI;AAC1C;AAEA,SAAgB,mBAA+C,OAAiB;CAC9E,OAAO,MAAM,QAAQ,SAAS,CAAC,uBAAuB,KAAK,IAAI,CAAC;AAClE;AAEA,SAAgB,wBAAwB,MAAoB;CAC1D,IAAI,CAAC,uBAAuB,IAAI,GAAG;CAanC,MAAM,IAAI,MAAM,aAAa,KAAK,qCAXhC,SAAS,iBACL,mCACA,SAAS,yBACP,4CACA,SAAS,iBACP,oCACA,SAAS,YACP,iCACA,SAAS,SACP,4BACA,IACsE;AACtF;AAEA,SAAgB,mBAAmB,MAAc,KAAsB;CACrE,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC/D,IAAI,4CAA4C,KAAK,OAAO,GAC1D,OAAO,qBAAqB,KAAK;CAEnC,OAAO;AACT;AAEA,SAAgB,+BAA+B,MAAc,MAAqC;CAChG,MAAM,cAAc,6BAA6B;CACjD,IAAI,CAAC,aAAa;CAElB,MAAM,kBAAkB,OAAO,KAAK,IAAI,CAAC,CAAC,QAAQ,YAAY,CAAC,YAAY,SAAS,OAAO,CAAC;CAC5F,IAAI,gBAAgB,WAAW,GAAG;CAElC,MAAM,IAAI,MACR,CACE,uBAAuB,gBAAgB,WAAW,IAAI,KAAK,IAAI,OAAO,KAAK,IAAI,gBAAgB,KAAK,IAAI,EAAE,IAC1G,sBAAsB,YAAY,KAAK,IAAI,EAAE,EAC/C,CAAC,CAAC,KAAK,GAAG,CACZ;AACF"}
|
package/docs/graph-tools.md
CHANGED
|
@@ -21,8 +21,8 @@ Chain Insights tools such as `aml_address_risk` are recipes built over
|
|
|
21
21
|
Chain Insights Graph endpoint.
|
|
22
22
|
|
|
23
23
|
`cia mcp tools` lists this remote GraphRAG surface. Use `cia networks` for the
|
|
24
|
-
short network overview
|
|
25
|
-
|
|
24
|
+
short network overview, `cia network <name>` for one network's details and
|
|
25
|
+
available tools, and `cia mcp networks` for the full network capability matrix.
|
|
26
26
|
|
|
27
27
|
The Chain Insights MCP proxy adds product-facing local metadata tools such as
|
|
28
28
|
`meta_network_capabilities`, `meta_usage_status`, and `meta_help`. On hosted
|
|
@@ -37,7 +37,10 @@ primitive-backend status instead.
|
|
|
37
37
|
- Use `USE topology` for topology (the address / FLOWS_TO / LINKED graph,
|
|
38
38
|
covering unified recent and full historical activity in one graph, plus the
|
|
39
39
|
node `risk_score`/`risk_level` verdict).
|
|
40
|
-
- Use `USE facts` for
|
|
40
|
+
- Use `USE facts` for bounded individual `TRANSFER` rows and their amount,
|
|
41
|
+
`amount_usd`, asset, transaction, and block facts. Address labels, risk,
|
|
42
|
+
lifetime metrics, and `FLOWS_TO`/`LINKED` relationships belong to
|
|
43
|
+
`USE topology`.
|
|
41
44
|
- Use `meta_usage_status` through Chain Insights before public hosted reads
|
|
42
45
|
when you need the caller's remaining free-tier allowance.
|
|
43
46
|
- Hosted endpoints can expose a public free tier for graph_query. The default
|
|
@@ -125,6 +128,16 @@ Required input:
|
|
|
125
128
|
Optional input:
|
|
126
129
|
|
|
127
130
|
- `compare_address`
|
|
131
|
+
- `version` — omit it to use the latest contract, or set it to `v1` to pin the
|
|
132
|
+
current AML contract.
|
|
133
|
+
|
|
134
|
+
CLI output is human-readable by default. Add `--json` to print indented JSON:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
cia mcp aml-address-risk --json --address 0xYourAddressHere --network robinhood
|
|
138
|
+
cia mcp call --json graph_query network=robinhood \
|
|
139
|
+
"query=USE topology MATCH (a:Address) RETURN a.address AS address LIMIT 10"
|
|
140
|
+
```
|
|
128
141
|
|
|
129
142
|
## Manual Fund-Flow Traversal
|
|
130
143
|
|
package/docs/mcp-proxy.md
CHANGED
|
@@ -91,8 +91,9 @@ Chain Insights presents this as local, prefixed metadata through
|
|
|
91
91
|
`meta_network_capabilities`.
|
|
92
92
|
|
|
93
93
|
The CLI keeps these catalogs distinct: `cia mcp tools` lists remote GraphRAG
|
|
94
|
-
tools and caches that schema for 24 hours, while `cia
|
|
95
|
-
|
|
94
|
+
tools and caches that schema for 24 hours, while `cia networks` and
|
|
95
|
+
`cia network <name>` report the network list and each network's advertised
|
|
96
|
+
tools. `cia mcp networks` exposes the same full network capability matrix.
|
|
96
97
|
Use `cia mcp tools --refresh` after a backend tool change.
|
|
97
98
|
|
|
98
99
|
`meta_usage_status` is a Chain Insights proxy tool. On hosted Chain Insights Graph
|
|
@@ -143,6 +144,36 @@ not include `graph_query_batch`; use a tester access key or paid x402 mode for
|
|
|
143
144
|
regular usage and batches. Use explicit LIMIT and pagination in your query when
|
|
144
145
|
you want bounded result sets.
|
|
145
146
|
|
|
147
|
+
### CLI Output And Tool Versions
|
|
148
|
+
|
|
149
|
+
The CLI renders JSON graph results as a readable summary and table by default.
|
|
150
|
+
Use `--json` for indented machine-readable output:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
cia mcp call graph_query \
|
|
154
|
+
network=robinhood \
|
|
155
|
+
"query=USE topology MATCH (a:Address) RETURN a.address AS address LIMIT 10"
|
|
156
|
+
|
|
157
|
+
cia mcp call --json graph_query \
|
|
158
|
+
network=robinhood \
|
|
159
|
+
"query=USE topology MATCH (a:Address) RETURN a.address AS address LIMIT 10"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`aml_address_risk` also supports JSON output and version selection:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cia mcp aml-address-risk --json \
|
|
166
|
+
--address 0xYourAddressHere --network robinhood
|
|
167
|
+
|
|
168
|
+
cia mcp aml-address-risk --version v1 \
|
|
169
|
+
--address 0xYourAddressHere --network robinhood
|
|
170
|
+
cia mcp call aml_address_risk \
|
|
171
|
+
network=robinhood address=0xYourAddressHere version=v1
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Omit the version to route to the latest supported AML contract. The package
|
|
175
|
+
version shown by `cia --version` is separate from tool contract versions.
|
|
176
|
+
|
|
146
177
|
UAT on 2026-05-31 showed the 10-second free tier was enough for exact
|
|
147
178
|
address checks, sample address reads, sample flow reads, and the
|
|
148
179
|
free-to-paid handoff, but bounded sample reads still returned topology data
|
package/docs/stability.md
CHANGED
|
@@ -32,6 +32,10 @@ Tool names are contract. Removing or renaming one is a deprecation event.
|
|
|
32
32
|
Tool _arguments_ may grow additively at any time; existing argument names and
|
|
33
33
|
meanings are stable.
|
|
34
34
|
|
|
35
|
+
The CLI supports `--json` for indented structured output. The
|
|
36
|
+
`aml_address_risk` tool accepts an optional `version`; omitting it routes to
|
|
37
|
+
the latest contract, while `version=v1` pins the current contract.
|
|
38
|
+
|
|
35
39
|
### Config keys
|
|
36
40
|
|
|
37
41
|
Documented config keys (see `docs/architecture.md`, "Supported config keys")
|
package/package.json
CHANGED
|
@@ -23,6 +23,8 @@ Required:
|
|
|
23
23
|
Optional:
|
|
24
24
|
|
|
25
25
|
- `compare_address` — second address for a pairwise compare
|
|
26
|
+
- `version` — omit to use the latest AML contract, or set `v1` to pin the
|
|
27
|
+
current contract
|
|
26
28
|
|
|
27
29
|
The tool returns raw addresses. There is no identity-resolution step.
|
|
28
30
|
|
|
@@ -30,6 +32,10 @@ The screen covers risk, behavior, neighborhood context, and exchange
|
|
|
30
32
|
exposure. Treat exchange hot wallets as terminals, not as intermediate
|
|
31
33
|
hops.
|
|
32
34
|
|
|
35
|
+
For the CLI, use `cia mcp aml-address-risk` for the readable summary. Add
|
|
36
|
+
`--json` for indented structured output. When using `cia mcp call`, pass the
|
|
37
|
+
version as `version=v1`.
|
|
38
|
+
|
|
33
39
|
After the screen, load the schema skill for that network:
|
|
34
40
|
`chain-insights-schema-evm` or `chain-insights-schema-bittensor`.
|
|
35
41
|
Use `chain-insights-cypher` only for extra graph reads.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities-CY3Ok5ls.mjs","names":[],"sources":["../src/mcp/capabilities.ts"],"sourcesContent":["import type { InvestigatorConfig } from '../config/schema.js'\nimport { applyMcpAuthHeaders, resolveGraphMcpEndpoint } from './client.js'\n\nexport interface NetworkTopologyLayer {\n enabled: boolean\n coverage?: {\n from_block?: number\n to_block?: number\n from_timestamp?: string\n to_timestamp?: string\n chain_tip_block?: number\n blocks_behind_tip?: number\n }\n}\n\nexport interface NetworkLayerCapability {\n enabled: boolean\n live?: NetworkTopologyLayer\n archive?: NetworkTopologyLayer\n}\n\nexport interface NetworkCapability {\n network: string\n display_name?: string\n status: string\n default?: boolean\n layers: Record<string, NetworkLayerCapability>\n tools: Record<string, string>\n coverage?: {\n from_block?: number\n to_block?: number\n from_timestamp?: string\n to_timestamp?: string\n chain_tip_block?: number\n blocks_behind_tip?: number\n }\n freshness?: {\n last_processed_at?: string\n last_successful_sync_at?: string\n max_data_age_seconds?: number\n last_processing_duration_seconds?: number\n }\n}\n\nexport interface NetworkCapabilitiesDocument {\n schema: 'chain-insights.network-capabilities.v1'\n networks: NetworkCapability[]\n}\n\nexport const PUBLIC_CHAIN_INSIGHTS_TOOL_STATUS = {\n aml_address_risk: 'available',\n graph_query: 'available',\n graph_query_batch: 'available',\n meta_network_capabilities: 'available',\n meta_usage_status: 'available',\n meta_help: 'available',\n wallet_balance: 'available',\n} as const\nconst AVAILABLE_TOOLS_PER_LINE = 3\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction advertisedNetwork(raw: unknown): NetworkCapability | null {\n if (!isRecord(raw) || typeof raw.network !== 'string' || raw.network.trim() === '') {\n return null\n }\n const network = raw.network.trim()\n const capability: NetworkCapability = {\n network,\n display_name:\n typeof raw.display_name === 'string' && raw.display_name.trim() !== ''\n ? raw.display_name\n : network,\n status: typeof raw.status === 'string' && raw.status.trim() !== '' ? raw.status : 'live',\n layers: {},\n tools: { ...PUBLIC_CHAIN_INSIGHTS_TOOL_STATUS },\n }\n if (raw.default === true) capability.default = true\n if (raw.default === false) capability.default = false\n if (isRecord(raw.coverage)) capability.coverage = raw.coverage as NetworkCapability['coverage']\n if (isRecord(raw.freshness))\n capability.freshness = raw.freshness as NetworkCapability['freshness']\n return capability\n}\n\n/** Repeat GraphRAG's network list. CIA does not add, drop, or invent names. */\nexport function mirrorGraphNetworkCapabilities(document: {\n networks: unknown[]\n}): NetworkCapabilitiesDocument {\n return {\n schema: 'chain-insights.network-capabilities.v1',\n networks: document.networks\n .map((network) => advertisedNetwork(network))\n .filter((network): network is NetworkCapability => network !== null),\n }\n}\n\nfunction publicNetworkCapabilities(\n document: NetworkCapabilitiesDocument\n): NetworkCapabilitiesDocument {\n return mirrorGraphNetworkCapabilities(document)\n}\n\nfunction metadataNetworksUrl(endpoint: string): URL {\n const url = new URL(endpoint)\n url.pathname = '/metadata/networks'\n url.search = ''\n url.hash = ''\n return url\n}\n\nexport async function fetchNetworkCapabilities(\n config: Pick<InvestigatorConfig, 'graphMcpAuthToken' | 'graphMcpMode' | 'graphMcpEndpoint'>\n): Promise<NetworkCapabilitiesDocument> {\n const endpoint = resolveGraphMcpEndpoint(config)\n const request = metadataNetworksUrl(endpoint)\n const headers = new Headers()\n const token = config.graphMcpAuthToken?.trim()\n if (token) {\n applyMcpAuthHeaders(headers, token)\n }\n let response: Response\n try {\n response = await fetch(request, { headers })\n } catch (err) {\n throw new Error(`network capabilities unavailable at ${request}: ${(err as Error).message}`)\n }\n if (!response.ok) {\n throw new Error(`network capabilities unavailable at ${request}: HTTP ${response.status}`)\n }\n const parsed = (await response.json()) as NetworkCapabilitiesDocument\n if (\n parsed.schema !== 'chain-insights.network-capabilities.v1' ||\n !Array.isArray(parsed.networks)\n ) {\n throw new Error('network capabilities response has unsupported schema')\n }\n return publicNetworkCapabilities(parsed)\n}\n\nfunction availableTools(network: NetworkCapability): string[] {\n const effectiveTools = {\n ...(network.tools ?? {}),\n ...PUBLIC_CHAIN_INSIGHTS_TOOL_STATUS,\n }\n const tools = Object.entries(effectiveTools)\n .filter(([, status]) => status === 'available')\n .map(([name]) => name)\n return tools.sort()\n}\n\nfunction availableToolLines(network: NetworkCapability): string[] {\n const tools = availableTools(network)\n if (tools.length === 0) return ['none']\n const lines: string[] = []\n for (let index = 0; index < tools.length; index += AVAILABLE_TOOLS_PER_LINE) {\n lines.push(tools.slice(index, index + AVAILABLE_TOOLS_PER_LINE).join(', '))\n }\n return lines\n}\n\nfunction shortDate(value?: string): string {\n if (!value) return ''\n return value.slice(0, 10)\n}\n\nfunction datasetLabel(network: NetworkCapability): string {\n const coverage = network.coverage\n if (!coverage) return 'unknown'\n const blockRange =\n coverage.from_block !== undefined && coverage.to_block !== undefined\n ? `${coverage.from_block}..${coverage.to_block}`\n : 'blocks unknown'\n const dateRange =\n coverage.from_timestamp && coverage.to_timestamp\n ? `${shortDate(coverage.from_timestamp)}..${shortDate(coverage.to_timestamp)}`\n : ''\n if (blockRange === 'blocks unknown' && dateRange === '') return 'unknown'\n if (blockRange === 'blocks unknown') return dateRange\n if (dateRange === '') return blockRange\n return `${blockRange} / ${dateRange}`\n}\n\nfunction displayName(network: NetworkCapability): string {\n return network.display_name || network.network\n}\n\nfunction statusLabel(network: NetworkCapability): string {\n return network.default ? `${network.status} (default)` : network.status\n}\n\nfunction formatTable(headers: string[], rows: string[][], minimumWidths: number[]): string {\n const widths = headers.map((header, index) =>\n Math.max(\n minimumWidths[index] ?? 0,\n header.length,\n ...rows.map((row) => row[index]?.length ?? 0)\n )\n )\n const row = (values: string[]) =>\n values.map((value, index) => value.padEnd(widths[index]!)).join(' ')\n return [row(headers), widths.map((width) => '-'.repeat(width)).join(' '), ...rows.map(row)].join(\n '\\n'\n )\n}\n\nexport function formatNetworkOverview(document: NetworkCapabilitiesDocument): string {\n if (document.networks.length === 0) return 'No supported networks advertised.'\n const rows = document.networks.map((network) => [\n displayName(network),\n statusLabel(network),\n datasetLabel(network),\n ])\n return formatTable(['Network', 'Status', 'Dataset'], rows, [14, 10, 38])\n}\n\nexport function formatNetworkCapabilities(document: NetworkCapabilitiesDocument): string {\n if (document.networks.length === 0) return 'No supported networks advertised.'\n const networkRows = document.networks.flatMap((network) => {\n const toolLines = availableToolLines(network)\n return toolLines.map((toolLine, index) => [\n index === 0 ? displayName(network) : '',\n index === 0 ? datasetLabel(network) : '',\n toolLine,\n ])\n })\n return formatTable(['Network', 'Dataset', 'Chain Insights tools'], networkRows, [14, 38, 64])\n}\n\nexport function findNetworkCapability(\n document: NetworkCapabilitiesDocument,\n name: string\n): NetworkCapability | undefined {\n const normalizedName = name.trim().toLowerCase()\n if (!normalizedName) return undefined\n return document.networks.find((network) =>\n [network.network, network.display_name].some(\n (candidate) => candidate?.trim().toLowerCase() === normalizedName\n )\n )\n}\n\nexport function formatNetworkCapability(network: NetworkCapability): string {\n const rows = [\n ['Network', network.display_name || network.network],\n ['Identifier', network.network],\n ['Status', network.default ? `${network.status} (default)` : network.status],\n ['Dataset', datasetLabel(network)],\n ['Available tools', availableTools(network).join(', ') || 'none'],\n ]\n const labelWidth = Math.max(...rows.map(([label]) => label.length))\n return rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`).join('\\n')\n}\n"],"mappings":"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"}
|