@bonnard/mcp-charts 0.1.1 → 0.1.3

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.
@@ -1,5 +1,5 @@
1
1
  import { Pool } from 'pg';
2
- import { C as ChartData } from './types-CHiUU9mj.js';
2
+ import { C as ChartData } from './types-6ALzXWjE.js';
3
3
 
4
4
  /** A Postgres result column: name + the type OID (`dataTypeID` from pg/PGlite result `fields`). */
5
5
  interface PostgresField {
package/dist/postgres.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  assertReadOnlySql,
3
3
  buildChartData,
4
4
  defaultNormalizeCell
5
- } from "./chunk-NGXV4I2L.js";
5
+ } from "./chunk-I3HINKHN.js";
6
6
 
7
7
  // src/adapters/postgres.ts
8
8
  var OID = {
@@ -1,5 +1,5 @@
1
1
  import { Connection } from 'snowflake-sdk';
2
- import { C as ChartData } from './types-CHiUU9mj.js';
2
+ import { C as ChartData } from './types-6ALzXWjE.js';
3
3
 
4
4
  /** A Snowflake result column: name + the logical type from `Column.getType()`. */
5
5
  interface SnowflakeColumn {
package/dist/snowflake.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  assertReadOnlySql,
3
3
  buildChartData,
4
4
  defaultNormalizeCell
5
- } from "./chunk-NGXV4I2L.js";
5
+ } from "./chunk-I3HINKHN.js";
6
6
 
7
7
  // src/adapters/snowflake.ts
8
8
  var NUMERIC = /* @__PURE__ */ new Set(["fixed", "real"]);
@@ -33,6 +33,9 @@ interface ChartData {
33
33
  rows: Record<string, unknown>[];
34
34
  fields?: FieldMeta[];
35
35
  encode?: Encode;
36
+ /** Data-source advisories (e.g. "result truncated at the row cap") that resolve() merges
37
+ * into ChartSpec.notes so they surface on the chart. */
38
+ notes?: string[];
36
39
  }
37
40
  /** Context passed to data callbacks — extensible bag so the signature never breaks. */
38
41
  interface ChartContext {
@@ -58,6 +61,9 @@ interface AxisSpec {
58
61
  currency?: string;
59
62
  /** x-axis only: values are numeric, so line/area render on a linear (value) scale, not categories. */
60
63
  numeric?: boolean;
64
+ /** percent only: values are 0-1 fractions (renderer scales by 100). Decided once per column
65
+ * at resolve time so a series crossing 1.0 doesn't flip scale between adjacent values. */
66
+ fraction?: boolean;
61
67
  }
62
68
  interface ColumnSpec {
63
69
  key: string;
@@ -65,6 +71,8 @@ interface ColumnSpec {
65
71
  format?: FieldFormat;
66
72
  granularity?: TimeGranularity;
67
73
  currency?: string;
74
+ /** percent only: values are 0-1 fractions (renderer scales by 100). See AxisSpec.fraction. */
75
+ fraction?: boolean;
68
76
  }
69
77
  /** A horizontal reference line on the value axis (target, average, threshold). */
70
78
  interface ReferenceLine {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bonnard/mcp-charts",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Add beautiful, agent-ready charts to your MCP server in a few lines.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/resolve/infer.ts","../src/adapters/sql.ts"],"sourcesContent":["// Typing inference: turn a ChartData into a complete, role-tagged FieldMeta list.\n// Priority: declared fields win; anything missing is sniffed from the row values.\n// This is what lets resolve() decide x / y / series when the data source did not\n// supply full typing (e.g. raw SQL rows).\nimport type { ChartData, FieldFormat, FieldKind, FieldMeta, FieldRole, TimeGranularity } from \"../types.js\";\n\n// Period-string -> granularity. Lets the raw-SQL path recognize time buckets that aren't full ISO\n// dates (e.g. strftime('%Y-%m') -> \"2025-01\") so time series are typed TEMPORAL — ordered, drawn\n// vertically, nicely labelled — instead of being mistaken for high-cardinality categories. Used\n// when the data source declares no column types, so the kind is inferred from the value shape.\nexport function sniffTimeGranularity(value: unknown): TimeGranularity | null {\n if (typeof value !== \"string\") return null;\n if (/^\\d{4}-\\d{2}-\\d{2}([T ]|$)/.test(value)) return \"day\"; // 2025-01-15 or a full datetime\n if (/^\\d{4}-\\d{2}$/.test(value)) return \"month\"; // 2025-01\n if (/^\\d{4}-?Q[1-4]$/i.test(value)) return \"quarter\"; // 2025-Q1 / 2025Q1\n if (/^\\d{4}-W\\d{2}$/i.test(value)) return \"week\"; // 2025-W03 (ISO week)\n if (/^\\d{4}$/.test(value)) {\n const y = Number(value); // bare year as a string (\"2025\"); guard the range to avoid 4-digit codes\n if (y >= 1900 && y <= 2100) return \"year\";\n }\n return null;\n}\n\nexport function sniffKind(rows: Record<string, unknown>[], name: string): FieldKind {\n const sample = rows.find((r) => r[name] != null)?.[name];\n if (sample == null) return \"string\";\n if (typeof sample === \"number\" || typeof sample === \"bigint\") return \"number\";\n if (typeof sample === \"boolean\") return \"boolean\";\n if (typeof sample === \"string\" && sniffTimeGranularity(sample) !== null) return \"time\";\n return \"string\";\n}\n\nexport function roleFromKind(kind: FieldKind): FieldRole {\n if (kind === \"time\") return \"time\";\n if (kind === \"number\") return \"measure\";\n return \"dimension\";\n}\n\nexport function titleCase(s: string): string {\n return s.replace(/_/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\n// Name-based hints — a purely conventional fallback used only when the data source declares no\n// format/granularity. Centralized here so the adapter path (buildChartData) and the raw-rows path\n// (inferFields) apply identical rules.\n\n// A numeric column whose name reads as a rate/ratio renders as a percent. Currency is deliberately\n// NOT guessed from names (the code is unknowable) — declare it. camelCase is normalized to snake.\nexport function formatHint(name: string): FieldFormat | undefined {\n const n = name.replace(/([a-z0-9])([A-Z])/g, \"$1_$2\").toLowerCase();\n if (/(^|_)(rate|pct|percent|percentage|ratio)(_|$)/.test(n)) return \"percent\";\n return undefined;\n}\n\n// Granularity from the column name, for time columns whose values don't reveal it (e.g. a DATE\n// named \"week_start\" holding full dates). Defaults to day.\nexport function granularityHint(name: string): TimeGranularity {\n const n = name.toLowerCase();\n if (n.includes(\"year\")) return \"year\";\n if (n.includes(\"quarter\")) return \"quarter\";\n if (n.includes(\"month\")) return \"month\";\n if (n.includes(\"week\")) return \"week\";\n return \"day\";\n}\n\n/** Complete field metadata for every column, declared values taking precedence. */\nexport function inferFields(data: ChartData): FieldMeta[] {\n const { rows, fields } = data;\n const declared = new Map((fields ?? []).map((f) => [f.name, f]));\n // Union of returned columns + any declared-only names: declaring fields for SOME columns\n // augments their typing, it does not drop the columns you didn't declare.\n const rowKeys = rows[0] ? Object.keys(rows[0]) : [];\n const names = [...rowKeys, ...[...declared.keys()].filter((n) => !rowKeys.includes(n))];\n\n return names.map((name) => {\n const d = declared.get(name);\n const kind: FieldKind = d?.kind ?? sniffKind(rows, name);\n const role: FieldRole = d?.role ?? roleFromKind(kind);\n // Time columns with no declared granularity: infer it from the value shape (YYYY-MM -> month).\n // declared > value-sniff > name-hint\n const granularity =\n d?.granularity ??\n (kind === \"time\"\n ? (sniffTimeGranularity(rows.find((r) => r[name] != null)?.[name]) ?? granularityHint(name))\n : undefined);\n // declared > name-hint (rate -> percent); only for numeric columns\n const format = d?.format ?? (kind === \"number\" ? formatHint(name) : undefined);\n return {\n name,\n kind,\n role,\n label: d?.label ?? titleCase(name),\n ...(format && { format }),\n ...(granularity && { granularity }),\n ...(d?.additive != null && { additive: d.additive }),\n ...(d?.currency && { currency: d.currency }),\n };\n });\n}\n","// Shared adapter core. An adapter's only engine-specific jobs are (a) report each column's engine\n// type, (b) map that type to a FieldKind, and (c) normalize a cell to a flat scalar. The rest —\n// assembling typed FieldMeta (with name-based format/granularity hints), the ChartData shape, and\n// the read-only check — lives here so every adapter shares it.\n//\n// Connection, auth, read-only enforcement, cost caps and timeouts belong to the caller's database\n// role; assertReadOnlySql is a cheap extra check on LLM-authored SQL, not a substitute for one.\nimport type { ChartData, FieldKind, FieldMeta } from \"../types.js\";\nimport { roleFromKind, titleCase, formatHint, granularityHint } from \"../resolve/infer.js\";\n\n/** A source column: its name + the engine's declared type, in whatever shape the driver reports. */\nexport interface SourceColumn {\n name: string;\n /** Engine type, passed verbatim to mapKind (e.g. \"INT64\", a Postgres OID, \"VARCHAR\", a field object). */\n type: unknown;\n}\n\n/** Map an engine's column type to our FieldKind. The one piece of real per-engine work. */\nexport type KindMapper = (type: unknown, column: SourceColumn) => FieldKind;\n\n/** Coerce one cell value to a flat scalar, guided by the resolved kind. */\nexport type CellNormalizer = (value: unknown, kind: FieldKind, column: SourceColumn) => unknown;\n\nexport interface BuildChartDataOptions {\n rows: Record<string, unknown>[];\n columns: SourceColumn[];\n mapKind: KindMapper;\n /** Optional; defaults to {@link defaultNormalizeCell}, which suits drivers that return JS-native values. */\n normalizeCell?: CellNormalizer;\n}\n\n/** Default cell normalizer: pass scalars through, coerce numbers/booleans, stringify non-scalars.\n * Handles `{ value }` wrapper objects (some drivers wrap typed values). Adapters whose driver\n * needs special handling (e.g. BigQuery's typed-string + wrapper mix) supply their own. */\nexport function defaultNormalizeCell(value: unknown, kind: FieldKind): unknown {\n if (value == null) return null;\n const obj = typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n if (kind === \"number\") {\n const raw = obj ? (\"value\" in obj ? obj.value : (value as { toString(): string }).toString()) : value;\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n }\n if (kind === \"boolean\") {\n if (typeof value === \"boolean\") return value;\n const s = String(obj && \"value\" in obj ? obj.value : value).toLowerCase();\n return s === \"true\" || s === \"1\" ? true : s === \"false\" || s === \"0\" ? false : null;\n }\n if (kind === \"time\") {\n if (obj && \"value\" in obj) return String(obj.value);\n return value instanceof Date ? value.toISOString() : typeof value === \"string\" ? value : String(value);\n }\n // string-ish: unwrap { value }, stringify arrays/objects so the scalar guard passes, else pass through.\n if (obj) {\n if (Array.isArray(value)) return JSON.stringify(value);\n if (\"value\" in obj) return String(obj.value);\n return JSON.stringify(value);\n }\n return value;\n}\n\n/**\n * Turn a SQL result (rows + column types) into typed ChartData. Builds a complete FieldMeta per\n * column from the engine kind + name-based format/granularity hints, then normalizes every cell.\n */\nexport function buildChartData({\n rows,\n columns,\n mapKind,\n normalizeCell = defaultNormalizeCell,\n}: BuildChartDataOptions): ChartData {\n const fields: FieldMeta[] = columns.map((col) => {\n const kind = mapKind(col.type, col);\n const format = kind === \"number\" ? formatHint(col.name) : undefined;\n const granularity = kind === \"time\" ? granularityHint(col.name) : undefined;\n return {\n name: col.name,\n kind,\n role: roleFromKind(kind),\n label: titleCase(col.name),\n ...(format && { format }),\n ...(granularity && { granularity }),\n };\n });\n const kindByName = new Map(fields.map((f) => [f.name, f.kind!]));\n const out = rows.map((row) => {\n const r: Record<string, unknown> = {};\n for (const col of columns) r[col.name] = normalizeCell(row[col.name], kindByName.get(col.name)!, col);\n return r;\n });\n return { rows: out, fields };\n}\n\n/**\n * Reject anything that isn't a single SELECT/WITH statement. A cheap check on LLM-authored SQL;\n * enforce real read-only access with a database role.\n */\nexport function assertReadOnlySql(sql: string): void {\n const t = sql.trim().replace(/;\\s*$/, \"\");\n if (/;/.test(t)) throw new Error(\"Only a single statement is allowed.\");\n if (!/^(select|with)\\b/i.test(t)) throw new Error(\"Only read-only SELECT queries are allowed.\");\n if (\n /\\b(insert|update|delete|merge|drop|alter|create|truncate|grant|revoke|call|export|load|copy|execute|attach|vacuum|lock)\\b/i.test(\n t,\n )\n ) {\n throw new Error(\"Write/DDL statements are not allowed.\");\n }\n}\n"],"mappings":"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","names":[]}