@miao-vision/cli 0.1.2 → 0.1.6

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/dist/cli.cjs CHANGED
@@ -40685,10 +40685,20 @@ function coerceCell(value) {
40685
40685
  }
40686
40686
 
40687
40687
  // src/data-profiler.ts
40688
- function profileDataset(dataset) {
40689
- const columns = dataset.columns.map((col) => profileColumn(col, dataset.rows));
40688
+ function profileSummary(dataset) {
40689
+ const allCols = dataset.columns.map((name) => {
40690
+ const values = dataset.rows.map((r) => r[name]).filter((v) => v !== null && v !== void 0 && v !== "");
40691
+ const type = inferColumnType(values);
40692
+ return { name, type };
40693
+ });
40694
+ return { file: dataset.file, rows: dataset.rows.length, sheet: dataset.sheet, columns: allCols };
40695
+ }
40696
+ function profileDataset(dataset, options = {}) {
40697
+ const targetCols = options.columns ? dataset.columns.filter((c) => options.columns.includes(c)) : dataset.columns;
40698
+ const columns = targetCols.map((col) => profileColumn(col, dataset.rows, options.reliableOnly));
40690
40699
  const numericNames = columns.filter((c) => c.type === "number").map((c) => c.name);
40691
- const correlations = numericNames.length >= 2 ? computeCorrelations(numericNames, dataset.rows) : void 0;
40700
+ const rawCorrelations = numericNames.length >= 2 ? computeCorrelations(numericNames, dataset.rows) : void 0;
40701
+ const correlations = options.reliableOnly ? rawCorrelations?.filter((c) => c.reliable) : rawCorrelations;
40692
40702
  const hints = generateHints(columns, correlations ?? []);
40693
40703
  const quality = computeDataQuality(columns, dataset.rows.length);
40694
40704
  const insights = generateProfileInsights(columns, quality, correlations ?? [], dataset.rows.length);
@@ -40703,7 +40713,7 @@ function profileDataset(dataset) {
40703
40713
  insights: insights.length > 0 ? insights : void 0
40704
40714
  };
40705
40715
  }
40706
- function profileColumn(name, rows) {
40716
+ function profileColumn(name, rows, reliableOnly = false) {
40707
40717
  const values = rows.map((row) => row[name]);
40708
40718
  const nonNull = values.filter((v) => v !== null && v !== void 0 && v !== "");
40709
40719
  const type = inferColumnType(nonNull);
@@ -40729,7 +40739,7 @@ function profileColumn(name, rows) {
40729
40739
  const sorted = [...nums].sort((a, b) => a - b);
40730
40740
  profile.min = sorted[0];
40731
40741
  profile.max = sorted[sorted.length - 1];
40732
- Object.assign(profile, computeNumericStats(nums, sorted));
40742
+ Object.assign(profile, computeNumericStats(nums, sorted, rows.length, reliableOnly));
40733
40743
  }
40734
40744
  }
40735
40745
  if (type === "string") {
@@ -40752,7 +40762,7 @@ function profileColumn(name, rows) {
40752
40762
  profile.role = inferColumnRole(profile);
40753
40763
  return profile;
40754
40764
  }
40755
- function computeNumericStats(values, sorted) {
40765
+ function computeNumericStats(values, sorted, totalRows, reliableOnly) {
40756
40766
  const n = values.length;
40757
40767
  const sum = values.reduce((s, v) => s + v, 0);
40758
40768
  const mean = sum / n;
@@ -40764,18 +40774,31 @@ function computeNumericStats(values, sorted) {
40764
40774
  const skewness = stddev < 1e-10 ? 0 : round(3 * (mean - median) / stddev);
40765
40775
  const iqr = p75 - p25;
40766
40776
  const outlierCount = values.filter((v) => v < p25 - 1.5 * iqr || v > p75 + 1.5 * iqr).length;
40767
- return {
40777
+ const skewnessReliable = totalRows >= 30;
40778
+ const outlierReliable = totalRows >= 20;
40779
+ const histogramReliable = totalRows >= 20;
40780
+ const result = {
40768
40781
  sum: round(sum),
40769
40782
  mean: round(mean),
40770
40783
  median: round(median),
40771
40784
  p25: round(p25),
40772
40785
  p75: round(p75),
40773
40786
  stddev: round(stddev),
40774
- skewness,
40775
- coefficientOfVariation: Math.abs(mean) < 1e-10 ? void 0 : round(stddev / Math.abs(mean)),
40776
- outlierCount,
40777
- histogram: computeHistogram(values, sorted[0], sorted[n - 1])
40787
+ coefficientOfVariation: Math.abs(mean) < 1e-10 ? void 0 : round(stddev / Math.abs(mean))
40778
40788
  };
40789
+ if (!reliableOnly || skewnessReliable) {
40790
+ result.skewness = skewness;
40791
+ result.skewnessReliable = skewnessReliable;
40792
+ }
40793
+ if (!reliableOnly || outlierReliable) {
40794
+ result.outlierCount = outlierCount;
40795
+ result.outlierReliable = outlierReliable;
40796
+ }
40797
+ if (!reliableOnly || histogramReliable) {
40798
+ result.histogram = computeHistogram(values, sorted[0], sorted[n - 1]);
40799
+ result.histogramReliable = histogramReliable;
40800
+ }
40801
+ return result;
40779
40802
  }
40780
40803
  function percentile(sorted, p) {
40781
40804
  if (sorted.length === 0) return 0;
@@ -40862,7 +40885,7 @@ function computeCorrelations(numericNames, rows) {
40862
40885
  const pairs = rows.map((row) => [Number(row[a]), Number(row[b])]).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));
40863
40886
  if (pairs.length < 3) continue;
40864
40887
  const r = pearsonR(pairs);
40865
- if (Math.abs(r) >= 0.3) result.push({ a, b, r: round(r) });
40888
+ if (Math.abs(r) >= 0.3) result.push({ a, b, r: round(r), n: pairs.length, reliable: pairs.length >= 10 });
40866
40889
  }
40867
40890
  }
40868
40891
  return result.sort((x, y) => Math.abs(y.r) - Math.abs(x.r));
@@ -40953,14 +40976,6 @@ function generateProfileInsights(columns, quality, correlations, rowCount) {
40953
40976
  });
40954
40977
  }
40955
40978
  for (const col of columns) {
40956
- if (col.role === "dimension" && col.distinctCount >= 2 && col.distinctCount <= 12 && col.topSharePct && col.topSharePct >= 0.5) {
40957
- insights.push({
40958
- type: "suggestion",
40959
- title: `Dominant category in ${col.name}`,
40960
- description: `${String(col.topValue)} accounts for ${Math.round(col.topSharePct * 100)}% of non-empty values; annotate this imbalance in share or ranking charts.`,
40961
- fields: [col.name]
40962
- });
40963
- }
40964
40979
  if (col.role === "measure" && col.coefficientOfVariation !== void 0 && col.coefficientOfVariation >= 1) {
40965
40980
  insights.push({
40966
40981
  type: "trend",
@@ -41024,6 +41039,153 @@ function round(value) {
41024
41039
  return Math.round(value * 1e4) / 1e4;
41025
41040
  }
41026
41041
 
41042
+ // src/data-query.ts
41043
+ var SUPPORTED_OPS = ["sum", "count", "avg", "min", "max"];
41044
+ function queryDataset(rows, options) {
41045
+ const groupByCols = options.groupby ? options.groupby.split(",").map((c) => c.trim()).filter(Boolean) : [];
41046
+ const measuresResult = options.measure ? parseMeasures(options.measure) : [];
41047
+ if (isAgentError(measuresResult)) return measuresResult;
41048
+ const measures = measuresResult;
41049
+ let current;
41050
+ if (options.filter) {
41051
+ const filterResult = applyFilter(rows, options.filter);
41052
+ if (!filterResult.ok) return filterResult.error;
41053
+ current = filterResult.rows;
41054
+ } else {
41055
+ current = [...rows];
41056
+ }
41057
+ if (groupByCols.length > 0 || measures.length > 0) {
41058
+ current = aggregateQuery(current, groupByCols, measures);
41059
+ }
41060
+ if (options.orderby) {
41061
+ const ordered = applyOrderBy(current, options.orderby);
41062
+ if (isAgentError(ordered)) return ordered;
41063
+ current = ordered;
41064
+ }
41065
+ if (options.limit != null && options.limit > 0) {
41066
+ current = current.slice(0, options.limit);
41067
+ }
41068
+ return {
41069
+ rows: current,
41070
+ sql: buildSql(groupByCols, measures, options.filter, options.orderby, options.limit),
41071
+ rowCount: current.length
41072
+ };
41073
+ }
41074
+ function parseMeasures(measure) {
41075
+ const parts = measure.split(",").map((s) => s.trim()).filter(Boolean);
41076
+ const results = [];
41077
+ for (const part of parts) {
41078
+ const match = part.match(/^(\w+)\(([^)]+)\)(?:\s+as\s+(\w+))?$/i);
41079
+ if (!match) {
41080
+ return agentError(
41081
+ "QUERY_INVALID_MEASURE",
41082
+ `Cannot parse measure: "${part}". Use "fn(col) as alias". Supported functions: ${SUPPORTED_OPS.join(", ")}.`
41083
+ );
41084
+ }
41085
+ const [, fnRaw, rawField, aliasRaw] = match;
41086
+ const fn = fnRaw.toLowerCase();
41087
+ if (!SUPPORTED_OPS.includes(fn)) {
41088
+ return agentError(
41089
+ "QUERY_UNSUPPORTED_FUNCTION",
41090
+ `Unsupported aggregate function: "${fn}". Supported: ${SUPPORTED_OPS.join(", ")}.`
41091
+ );
41092
+ }
41093
+ const field = rawField.trim();
41094
+ const alias = aliasRaw ?? `${fn}_${field.replace("*", "all")}`;
41095
+ results.push({ fn, field, alias });
41096
+ }
41097
+ return results;
41098
+ }
41099
+ function applyFilter(rows, filter) {
41100
+ const match = filter.match(/^(\w+)\s*=\s*(.+)$/);
41101
+ if (!match) {
41102
+ return {
41103
+ ok: false,
41104
+ error: agentError(
41105
+ "QUERY_INVALID_FILTER",
41106
+ `Cannot parse filter: "${filter}". Use "column=value" (simple equality only).`
41107
+ )
41108
+ };
41109
+ }
41110
+ const [, col, rawVal] = match;
41111
+ const val = rawVal.trim();
41112
+ const numVal = Number(val);
41113
+ const filtered = rows.filter((row) => {
41114
+ const cell = row[col];
41115
+ if (Number.isFinite(numVal) && Number.isFinite(Number(cell))) {
41116
+ return Number(cell) === numVal;
41117
+ }
41118
+ return String(cell ?? "") === val;
41119
+ });
41120
+ return { ok: true, rows: filtered };
41121
+ }
41122
+ function aggregateQuery(rows, groupByCols, measures) {
41123
+ if (groupByCols.length === 0) {
41124
+ const out = {};
41125
+ for (const m of measures) {
41126
+ out[m.alias] = computeAggregate(rows, m);
41127
+ }
41128
+ return [out];
41129
+ }
41130
+ const groups = /* @__PURE__ */ new Map();
41131
+ for (const row of rows) {
41132
+ const key = JSON.stringify(groupByCols.map((c) => row[c]));
41133
+ const existing = groups.get(key) ?? [];
41134
+ existing.push(row);
41135
+ groups.set(key, existing);
41136
+ }
41137
+ return Array.from(groups.values()).map((groupRows) => {
41138
+ const first = groupRows[0];
41139
+ const out = {};
41140
+ for (const col of groupByCols) {
41141
+ out[col] = first[col];
41142
+ }
41143
+ for (const m of measures) {
41144
+ out[m.alias] = computeAggregate(groupRows, m);
41145
+ }
41146
+ return out;
41147
+ });
41148
+ }
41149
+ function computeAggregate(rows, measure) {
41150
+ if (measure.fn === "count") return rows.length;
41151
+ const values = rows.map((r) => Number(r[measure.field])).filter((v) => Number.isFinite(v));
41152
+ if (values.length === 0) return 0;
41153
+ if (measure.fn === "avg") return values.reduce((s, v) => s + v, 0) / values.length;
41154
+ if (measure.fn === "min") return Math.min(...values);
41155
+ if (measure.fn === "max") return Math.max(...values);
41156
+ return values.reduce((s, v) => s + v, 0);
41157
+ }
41158
+ function applyOrderBy(rows, orderby) {
41159
+ const parts = orderby.trim().split(/\s+/);
41160
+ const col = parts[0];
41161
+ const dir = (parts[1] ?? "desc").toLowerCase();
41162
+ if (dir !== "asc" && dir !== "desc") {
41163
+ return agentError("QUERY_INVALID_ORDERBY", `Invalid sort direction: "${dir}". Use "asc" or "desc".`);
41164
+ }
41165
+ const sign = dir === "asc" ? 1 : -1;
41166
+ return [...rows].sort((a, b) => {
41167
+ const aNum = Number(a[col]);
41168
+ const bNum = Number(b[col]);
41169
+ if (Number.isFinite(aNum) && Number.isFinite(bNum)) return (aNum - bNum) * sign;
41170
+ return String(a[col] ?? "").localeCompare(String(b[col] ?? "")) * sign;
41171
+ });
41172
+ }
41173
+ function buildSql(groupByCols, measures, filter, orderby, limit) {
41174
+ const selectParts = [
41175
+ ...groupByCols,
41176
+ ...measures.map((m) => `${m.fn.toUpperCase()}(${m.field}) AS ${m.alias}`)
41177
+ ];
41178
+ let sql = `SELECT ${selectParts.length > 0 ? selectParts.join(", ") : "*"} FROM data`;
41179
+ if (filter) {
41180
+ const [col, val] = filter.split("=", 2);
41181
+ sql += ` WHERE ${col.trim()} = ${val.trim()}`;
41182
+ }
41183
+ if (groupByCols.length > 0) sql += ` GROUP BY ${groupByCols.join(", ")}`;
41184
+ if (orderby) sql += ` ORDER BY ${orderby}`;
41185
+ if (limit != null) sql += ` LIMIT ${limit}`;
41186
+ return sql;
41187
+ }
41188
+
41027
41189
  // src/data-transform.ts
41028
41190
  function prepareChartData(rows, chart) {
41029
41191
  let current = [...rows];
@@ -56251,56 +56413,6 @@ function collectDerivedFields(chart) {
56251
56413
  return fields;
56252
56414
  }
56253
56415
 
56254
- // src/deck-schema.ts
56255
- var SLIDE_LAYOUTS = [
56256
- "cover",
56257
- "title-only",
56258
- "text-points",
56259
- "text-chart",
56260
- "metrics-chart",
56261
- "chart-full",
56262
- "table-full",
56263
- "ending"
56264
- ];
56265
- var metricTransformSchema = external_exports.object({
56266
- type: external_exports.enum(["derive-month", "aggregate", "sort", "limit", "filter"]),
56267
- field: external_exports.string().optional(),
56268
- as: external_exports.string().optional(),
56269
- groupBy: external_exports.array(external_exports.string()).optional(),
56270
- measures: external_exports.array(external_exports.object({
56271
- field: external_exports.string(),
56272
- op: external_exports.enum(["sum", "avg", "count", "min", "max"]),
56273
- as: external_exports.string()
56274
- })).optional(),
56275
- order: external_exports.enum(["asc", "desc"]).optional(),
56276
- value: external_exports.unknown().optional()
56277
- });
56278
- var slideMetricSchema = external_exports.object({
56279
- label: external_exports.string().min(1),
56280
- value: external_exports.union([external_exports.string(), external_exports.number()]).optional(),
56281
- format: external_exports.string().optional(),
56282
- data: external_exports.object({
56283
- transform: external_exports.array(metricTransformSchema).optional()
56284
- }).optional()
56285
- });
56286
- var slideSpecSchema = external_exports.object({
56287
- layout: external_exports.enum(SLIDE_LAYOUTS),
56288
- eyebrow: external_exports.string().optional(),
56289
- title: external_exports.string().optional(),
56290
- claim: external_exports.string().optional(),
56291
- bullets: external_exports.array(external_exports.string()).optional(),
56292
- callout: external_exports.string().optional(),
56293
- annotation: external_exports.string().optional(),
56294
- metrics: external_exports.array(slideMetricSchema).optional(),
56295
- charts: external_exports.array(chartSpecSchema).optional()
56296
- });
56297
- var deckSpecSchema = external_exports.object({
56298
- title: external_exports.string().optional(),
56299
- description: external_exports.string().optional(),
56300
- theme: external_exports.enum(["default", "editorial", "dark", "minimal"]).optional(),
56301
- slides: external_exports.array(slideSpecSchema).min(1, "Deck must have at least one slide")
56302
- });
56303
-
56304
56416
  // src/deck-layouts.ts
56305
56417
  function withSize(chart, width, height) {
56306
56418
  return { ...chart, style: { ...chart.style, width, height } };
@@ -56648,9 +56760,235 @@ function renderDeckHtml(spec, rows, themeOverride) {
56648
56760
  </html>`;
56649
56761
  }
56650
56762
 
56763
+ // src/deck-schema.ts
56764
+ var SLIDE_LAYOUTS = [
56765
+ "cover",
56766
+ "title-only",
56767
+ "text-points",
56768
+ "text-chart",
56769
+ "metrics-chart",
56770
+ "chart-full",
56771
+ "table-full",
56772
+ "ending"
56773
+ ];
56774
+ var metricTransformSchema = external_exports.object({
56775
+ type: external_exports.enum(["derive-month", "aggregate", "sort", "limit", "filter"]),
56776
+ field: external_exports.string().optional(),
56777
+ as: external_exports.string().optional(),
56778
+ groupBy: external_exports.array(external_exports.string()).optional(),
56779
+ measures: external_exports.array(external_exports.object({
56780
+ field: external_exports.string(),
56781
+ op: external_exports.enum(["sum", "avg", "count", "min", "max"]),
56782
+ as: external_exports.string()
56783
+ })).optional(),
56784
+ order: external_exports.enum(["asc", "desc"]).optional(),
56785
+ value: external_exports.unknown().optional()
56786
+ });
56787
+ var slideMetricSchema = external_exports.object({
56788
+ label: external_exports.string().min(1),
56789
+ value: external_exports.union([external_exports.string(), external_exports.number()]).optional(),
56790
+ format: external_exports.string().optional(),
56791
+ data: external_exports.object({
56792
+ transform: external_exports.array(metricTransformSchema).optional()
56793
+ }).optional()
56794
+ });
56795
+ var slideSpecSchema = external_exports.object({
56796
+ layout: external_exports.enum(SLIDE_LAYOUTS),
56797
+ eyebrow: external_exports.string().optional(),
56798
+ title: external_exports.string().optional(),
56799
+ claim: external_exports.string().optional(),
56800
+ bullets: external_exports.array(external_exports.string()).optional(),
56801
+ callout: external_exports.string().optional(),
56802
+ annotation: external_exports.string().optional(),
56803
+ metrics: external_exports.array(slideMetricSchema).optional(),
56804
+ charts: external_exports.array(chartSpecSchema).optional()
56805
+ });
56806
+ var deckSpecSchema = external_exports.object({
56807
+ title: external_exports.string().optional(),
56808
+ description: external_exports.string().optional(),
56809
+ theme: external_exports.enum(["default", "editorial", "dark", "minimal"]).optional(),
56810
+ slides: external_exports.array(slideSpecSchema).min(1, "Deck must have at least one slide")
56811
+ });
56812
+
56813
+ // src/deck-validator.ts
56814
+ var REQUIRED_ENCODINGS2 = {
56815
+ bar: ["x", "y"],
56816
+ line: ["x", "y"],
56817
+ area: ["x", "y"],
56818
+ pie: ["label", "value"],
56819
+ scatter: ["x", "y"],
56820
+ histogram: ["x"],
56821
+ heatmap: ["x", "y", "value"],
56822
+ table: [],
56823
+ bigvalue: ["value"]
56824
+ };
56825
+ function parseDeckSpec(spec) {
56826
+ const parsed = deckSpecSchema.safeParse(spec);
56827
+ if (!parsed.success) {
56828
+ const errors = formatDeckSpecIssues(parsed.error);
56829
+ return agentError("INVALID_DECK_SPEC", errors[0]?.message ?? "DeckSpec is invalid.", { errors });
56830
+ }
56831
+ const semanticErrors = validateDeckSpecSemantics(parsed.data);
56832
+ if (semanticErrors.length > 0) {
56833
+ return agentError("INVALID_DECK_SPEC", semanticErrors[0].message, { errors: semanticErrors });
56834
+ }
56835
+ return ok(parsed.data);
56836
+ }
56837
+ function validateDeckSpecSemantics(spec) {
56838
+ const errors = [];
56839
+ spec.slides.forEach((slide, index) => {
56840
+ if (["text-chart", "metrics-chart", "chart-full"].includes(slide.layout) && !slide.charts?.length) {
56841
+ const path = `slides[${index}].charts`;
56842
+ errors.push({
56843
+ path,
56844
+ message: `${path}: Layout '${slide.layout}' requires at least one chart.`,
56845
+ hint: hintForIssue(path, "requires at least one chart")
56846
+ });
56847
+ }
56848
+ if (slide.layout === "metrics-chart" && !slide.metrics?.length) {
56849
+ const path = `slides[${index}].metrics`;
56850
+ errors.push({
56851
+ path,
56852
+ message: `${path}: Layout 'metrics-chart' requires at least one metric.`,
56853
+ hint: hintForIssue(path, "requires at least one metric")
56854
+ });
56855
+ }
56856
+ if ((slide.metrics?.length ?? 0) > 4) {
56857
+ const path = `slides[${index}].metrics`;
56858
+ errors.push({
56859
+ path,
56860
+ message: `${path}: A slide can include at most 4 metrics.`,
56861
+ hint: hintForIssue(path, "at most 4 metrics")
56862
+ });
56863
+ }
56864
+ if (slide.layout === "table-full" && slide.charts?.[0] && slide.charts[0].type !== "table") {
56865
+ const path = `slides[${index}].charts[0].type`;
56866
+ errors.push({
56867
+ path,
56868
+ message: `${path}: Layout 'table-full' only accepts a table chart.`,
56869
+ hint: hintForIssue(path, "only accepts a table chart")
56870
+ });
56871
+ }
56872
+ });
56873
+ return errors;
56874
+ }
56875
+ function validateDeckFields(spec, profile) {
56876
+ const sourceFields = new Set(profile.columns.map((column) => column.name));
56877
+ for (let slideIndex = 0; slideIndex < spec.slides.length; slideIndex += 1) {
56878
+ const slide = spec.slides[slideIndex];
56879
+ for (let chartIndex = 0; chartIndex < (slide.charts ?? []).length; chartIndex += 1) {
56880
+ const result = validateChartFields(slide.charts[chartIndex], sourceFields, `slides[${slideIndex}].charts[${chartIndex}]`);
56881
+ if (isAgentError(result)) return result;
56882
+ }
56883
+ for (let metricIndex = 0; metricIndex < (slide.metrics ?? []).length; metricIndex += 1) {
56884
+ const metric = slide.metrics[metricIndex];
56885
+ const result = validateMetricFields(metric, sourceFields, `slides[${slideIndex}].metrics[${metricIndex}]`);
56886
+ if (isAgentError(result)) return result;
56887
+ }
56888
+ }
56889
+ return ok(spec);
56890
+ }
56891
+ function formatDeckSpecIssues(error51) {
56892
+ return error51.issues.map((issue2) => {
56893
+ const path = issue2.path.length ? formatPath(issue2.path) : "deck";
56894
+ return {
56895
+ path,
56896
+ message: `${path}: ${issue2.message}`,
56897
+ hint: hintForIssue(path, issue2.message)
56898
+ };
56899
+ });
56900
+ }
56901
+ function validateChartFields(chart, sourceFields, path) {
56902
+ if (!MVP_CHART_TYPES.includes(chart.type)) {
56903
+ return deckFieldError(path, chart.type, `Chart type '${chart.type}' is not supported.`);
56904
+ }
56905
+ for (const encoding of REQUIRED_ENCODINGS2[chart.type] ?? []) {
56906
+ if (!chart.encoding[encoding]?.field) {
56907
+ return deckFieldError(`${path}.encoding.${encoding}`, encoding, `Chart type '${chart.type}' requires encoding '${encoding}'.`);
56908
+ }
56909
+ }
56910
+ const available = applyTransforms(chart.data?.transform ?? [], sourceFields, path);
56911
+ if (isAgentError(available)) return available;
56912
+ for (const [encoding, spec] of Object.entries(chart.encoding)) {
56913
+ if (spec?.field && !available.value.has(spec.field)) {
56914
+ return deckFieldError(`${path}.encoding.${encoding}.field`, spec.field, `Field '${spec.field}' is not available for this chart encoding.`);
56915
+ }
56916
+ }
56917
+ return ok(chart);
56918
+ }
56919
+ function validateMetricFields(metric, sourceFields, path) {
56920
+ const available = applyTransforms(metric.data?.transform ?? [], sourceFields, path);
56921
+ if (isAgentError(available)) return available;
56922
+ return ok(metric);
56923
+ }
56924
+ function applyTransforms(transforms, sourceFields, path) {
56925
+ let available = new Set(sourceFields);
56926
+ for (let index = 0; index < transforms.length; index += 1) {
56927
+ const transform2 = transforms[index];
56928
+ const transformPath = `${path}.data.transform[${index}]`;
56929
+ if (transform2.field && !available.has(transform2.field)) {
56930
+ return deckFieldError(`${transformPath}.field`, transform2.field, `Field '${transform2.field}' was not found before transform '${transform2.type}'.`);
56931
+ }
56932
+ for (const field of transform2.groupBy ?? []) {
56933
+ if (!available.has(field)) {
56934
+ return deckFieldError(`${transformPath}.groupBy`, field, `Group field '${field}' was not found before transform '${transform2.type}'.`);
56935
+ }
56936
+ }
56937
+ for (const measure of transform2.measures ?? []) {
56938
+ if (!available.has(measure.field)) {
56939
+ return deckFieldError(`${transformPath}.measures.${measure.as}`, measure.field, `Measure field '${measure.field}' was not found before transform '${transform2.type}'.`);
56940
+ }
56941
+ }
56942
+ available = nextAvailableFields(available, transform2);
56943
+ }
56944
+ return ok(available);
56945
+ }
56946
+ function nextAvailableFields(current, transform2) {
56947
+ if (transform2.type === "derive-month" && transform2.as) {
56948
+ return /* @__PURE__ */ new Set([...current, transform2.as]);
56949
+ }
56950
+ if (transform2.type === "aggregate") {
56951
+ return /* @__PURE__ */ new Set([
56952
+ ...transform2.groupBy ?? [],
56953
+ ...(transform2.measures ?? []).map((measure) => measure.as)
56954
+ ]);
56955
+ }
56956
+ return current;
56957
+ }
56958
+ function deckFieldError(path, field, message) {
56959
+ return agentError("DECK_FIELD_NOT_FOUND", message, {
56960
+ path,
56961
+ field,
56962
+ hint: `Check ${path} and use a field from the input data or a field created by an earlier transform.`
56963
+ });
56964
+ }
56965
+ function formatPath(path) {
56966
+ return path.reduce((result, item) => {
56967
+ if (typeof item === "number") return `${result}[${item}]`;
56968
+ const segment = String(item);
56969
+ return result ? `${result}.${segment}` : segment;
56970
+ }, "");
56971
+ }
56972
+ function hintForIssue(path, message) {
56973
+ if (message.includes("requires at least one chart")) return `Add a chart under ${path}.`;
56974
+ if (message.includes("requires at least one metric")) return `Add one to four metrics under ${path}.`;
56975
+ if (message.includes("at most 4 metrics")) return `Reduce ${path} to four metrics or split them across multiple slides.`;
56976
+ if (message.includes("only accepts a table chart")) return `Change ${path} to 'table' or use a chart-focused layout.`;
56977
+ return `Check ${path} in the DeckSpec.`;
56978
+ }
56979
+
56651
56980
  // src/cli.ts
56981
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["h", "help", "summary", "reliable-only"]);
56652
56982
  async function main() {
56653
56983
  const args = parseArgs(process.argv.slice(2));
56984
+ if (args.command === "--help" || args.command === "-h" || args.command === "help" || !args.command) {
56985
+ printHelp();
56986
+ return;
56987
+ }
56988
+ if (args.flags["help"] === true || args.flags["h"] === true) {
56989
+ printHelp(args.command);
56990
+ return;
56991
+ }
56654
56992
  try {
56655
56993
  if (args.command === "profile") {
56656
56994
  printJson(runProfile(args));
@@ -56672,8 +57010,12 @@ async function main() {
56672
57010
  printJson(runDeck(args));
56673
57011
  return;
56674
57012
  }
57013
+ if (args.command === "query") {
57014
+ printJson(runQuery(args));
57015
+ return;
57016
+ }
56675
57017
  printJson(agentError("UNKNOWN_COMMAND", `Unknown command: ${args.command ?? "(none)"}`, {
56676
- commands: ["profile", "validate", "catalog", "render", "deck"]
57018
+ commands: ["profile", "validate", "catalog", "render", "deck", "query"]
56677
57019
  }));
56678
57020
  process.exitCode = 1;
56679
57021
  } catch (error51) {
@@ -56683,13 +57025,19 @@ async function main() {
56683
57025
  }
56684
57026
  function runProfile(args) {
56685
57027
  const file2 = args.positional[0];
56686
- if (!file2) return fail(agentError("MISSING_INPUT", "Usage: miao-viz profile <file> [--sheet <name>] [--limit <rows>]"));
57028
+ if (!file2) return fail(agentError("MISSING_INPUT", "Usage: miao-viz profile <file> [--summary] [--columns col1,col2] [--reliable-only] [--sheet <name>] [--limit <rows>]"));
56687
57029
  const dataset = loadDataset(file2, {
56688
57030
  sheet: stringFlag(args, "sheet"),
56689
57031
  limit: numberFlag(args, "limit")
56690
57032
  });
56691
57033
  if (isAgentError(dataset)) return fail(dataset);
56692
- return { ok: true, value: profileDataset(dataset.value) };
57034
+ if (args.flags["summary"] === true) {
57035
+ return { ok: true, value: profileSummary(dataset.value) };
57036
+ }
57037
+ const columnsFlag = stringFlag(args, "columns");
57038
+ const columns = columnsFlag ? columnsFlag.split(",").map((c) => c.trim()).filter(Boolean) : void 0;
57039
+ const reliableOnly = args.flags["reliable-only"] === true;
57040
+ return { ok: true, value: profileDataset(dataset.value, { columns, reliableOnly }) };
56693
57041
  }
56694
57042
  function runValidate(args) {
56695
57043
  const specPath = requiredFlag(args, "spec");
@@ -56759,14 +57107,30 @@ function runDeck(args) {
56759
57107
  });
56760
57108
  if (isAgentError(dataset)) return fail(dataset);
56761
57109
  const raw = readSpec(specPath);
56762
- const parsed = deckSpecSchema.safeParse(raw);
56763
- if (!parsed.success) {
56764
- return fail(agentError("INVALID_DECK_SPEC", parsed.error.issues.map((i) => i.message).join("; ")));
56765
- }
57110
+ const parsed = parseDeckSpec(raw);
57111
+ if (isAgentError(parsed)) return fail(parsed);
57112
+ const profile = profileDataset(dataset.value);
57113
+ const validation = validateDeckFields(parsed.value, profile);
57114
+ if (isAgentError(validation)) return fail(validation);
56766
57115
  const themeFlag = stringFlag(args, "theme");
56767
- const html = renderDeckHtml(parsed.data, dataset.value.rows, themeFlag);
57116
+ const html = renderDeckHtml(validation.value, dataset.value.rows, themeFlag);
56768
57117
  writeOutput(output, html);
56769
- return { ok: true, value: { output, slides: parsed.data.slides.length } };
57118
+ return { ok: true, value: { output, slides: validation.value.slides.length } };
57119
+ }
57120
+ function runQuery(args) {
57121
+ const file2 = args.positional[0];
57122
+ if (!file2) return fail(agentError("MISSING_INPUT", 'Usage: miao-viz query <file> [--groupby cols] [--measure "fn(col) as alias"] [--filter col=val] [--orderby "col desc"] [--limit n]'));
57123
+ const dataset = loadDataset(file2, { sheet: stringFlag(args, "sheet") });
57124
+ if (isAgentError(dataset)) return fail(dataset);
57125
+ const result = queryDataset(dataset.value.rows, {
57126
+ groupby: stringFlag(args, "groupby"),
57127
+ measure: stringFlag(args, "measure"),
57128
+ filter: stringFlag(args, "filter"),
57129
+ orderby: stringFlag(args, "orderby"),
57130
+ limit: numberFlag(args, "limit")
57131
+ });
57132
+ if (isAgentError(result)) return fail(result);
57133
+ return { ok: true, value: result };
56770
57134
  }
56771
57135
  function normalizeSpec(spec) {
56772
57136
  const parsed = singleOrReportSpecSchema.safeParse(spec);
@@ -56805,6 +57169,10 @@ function parseArgs(argv) {
56805
57169
  const value = rest[i];
56806
57170
  if (value.startsWith("--")) {
56807
57171
  const key = value.slice(2);
57172
+ if (BOOLEAN_FLAGS.has(key)) {
57173
+ flags[key] = true;
57174
+ continue;
57175
+ }
56808
57176
  const next = rest[i + 1];
56809
57177
  if (!next || next.startsWith("--")) {
56810
57178
  flags[key] = true;
@@ -56850,6 +57218,101 @@ function printJson(value) {
56850
57218
  process.stdout.write(`${JSON.stringify(value, null, 2)}
56851
57219
  `);
56852
57220
  }
57221
+ var COMMAND_HELP = {
57222
+ profile: `Usage: miao-viz profile <file> [options]
57223
+
57224
+ Profile a data file and output column statistics.
57225
+
57226
+ Arguments:
57227
+ file Path to CSV, Excel (.xlsx/.xls), or JSON file
57228
+
57229
+ Options:
57230
+ --summary Return only file, row count, and column names+types (~200 tokens)
57231
+ --columns col1,col2 Deep-profile only the specified columns (comma-separated)
57232
+ --reliable-only Suppress statistics where sample size is too small to be reliable
57233
+ --sheet <name> Sheet name (Excel only)
57234
+ --limit <n> Max rows to read
57235
+
57236
+ Reliability thresholds:
57237
+ skewness rows >= 30
57238
+ correlation n >= 10 (paired non-null values)
57239
+ outlierCount rows >= 20
57240
+ histogram rows >= 20
57241
+ `,
57242
+ validate: `Usage: miao-viz validate --spec <file> --profile <file>
57243
+
57244
+ Validate a vizspec against a data profile.
57245
+
57246
+ Options:
57247
+ --spec <file> Path to vizspec YAML/JSON
57248
+ --profile <file> Path to profile JSON (output of "profile")
57249
+ `,
57250
+ catalog: `Usage: miao-viz catalog
57251
+
57252
+ List all available chart types and their required fields.
57253
+ `,
57254
+ render: `Usage: miao-viz render --input <file> --spec <file> --output <file> [options]
57255
+
57256
+ Render a vizspec to HTML or SVG.
57257
+
57258
+ Options:
57259
+ --input <file> Path to data file
57260
+ --spec <file> Path to vizspec YAML/JSON
57261
+ --output <file> Output file path
57262
+ --format <fmt> Output format: html, svg (default: html)
57263
+ --theme <name> Theme: default, editorial, dark, minimal
57264
+ --sheet <name> Sheet name (Excel only)
57265
+ --limit <n> Max rows to read
57266
+ `,
57267
+ deck: `Usage: miao-viz deck --input <file> --spec <file> --output <file> [options]
57268
+
57269
+ Render a deck spec to HTML slides.
57270
+
57271
+ Options:
57272
+ --input <file> Path to data file
57273
+ --spec <file> Path to deck spec YAML/JSON
57274
+ --output <file> Output file path
57275
+ --theme <name> Theme: default, editorial, dark, minimal
57276
+ --sheet <name> Sheet name (Excel only)
57277
+ --limit <n> Max rows to read
57278
+ `,
57279
+ query: `Usage: miao-viz query <file> [options]
57280
+
57281
+ Run an aggregation query against a data file and return JSON results.
57282
+ Use this to get real computed values before writing chart insights.
57283
+
57284
+ Supported aggregate functions: sum, count, avg, min, max
57285
+
57286
+ Options:
57287
+ --groupby <cols> Comma-separated column names to group by
57288
+ --measure <exprs> Aggregate expressions, e.g. "sum(sales) as total, count(*) as cnt"
57289
+ --filter <col=val> Simple equality filter (one condition only)
57290
+ --orderby <col dir> Sort column and direction, e.g. "total_sales desc"
57291
+ --limit <n> Max rows to return
57292
+ --sheet <name> Sheet name (Excel only)
57293
+ `
57294
+ };
57295
+ function printHelp(command) {
57296
+ if (command && COMMAND_HELP[command]) {
57297
+ process.stdout.write(COMMAND_HELP[command]);
57298
+ return;
57299
+ }
57300
+ process.stdout.write(`miao-viz \u2014 local data visualization CLI
57301
+
57302
+ Usage:
57303
+ miao-viz <command> [options]
57304
+
57305
+ Commands:
57306
+ profile Profile a data file (CSV, Excel, JSON)
57307
+ query Run an aggregation query to get real computed values
57308
+ validate Validate a vizspec against a data profile
57309
+ catalog List all available chart types
57310
+ render Render a vizspec to HTML or SVG
57311
+ deck Render a deck spec to HTML slides
57312
+
57313
+ Run "miao-viz <command> --help" for command-specific options.
57314
+ `);
57315
+ }
56853
57316
  main();
56854
57317
  /*! Bundled license information:
56855
57318
 
package/dist/types.ts CHANGED
@@ -90,9 +90,12 @@ export interface ColumnProfile {
90
90
  p75?: number
91
91
  stddev?: number
92
92
  skewness?: number
93
+ skewnessReliable?: boolean // rows >= 30
93
94
  coefficientOfVariation?: number
94
95
  outlierCount?: number
96
+ outlierReliable?: boolean // rows >= 20
95
97
  histogram?: HistogramBucket[]
98
+ histogramReliable?: boolean // rows >= 20
96
99
  // string
97
100
  topValue?: unknown
98
101
  topSharePct?: number
@@ -131,7 +134,7 @@ export interface DataProfile {
131
134
  columns: ColumnProfile[]
132
135
  sheet?: string
133
136
  quality?: DataQualityProfile
134
- correlations?: Array<{ a: string; b: string; r: number }>
137
+ correlations?: Array<{ a: string; b: string; r: number; n: number; reliable: boolean }>
135
138
  hints?: ProfileHint[]
136
139
  insights?: ProfileInsight[]
137
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miao-vision/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.6",
4
4
  "description": "Miao Vision local data visualization CLI for agent workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/guming/miao-vision.git",
22
+ "url": "https://github.com/miaoshou-dev/miao-vision.git",
23
23
  "directory": "packages/miao-viz-cli"
24
24
  },
25
25
  "publishConfig": {