@miao-vision/cli 0.1.3 → 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 +467 -82
- package/dist/types.ts +4 -1
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -40685,10 +40685,20 @@ function coerceCell(value) {
|
|
|
40685
40685
|
}
|
|
40686
40686
|
|
|
40687
40687
|
// src/data-profiler.ts
|
|
40688
|
-
function
|
|
40689
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,7 +56760,225 @@ 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));
|
|
56654
56984
|
if (args.command === "--help" || args.command === "-h" || args.command === "help" || !args.command) {
|
|
@@ -56680,8 +57010,12 @@ async function main() {
|
|
|
56680
57010
|
printJson(runDeck(args));
|
|
56681
57011
|
return;
|
|
56682
57012
|
}
|
|
57013
|
+
if (args.command === "query") {
|
|
57014
|
+
printJson(runQuery(args));
|
|
57015
|
+
return;
|
|
57016
|
+
}
|
|
56683
57017
|
printJson(agentError("UNKNOWN_COMMAND", `Unknown command: ${args.command ?? "(none)"}`, {
|
|
56684
|
-
commands: ["profile", "validate", "catalog", "render", "deck"]
|
|
57018
|
+
commands: ["profile", "validate", "catalog", "render", "deck", "query"]
|
|
56685
57019
|
}));
|
|
56686
57020
|
process.exitCode = 1;
|
|
56687
57021
|
} catch (error51) {
|
|
@@ -56691,13 +57025,19 @@ async function main() {
|
|
|
56691
57025
|
}
|
|
56692
57026
|
function runProfile(args) {
|
|
56693
57027
|
const file2 = args.positional[0];
|
|
56694
|
-
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>]"));
|
|
56695
57029
|
const dataset = loadDataset(file2, {
|
|
56696
57030
|
sheet: stringFlag(args, "sheet"),
|
|
56697
57031
|
limit: numberFlag(args, "limit")
|
|
56698
57032
|
});
|
|
56699
57033
|
if (isAgentError(dataset)) return fail(dataset);
|
|
56700
|
-
|
|
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 }) };
|
|
56701
57041
|
}
|
|
56702
57042
|
function runValidate(args) {
|
|
56703
57043
|
const specPath = requiredFlag(args, "spec");
|
|
@@ -56767,14 +57107,30 @@ function runDeck(args) {
|
|
|
56767
57107
|
});
|
|
56768
57108
|
if (isAgentError(dataset)) return fail(dataset);
|
|
56769
57109
|
const raw = readSpec(specPath);
|
|
56770
|
-
const parsed =
|
|
56771
|
-
if (
|
|
56772
|
-
|
|
56773
|
-
|
|
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);
|
|
56774
57115
|
const themeFlag = stringFlag(args, "theme");
|
|
56775
|
-
const html = renderDeckHtml(
|
|
57116
|
+
const html = renderDeckHtml(validation.value, dataset.value.rows, themeFlag);
|
|
56776
57117
|
writeOutput(output, html);
|
|
56777
|
-
return { ok: true, value: { output, slides:
|
|
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 };
|
|
56778
57134
|
}
|
|
56779
57135
|
function normalizeSpec(spec) {
|
|
56780
57136
|
const parsed = singleOrReportSpecSchema.safeParse(spec);
|
|
@@ -56813,6 +57169,10 @@ function parseArgs(argv) {
|
|
|
56813
57169
|
const value = rest[i];
|
|
56814
57170
|
if (value.startsWith("--")) {
|
|
56815
57171
|
const key = value.slice(2);
|
|
57172
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
57173
|
+
flags[key] = true;
|
|
57174
|
+
continue;
|
|
57175
|
+
}
|
|
56816
57176
|
const next = rest[i + 1];
|
|
56817
57177
|
if (!next || next.startsWith("--")) {
|
|
56818
57178
|
flags[key] = true;
|
|
@@ -56864,11 +57224,20 @@ var COMMAND_HELP = {
|
|
|
56864
57224
|
Profile a data file and output column statistics.
|
|
56865
57225
|
|
|
56866
57226
|
Arguments:
|
|
56867
|
-
file
|
|
57227
|
+
file Path to CSV, Excel (.xlsx/.xls), or JSON file
|
|
56868
57228
|
|
|
56869
57229
|
Options:
|
|
56870
|
-
--
|
|
56871
|
-
--
|
|
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
|
|
56872
57241
|
`,
|
|
56873
57242
|
validate: `Usage: miao-viz validate --spec <file> --profile <file>
|
|
56874
57243
|
|
|
@@ -56906,6 +57275,21 @@ Options:
|
|
|
56906
57275
|
--theme <name> Theme: default, editorial, dark, minimal
|
|
56907
57276
|
--sheet <name> Sheet name (Excel only)
|
|
56908
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)
|
|
56909
57293
|
`
|
|
56910
57294
|
};
|
|
56911
57295
|
function printHelp(command) {
|
|
@@ -56920,6 +57304,7 @@ Usage:
|
|
|
56920
57304
|
|
|
56921
57305
|
Commands:
|
|
56922
57306
|
profile Profile a data file (CSV, Excel, JSON)
|
|
57307
|
+
query Run an aggregation query to get real computed values
|
|
56923
57308
|
validate Validate a vizspec against a data profile
|
|
56924
57309
|
catalog List all available chart types
|
|
56925
57310
|
render Render a vizspec to HTML or SVG
|
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.
|
|
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/
|
|
22
|
+
"url": "https://github.com/miaoshou-dev/miao-vision.git",
|
|
23
23
|
"directory": "packages/miao-viz-cli"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|