@miao-vision/cli 0.1.8 → 0.1.9

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.
Files changed (3) hide show
  1. package/dist/cli.cjs +1123 -98
  2. package/dist/types.ts +10 -1
  3. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -34716,7 +34716,7 @@ var require_stringify = __commonJS({
34716
34716
  props.push(doc.directives.tagString(tag));
34717
34717
  return props.join(" ");
34718
34718
  }
34719
- function stringify2(item, ctx, onComment, onChompKeep) {
34719
+ function stringify3(item, ctx, onComment, onChompKeep) {
34720
34720
  if (identity.isPair(item))
34721
34721
  return item.toString(ctx, onComment, onChompKeep);
34722
34722
  if (identity.isAlias(item)) {
@@ -34745,7 +34745,7 @@ var require_stringify = __commonJS({
34745
34745
  ${ctx.indent}${str}`;
34746
34746
  }
34747
34747
  exports2.createStringifyContext = createStringifyContext;
34748
- exports2.stringify = stringify2;
34748
+ exports2.stringify = stringify3;
34749
34749
  }
34750
34750
  });
34751
34751
 
@@ -34755,7 +34755,7 @@ var require_stringifyPair = __commonJS({
34755
34755
  "use strict";
34756
34756
  var identity = require_identity();
34757
34757
  var Scalar = require_Scalar();
34758
- var stringify2 = require_stringify();
34758
+ var stringify3 = require_stringify();
34759
34759
  var stringifyComment = require_stringifyComment();
34760
34760
  function stringifyPair({ key, value }, ctx, onComment, onChompKeep) {
34761
34761
  const { allNullValues, doc, indent, indentStep, options: { commentString, indentSeq, simpleKeys } } = ctx;
@@ -34777,7 +34777,7 @@ var require_stringifyPair = __commonJS({
34777
34777
  });
34778
34778
  let keyCommentDone = false;
34779
34779
  let chompKeep = false;
34780
- let str = stringify2.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
34780
+ let str = stringify3.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
34781
34781
  if (!explicitKey && !ctx.inFlow && str.length > 1024) {
34782
34782
  if (simpleKeys)
34783
34783
  throw new Error("With simple keys, single line scalar must not span more than 1024 characters");
@@ -34829,7 +34829,7 @@ ${indent}:`;
34829
34829
  ctx.indent = ctx.indent.substring(2);
34830
34830
  }
34831
34831
  let valueCommentDone = false;
34832
- const valueStr = stringify2.stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true);
34832
+ const valueStr = stringify3.stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true);
34833
34833
  let ws = " ";
34834
34834
  if (keyComment || vsb || vcb) {
34835
34835
  ws = vsb ? "\n" : "";
@@ -34967,7 +34967,7 @@ var require_addPairToJSMap = __commonJS({
34967
34967
  "use strict";
34968
34968
  var log = require_log();
34969
34969
  var merge2 = require_merge();
34970
- var stringify2 = require_stringify();
34970
+ var stringify3 = require_stringify();
34971
34971
  var identity = require_identity();
34972
34972
  var toJS = require_toJS();
34973
34973
  function addPairToJSMap(ctx, map2, { key, value }) {
@@ -35003,7 +35003,7 @@ var require_addPairToJSMap = __commonJS({
35003
35003
  if (typeof jsKey !== "object")
35004
35004
  return String(jsKey);
35005
35005
  if (identity.isNode(key) && ctx?.doc) {
35006
- const strCtx = stringify2.createStringifyContext(ctx.doc, {});
35006
+ const strCtx = stringify3.createStringifyContext(ctx.doc, {});
35007
35007
  strCtx.anchors = /* @__PURE__ */ new Set();
35008
35008
  for (const node of ctx.anchors.keys())
35009
35009
  strCtx.anchors.add(node.anchor);
@@ -35070,12 +35070,12 @@ var require_stringifyCollection = __commonJS({
35070
35070
  "../../node_modules/yaml/dist/stringify/stringifyCollection.js"(exports2) {
35071
35071
  "use strict";
35072
35072
  var identity = require_identity();
35073
- var stringify2 = require_stringify();
35073
+ var stringify3 = require_stringify();
35074
35074
  var stringifyComment = require_stringifyComment();
35075
35075
  function stringifyCollection(collection, ctx, options) {
35076
35076
  const flow = ctx.inFlow ?? collection.flow;
35077
- const stringify3 = flow ? stringifyFlowCollection : stringifyBlockCollection;
35078
- return stringify3(collection, ctx, options);
35077
+ const stringify4 = flow ? stringifyFlowCollection : stringifyBlockCollection;
35078
+ return stringify4(collection, ctx, options);
35079
35079
  }
35080
35080
  function stringifyBlockCollection({ comment, items }, ctx, { blockItemPrefix, flowChars, itemIndent, onChompKeep, onComment }) {
35081
35081
  const { indent, options: { commentString } } = ctx;
@@ -35100,7 +35100,7 @@ var require_stringifyCollection = __commonJS({
35100
35100
  }
35101
35101
  }
35102
35102
  chompKeep = false;
35103
- let str2 = stringify2.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true);
35103
+ let str2 = stringify3.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true);
35104
35104
  if (comment2)
35105
35105
  str2 += stringifyComment.lineComment(str2, itemIndent, commentString(comment2));
35106
35106
  if (chompKeep && comment2)
@@ -35167,7 +35167,7 @@ ${indent}${line}` : "\n";
35167
35167
  }
35168
35168
  if (comment)
35169
35169
  reqNewline = true;
35170
- let str = stringify2.stringify(item, itemCtx, () => comment = null);
35170
+ let str = stringify3.stringify(item, itemCtx, () => comment = null);
35171
35171
  if (i < items.length - 1)
35172
35172
  str += ",";
35173
35173
  if (comment)
@@ -36521,7 +36521,7 @@ var require_stringifyDocument = __commonJS({
36521
36521
  "../../node_modules/yaml/dist/stringify/stringifyDocument.js"(exports2) {
36522
36522
  "use strict";
36523
36523
  var identity = require_identity();
36524
- var stringify2 = require_stringify();
36524
+ var stringify3 = require_stringify();
36525
36525
  var stringifyComment = require_stringifyComment();
36526
36526
  function stringifyDocument(doc, options) {
36527
36527
  const lines = [];
@@ -36536,7 +36536,7 @@ var require_stringifyDocument = __commonJS({
36536
36536
  }
36537
36537
  if (hasDirectives)
36538
36538
  lines.push("---");
36539
- const ctx = stringify2.createStringifyContext(doc, options);
36539
+ const ctx = stringify3.createStringifyContext(doc, options);
36540
36540
  const { commentString } = ctx.options;
36541
36541
  if (doc.commentBefore) {
36542
36542
  if (lines.length !== 1)
@@ -36558,7 +36558,7 @@ var require_stringifyDocument = __commonJS({
36558
36558
  contentComment = doc.contents.comment;
36559
36559
  }
36560
36560
  const onChompKeep = contentComment ? void 0 : () => chompKeep = true;
36561
- let body = stringify2.stringify(doc.contents, ctx, () => contentComment = null, onChompKeep);
36561
+ let body = stringify3.stringify(doc.contents, ctx, () => contentComment = null, onChompKeep);
36562
36562
  if (contentComment)
36563
36563
  body += stringifyComment.lineComment(body, "", commentString(contentComment));
36564
36564
  if ((body[0] === "|" || body[0] === ">") && lines[lines.length - 1] === "---") {
@@ -36566,7 +36566,7 @@ var require_stringifyDocument = __commonJS({
36566
36566
  } else
36567
36567
  lines.push(body);
36568
36568
  } else {
36569
- lines.push(stringify2.stringify(doc.contents, ctx));
36569
+ lines.push(stringify3.stringify(doc.contents, ctx));
36570
36570
  }
36571
36571
  if (doc.directives?.docEnd) {
36572
36572
  if (doc.comment) {
@@ -38693,7 +38693,7 @@ var require_cst_scalar = __commonJS({
38693
38693
  var require_cst_stringify = __commonJS({
38694
38694
  "../../node_modules/yaml/dist/parse/cst-stringify.js"(exports2) {
38695
38695
  "use strict";
38696
- var stringify2 = (cst) => "type" in cst ? stringifyToken(cst) : stringifyItem(cst);
38696
+ var stringify3 = (cst) => "type" in cst ? stringifyToken(cst) : stringifyItem(cst);
38697
38697
  function stringifyToken(token) {
38698
38698
  switch (token.type) {
38699
38699
  case "block-scalar": {
@@ -38746,7 +38746,7 @@ var require_cst_stringify = __commonJS({
38746
38746
  res += stringifyToken(value);
38747
38747
  return res;
38748
38748
  }
38749
- exports2.stringify = stringify2;
38749
+ exports2.stringify = stringify3;
38750
38750
  }
38751
38751
  });
38752
38752
 
@@ -40459,7 +40459,7 @@ var require_public_api = __commonJS({
40459
40459
  }
40460
40460
  return doc.toJS(Object.assign({ reviver: _reviver }, options));
40461
40461
  }
40462
- function stringify2(value, replacer, options) {
40462
+ function stringify3(value, replacer, options) {
40463
40463
  let _replacer = null;
40464
40464
  if (typeof replacer === "function" || Array.isArray(replacer)) {
40465
40465
  _replacer = replacer;
@@ -40484,7 +40484,7 @@ var require_public_api = __commonJS({
40484
40484
  exports2.parse = parse4;
40485
40485
  exports2.parseAllDocuments = parseAllDocuments;
40486
40486
  exports2.parseDocument = parseDocument;
40487
- exports2.stringify = stringify2;
40487
+ exports2.stringify = stringify3;
40488
40488
  }
40489
40489
  });
40490
40490
 
@@ -41204,8 +41204,79 @@ function prepareChartData(rows, chart) {
41204
41204
  for (const transform2 of chart.data?.transform ?? []) {
41205
41205
  current = applyTransform(current, transform2);
41206
41206
  }
41207
+ current = applyEncodingAggregates(current, chart);
41207
41208
  return current;
41208
41209
  }
41210
+ function inspectChartTransforms(rows, chart) {
41211
+ let current = [...rows];
41212
+ const transforms = [];
41213
+ let step = 1;
41214
+ for (const transform2 of chart.data?.transform ?? []) {
41215
+ const inputRows = current.length;
41216
+ current = applyTransform(current, transform2);
41217
+ transforms.push({
41218
+ step: step++,
41219
+ type: transform2.type,
41220
+ inputRows,
41221
+ outputRows: current.length,
41222
+ preview: current.slice(0, 3)
41223
+ });
41224
+ }
41225
+ const beforeEncoding = current;
41226
+ current = applyEncodingAggregates(current, chart);
41227
+ if (current !== beforeEncoding) {
41228
+ transforms.push({
41229
+ step,
41230
+ type: "encoding-aggregate",
41231
+ inputRows: beforeEncoding.length,
41232
+ outputRows: current.length,
41233
+ preview: current.slice(0, 3)
41234
+ });
41235
+ }
41236
+ return { rows: current, transforms };
41237
+ }
41238
+ function applyEncodingAggregates(rows, chart) {
41239
+ const enc = chart.encoding;
41240
+ if (!enc) return rows;
41241
+ if (chart.type === "bigvalue") {
41242
+ const valueEnc = enc.value;
41243
+ if (valueEnc?.field && valueEnc.aggregate) {
41244
+ const result = aggregateMeasure(rows, valueEnc.field, valueEnc.aggregate);
41245
+ return [{ [valueEnc.field]: result }];
41246
+ }
41247
+ return rows;
41248
+ }
41249
+ if (chart.type === "bar" || chart.type === "line" || chart.type === "area") {
41250
+ const xField = enc.x?.field;
41251
+ const yEnc = enc.y;
41252
+ if (xField && yEnc?.field && yEnc.aggregate) {
41253
+ return groupByAndAggregate(rows, xField, yEnc.field, yEnc.aggregate);
41254
+ }
41255
+ return rows;
41256
+ }
41257
+ if (chart.type === "pie") {
41258
+ const labelField = enc.label?.field;
41259
+ const valueEnc = enc.value;
41260
+ if (labelField && valueEnc?.field && valueEnc.aggregate) {
41261
+ return groupByAndAggregate(rows, labelField, valueEnc.field, valueEnc.aggregate);
41262
+ }
41263
+ return rows;
41264
+ }
41265
+ return rows;
41266
+ }
41267
+ function groupByAndAggregate(rows, groupField, valueField, op) {
41268
+ const groups = /* @__PURE__ */ new Map();
41269
+ for (const row of rows) {
41270
+ const key = row[groupField];
41271
+ const group = groups.get(key) ?? [];
41272
+ group.push(row);
41273
+ groups.set(key, group);
41274
+ }
41275
+ return [...groups.entries()].map(([key, groupRows]) => ({
41276
+ [groupField]: key,
41277
+ [valueField]: aggregateMeasure(groupRows, valueField, op)
41278
+ }));
41279
+ }
41209
41280
  function applyTransform(rows, transform2) {
41210
41281
  if (transform2.type === "derive-month" && transform2.field && transform2.as) {
41211
41282
  return rows.map((row) => ({
@@ -41292,11 +41363,14 @@ function renderChartSvg(chart, rows, svgTheme, options = {}) {
41292
41363
  if (chart.type === "pie") return renderPieChart(chart, data, theme, options);
41293
41364
  if (chart.type === "table") return renderTable(chart, data, options);
41294
41365
  if (chart.type === "bigvalue") return renderBigValue(chart, data);
41366
+ if (chart.type === "histogram") return renderHistogramChart(chart, data, theme, options);
41367
+ if (chart.type === "scatter") return renderScatterChart(chart, data, theme, options);
41368
+ if (chart.type === "heatmap") return renderHeatmapChart(chart, data, theme, options);
41295
41369
  return renderUnsupported(chart);
41296
41370
  }
41297
41371
  function renderBarChart(chart, rows, theme, options) {
41298
- const xField = chart.encoding.x?.field ?? "";
41299
- const yField = chart.encoding.y?.field ?? "";
41372
+ const xField = chart.encoding?.x?.field ?? "";
41373
+ const yField = chart.encoding?.y?.field ?? "";
41300
41374
  const width = numberStyle(chart, "width", 720);
41301
41375
  const height = numberStyle(chart, "height", 420);
41302
41376
  const margin = { top: 24, right: 24, bottom: 48, left: 72 };
@@ -41326,8 +41400,8 @@ function renderBarChart(chart, rows, theme, options) {
41326
41400
  `);
41327
41401
  }
41328
41402
  function renderLineChart(chart, rows, theme, options) {
41329
- const xField = chart.encoding.x?.field ?? "";
41330
- const yField = chart.encoding.y?.field ?? "";
41403
+ const xField = chart.encoding?.x?.field ?? "";
41404
+ const yField = chart.encoding?.y?.field ?? "";
41331
41405
  const width = numberStyle(chart, "width", 720);
41332
41406
  const height = numberStyle(chart, "height", 420);
41333
41407
  const margin = { top: 24, right: 24, bottom: 64, left: 72 };
@@ -41359,8 +41433,8 @@ function renderLineChart(chart, rows, theme, options) {
41359
41433
  `);
41360
41434
  }
41361
41435
  function renderPieChart(chart, rows, theme, options) {
41362
- const labelField = chart.encoding.label?.field ?? "";
41363
- const valueField = chart.encoding.value?.field ?? "";
41436
+ const labelField = chart.encoding?.label?.field ?? "";
41437
+ const valueField = chart.encoding?.value?.field ?? "";
41364
41438
  const width = numberStyle(chart, "width", 720);
41365
41439
  const height = numberStyle(chart, "height", 420);
41366
41440
  const cx = width / 2 - 80;
@@ -41389,19 +41463,182 @@ function renderPieChart(chart, rows, theme, options) {
41389
41463
  return svgFrame(width, height, theme.background, `${slices}${legend}`);
41390
41464
  }
41391
41465
  function renderTable(chart, rows, options) {
41392
- const columns = Object.keys(rows[0] ?? {}).slice(0, 8);
41466
+ const columns = Object.keys(rows[0] ?? {});
41393
41467
  const header = columns.map((c) => `<th>${escapeHtml(c)}</th>`).join("");
41394
41468
  const markField = chart.encoding?.label?.field ?? chart.encoding?.x?.field ?? columns[0] ?? "";
41395
41469
  const body = rows.slice(0, 20).map(
41396
- (row) => `<tr ${markAttrs(options.chartId, markField, row[markField], 0, String(row[markField] ?? "Row"))}>${columns.map((c) => `<td>${escapeHtml(String(row[c] ?? ""))}</td>`).join("")}</tr>`
41470
+ (row, i) => `<tr ${markAttrs(options.chartId, markField, row[markField], i, String(row[markField] ?? "Row"))}>${columns.map((c) => `<td>${escapeHtml(String(row[c] ?? ""))}</td>`).join("")}</tr>`
41397
41471
  ).join("");
41398
41472
  return `<div class="miao-table-wrap"><table class="miao-table"><caption>${escapeHtml(chart.title ?? "Table")}</caption><thead><tr>${header}</tr></thead><tbody>${body}</tbody></table></div>`;
41399
41473
  }
41400
41474
  function renderBigValue(chart, rows) {
41401
- const valueField = chart.encoding.value?.field ?? "";
41475
+ const valueField = chart.encoding?.value?.field ?? "";
41402
41476
  const value = rows[0]?.[valueField] ?? "";
41403
41477
  return `<div class="miao-bigvalue"><div class="miao-bigvalue-label">${escapeHtml(chart.title ?? valueField)}</div><div class="miao-bigvalue-number">${escapeHtml(String(value))}</div></div>`;
41404
41478
  }
41479
+ function renderHistogramChart(chart, rows, theme, options) {
41480
+ const xField = chart.encoding?.x?.field ?? "";
41481
+ const width = numberStyle(chart, "width", 720);
41482
+ const height = numberStyle(chart, "height", 420);
41483
+ const margin = { top: 24, right: 24, bottom: 56, left: 72 };
41484
+ const chartWidth = width - margin.left - margin.right;
41485
+ const chartHeight = height - margin.top - margin.bottom;
41486
+ const values = rows.map((row) => Number(row[xField])).filter(Number.isFinite);
41487
+ if (values.length === 0) return renderUnsupported(chart);
41488
+ const bucketCount = 8;
41489
+ const xMin = Math.min(...values);
41490
+ const xMax = Math.max(...values);
41491
+ const bucketSpan = xMax - xMin || 1;
41492
+ const bucketWidth = bucketSpan / bucketCount;
41493
+ const counts = Array(bucketCount).fill(0);
41494
+ for (const v of values) {
41495
+ counts[Math.min(Math.floor((v - xMin) / bucketWidth), bucketCount - 1)]++;
41496
+ }
41497
+ const yMax = Math.max(...counts, 1);
41498
+ const barGap = 2;
41499
+ const barW = (chartWidth - barGap * (bucketCount - 1)) / bucketCount;
41500
+ const color = theme.palette[0];
41501
+ const x0 = margin.left;
41502
+ const y0 = margin.top + chartHeight;
41503
+ const bars = counts.map((count, i) => {
41504
+ const barH = count / yMax * chartHeight;
41505
+ const x = margin.left + i * (barW + barGap);
41506
+ const y = margin.top + chartHeight - barH;
41507
+ const lo = xMin + i * bucketWidth;
41508
+ const tooltip = `${formatTick(lo)}\u2013${formatTick(lo + bucketWidth)}: ${count}`;
41509
+ return `<rect ${markAttrs(options.chartId, xField, lo, i, tooltip)} x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${barW.toFixed(1)}" height="${barH.toFixed(1)}" fill="${color}" />`;
41510
+ }).join("");
41511
+ const xTickPositions = [0, 2, 4, 6, 8];
41512
+ const xTicks = xTickPositions.map((i) => {
41513
+ const val = xMin + i * bucketWidth;
41514
+ const x = margin.left + i * (barW + barGap);
41515
+ return `<text x="${x.toFixed(1)}" y="${(y0 + 18).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="10">${escapeHtml(formatTick(val))}</text>`;
41516
+ }).join("");
41517
+ const tickCount = 4;
41518
+ const yTicks = Array.from({ length: tickCount + 1 }, (_, i) => ({
41519
+ count: Math.round(i / tickCount * yMax),
41520
+ y: margin.top + chartHeight - i / tickCount * chartHeight
41521
+ }));
41522
+ const gridLines = yTicks.filter((_, i) => i > 0).map(
41523
+ (t) => `<line x1="${x0}" y1="${t.y.toFixed(1)}" x2="${x0 + chartWidth}" y2="${t.y.toFixed(1)}" stroke="${theme.axisColor}" stroke-opacity="0.4" stroke-dasharray="4 3" />`
41524
+ ).join("");
41525
+ const yTickLabels = yTicks.map(
41526
+ (t) => `<text x="${(x0 - 6).toFixed(1)}" y="${(t.y + 4).toFixed(1)}" text-anchor="end" fill="${theme.labelColor}" font-size="11">${t.count}</text>`
41527
+ ).join("");
41528
+ return svgFrame(width, height, theme.background, `
41529
+ ${gridLines}
41530
+ <line x1="${x0}" y1="${y0}" x2="${(x0 + chartWidth).toFixed(1)}" y2="${y0}" stroke="${theme.axisColor}" />
41531
+ <line x1="${x0}" y1="${margin.top}" x2="${x0}" y2="${y0}" stroke="${theme.axisColor}" />
41532
+ ${yTickLabels}
41533
+ <text x="${(x0 + chartWidth / 2).toFixed(1)}" y="${(y0 + 50).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="12">${escapeHtml(xField)}</text>
41534
+ <text x="14" y="${(margin.top + chartHeight / 2).toFixed(1)}" text-anchor="middle" transform="rotate(-90 14 ${(margin.top + chartHeight / 2).toFixed(1)})" fill="${theme.labelColor}" font-size="12">count</text>
41535
+ ${bars}
41536
+ ${xTicks}
41537
+ `);
41538
+ }
41539
+ function renderScatterChart(chart, rows, theme, options) {
41540
+ const xField = chart.encoding?.x?.field ?? "";
41541
+ const yField = chart.encoding?.y?.field ?? "";
41542
+ const labelField = chart.encoding?.label?.field ?? "";
41543
+ const width = numberStyle(chart, "width", 720);
41544
+ const height = numberStyle(chart, "height", 420);
41545
+ const margin = { top: 24, right: 24, bottom: 56, left: 72 };
41546
+ const chartWidth = width - margin.left - margin.right;
41547
+ const chartHeight = height - margin.top - margin.bottom;
41548
+ const MAX_POINTS = 400;
41549
+ const sample = rows.length > MAX_POINTS ? rows.filter((_, i) => i % Math.ceil(rows.length / MAX_POINTS) === 0) : rows;
41550
+ const xValues = sample.map((row) => Number(row[xField])).filter(Number.isFinite);
41551
+ const yValues = sample.map((row) => Number(row[yField])).filter(Number.isFinite);
41552
+ if (xValues.length === 0 || yValues.length === 0) return renderUnsupported(chart);
41553
+ const xMin = Math.min(...xValues);
41554
+ const xMax = Math.max(...xValues);
41555
+ const yMin = Math.min(...yValues);
41556
+ const yMax = Math.max(...yValues);
41557
+ const xSpan = xMax - xMin || 1;
41558
+ const ySpan = yMax - yMin || 1;
41559
+ const color = theme.palette[0];
41560
+ const x0 = margin.left;
41561
+ const y0 = margin.top + chartHeight;
41562
+ const dots = sample.map((row, i) => {
41563
+ const xVal = Number(row[xField]);
41564
+ const yVal = Number(row[yField]);
41565
+ if (!Number.isFinite(xVal) || !Number.isFinite(yVal)) return "";
41566
+ const cx = margin.left + (xVal - xMin) / xSpan * chartWidth;
41567
+ const cy = margin.top + chartHeight - (yVal - yMin) / ySpan * chartHeight;
41568
+ const label = labelField ? String(row[labelField] ?? "") : `${formatTick(xVal)}, ${formatTick(yVal)}`;
41569
+ const tooltip = labelField ? `${label}: (${formatTick(xVal)}, ${formatTick(yVal)})` : `(${formatTick(xVal)}, ${formatTick(yVal)})`;
41570
+ return `<circle ${markAttrs(options.chartId, xField, xVal, i, tooltip)} cx="${cx.toFixed(1)}" cy="${cy.toFixed(1)}" r="4" fill="${color}" fill-opacity="0.55" />`;
41571
+ }).join("");
41572
+ const sampledNote = rows.length > MAX_POINTS ? `<text x="${(x0 + chartWidth).toFixed(1)}" y="${margin.top - 6}" text-anchor="end" fill="${theme.labelColor}" font-size="10">sampled ${MAX_POINTS} of ${rows.length} rows</text>` : "";
41573
+ const xTickCount = 5;
41574
+ const xTicks = Array.from({ length: xTickCount }, (_, i) => {
41575
+ const val = xMin + i / (xTickCount - 1) * xSpan;
41576
+ const x = margin.left + i / (xTickCount - 1) * chartWidth;
41577
+ return `<text x="${x.toFixed(1)}" y="${(y0 + 18).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="10">${escapeHtml(formatTick(val))}</text>`;
41578
+ }).join("");
41579
+ return svgFrame(width, height, theme.background, `
41580
+ ${buildAxis(margin, chartWidth, chartHeight, xField, yField, yMin, yMax, theme)}
41581
+ ${dots}
41582
+ ${xTicks}
41583
+ ${sampledNote}
41584
+ `);
41585
+ }
41586
+ function renderHeatmapChart(chart, rows, theme, options) {
41587
+ const xField = chart.encoding?.x?.field ?? "";
41588
+ const yField = chart.encoding?.y?.field ?? "";
41589
+ const valueField = chart.encoding?.value?.field ?? "";
41590
+ const width = numberStyle(chart, "width", 720);
41591
+ const height = numberStyle(chart, "height", 420);
41592
+ const margin = { top: 24, right: 24, bottom: 64, left: 80 };
41593
+ const chartWidth = width - margin.left - margin.right;
41594
+ const chartHeight = height - margin.top - margin.bottom;
41595
+ const xValues = [...new Set(rows.map((row) => String(row[xField] ?? "")))];
41596
+ const yValues = [...new Set(rows.map((row) => String(row[yField] ?? "")))];
41597
+ if (xValues.length === 0 || yValues.length === 0) return renderUnsupported(chart);
41598
+ const cellMap = /* @__PURE__ */ new Map();
41599
+ for (const row of rows) {
41600
+ cellMap.set(`${row[xField]}|${row[yField]}`, Number(row[valueField]) || 0);
41601
+ }
41602
+ const allValues = [...cellMap.values()];
41603
+ const minVal = Math.min(...allValues);
41604
+ const maxVal = Math.max(...allValues, minVal + 1);
41605
+ const span = maxVal - minVal;
41606
+ const cellW = chartWidth / xValues.length;
41607
+ const cellH = chartHeight / yValues.length;
41608
+ const color = theme.palette[0];
41609
+ const x0 = margin.left;
41610
+ const y0 = margin.top + chartHeight;
41611
+ const cells = yValues.flatMap(
41612
+ (yVal, yi) => xValues.map((xVal, xi) => {
41613
+ const val = cellMap.get(`${xVal}|${yVal}`) ?? 0;
41614
+ const opacity = (0.1 + (val - minVal) / span * 0.85).toFixed(2);
41615
+ const x = margin.left + xi * cellW;
41616
+ const y = margin.top + yi * cellH;
41617
+ const tooltip = `${xVal}, ${yVal}: ${formatTick(val)}`;
41618
+ return `<rect ${markAttrs(options.chartId, xField, xVal, xi + yi * xValues.length, tooltip)} x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${cellW.toFixed(1)}" height="${cellH.toFixed(1)}" fill="${color}" fill-opacity="${opacity}" stroke="${theme.background}" stroke-width="1" />`;
41619
+ })
41620
+ ).join("");
41621
+ const MAX_LABELS = 12;
41622
+ const xStep = Math.max(1, Math.ceil(xValues.length / MAX_LABELS));
41623
+ const xLabels = xValues.filter((_, i) => i % xStep === 0).map((val, i) => {
41624
+ const x = margin.left + (i * xStep + 0.5) * cellW;
41625
+ return `<text x="${x.toFixed(1)}" y="${(y0 + 16).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="10">${escapeHtml(val)}</text>`;
41626
+ }).join("");
41627
+ const yStep = Math.max(1, Math.ceil(yValues.length / MAX_LABELS));
41628
+ const yLabels = yValues.filter((_, i) => i % yStep === 0).map((val, i) => {
41629
+ const y = margin.top + (i * yStep + 0.5) * cellH;
41630
+ return `<text x="${(x0 - 6).toFixed(1)}" y="${(y + 4).toFixed(1)}" text-anchor="end" fill="${theme.labelColor}" font-size="10">${escapeHtml(val)}</text>`;
41631
+ }).join("");
41632
+ return svgFrame(width, height, theme.background, `
41633
+ <line x1="${x0}" y1="${y0}" x2="${(x0 + chartWidth).toFixed(1)}" y2="${y0}" stroke="${theme.axisColor}" />
41634
+ <line x1="${x0}" y1="${margin.top}" x2="${x0}" y2="${y0}" stroke="${theme.axisColor}" />
41635
+ ${cells}
41636
+ ${xLabels}
41637
+ ${yLabels}
41638
+ <text x="${(x0 + chartWidth / 2).toFixed(1)}" y="${(y0 + 52).toFixed(1)}" text-anchor="middle" fill="${theme.labelColor}" font-size="12">${escapeHtml(xField)}</text>
41639
+ <text x="14" y="${(margin.top + chartHeight / 2).toFixed(1)}" text-anchor="middle" transform="rotate(-90 14 ${(margin.top + chartHeight / 2).toFixed(1)})" fill="${theme.labelColor}" font-size="12">${escapeHtml(yField)}</text>
41640
+ `);
41641
+ }
41405
41642
  function renderUnsupported(chart) {
41406
41643
  return `<div class="miao-unsupported">Static HTML rendering for ${escapeHtml(chart.type)} is not implemented yet.</div>`;
41407
41644
  }
@@ -42117,12 +42354,38 @@ function escapeScriptJson(value) {
42117
42354
  return JSON.stringify(value).replace(/</g, "\\u003c");
42118
42355
  }
42119
42356
 
42357
+ // src/insight-utils.ts
42358
+ function normalizeInsight(insight) {
42359
+ if (typeof insight === "string") {
42360
+ return { text: insight, evidence: [], original: insight };
42361
+ }
42362
+ return {
42363
+ text: insight.text,
42364
+ evidence: insight.evidence ?? [],
42365
+ caveat: insight.caveat,
42366
+ severity: insight.severity,
42367
+ original: insight
42368
+ };
42369
+ }
42370
+ function normalizeInsights(insights) {
42371
+ return (insights ?? []).map(normalizeInsight);
42372
+ }
42373
+ function mapInsightText(insight, mapText) {
42374
+ if (typeof insight === "string") return mapText(insight);
42375
+ return { ...insight, text: mapText(insight.text) };
42376
+ }
42377
+ function insightPreview(text) {
42378
+ return `"${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"`;
42379
+ }
42380
+
42120
42381
  // src/html-export.ts
42121
42382
  var INSIGHTS_CSS = `
42122
42383
  .report-insights { margin: 0 0 32px; padding: 16px 20px 14px; border-radius: 4px; border: 1px solid rgba(128,128,128,0.18); background: rgba(128,128,128,0.04); }
42123
42384
  .insights-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.45; margin: 0 0 8px; }
42124
42385
  .insights-list { margin: 0; padding: 0 0 0 18px; }
42125
42386
  .insights-list li { margin: 5px 0; font-size: 13px; line-height: 1.55; opacity: 0.75; }
42387
+ .insight-warning { color: #8a4b00; }
42388
+ .insight-caveat { display: block; margin-top: 2px; font-size: 11px; opacity: 0.58; }
42126
42389
  `;
42127
42390
  function renderStaticHtml(spec, profile, rows, themeOverride, interactiveOptions = {}) {
42128
42391
  const theme = getTheme(themeOverride ?? spec.theme);
@@ -42229,7 +42492,11 @@ function chartIdFor(chart, index) {
42229
42492
  return chart.id ?? `chart-${index + 1}`;
42230
42493
  }
42231
42494
  function renderInsights(insights) {
42232
- const items = insights.map((s) => `<li>${escapeHtml(s)}</li>`).join("\n ");
42495
+ const items = normalizeInsights(insights).map((insight) => {
42496
+ const className = insight.severity === "warning" ? ' class="insight-warning"' : "";
42497
+ const caveat = insight.caveat ? `<span class="insight-caveat">${escapeHtml(insight.caveat)}</span>` : "";
42498
+ return `<li${className}>${escapeHtml(insight.text)}${caveat}</li>`;
42499
+ }).join("\n ");
42233
42500
  return `<section class="report-insights">
42234
42501
  <p class="insights-label">Key Observations</p>
42235
42502
  <ul class="insights-list">
@@ -42239,9 +42506,9 @@ function renderInsights(insights) {
42239
42506
  }
42240
42507
  function buildCaption(chart) {
42241
42508
  const parts = [];
42242
- if (chart.encoding.x?.field) parts.push(`x: ${chart.encoding.x.field}`);
42243
- if (chart.encoding.y?.field) parts.push(`y: ${chart.encoding.y.field}`);
42244
- if (chart.encoding.value?.field) parts.push(`value: ${chart.encoding.value.field}`);
42509
+ if (chart.encoding?.x?.field) parts.push(`x: ${chart.encoding.x.field}`);
42510
+ if (chart.encoding?.y?.field) parts.push(`y: ${chart.encoding.y.field}`);
42511
+ if (chart.encoding?.value?.field) parts.push(`value: ${chart.encoding.value.field}`);
42245
42512
  const transforms = chart.data?.transform ?? [];
42246
42513
  const agg = transforms.find((t) => t.type === "aggregate");
42247
42514
  if (agg?.groupBy?.length) parts.push(`grouped by ${agg.groupBy.join(", ")}`);
@@ -56806,6 +57073,15 @@ var chartInteractionSchema = external_exports.object({
56806
57073
  tooltip: external_exports.boolean().optional(),
56807
57074
  select: external_exports.enum(["filter", "detail"]).optional()
56808
57075
  });
57076
+ var insightSchema = external_exports.union([
57077
+ external_exports.string(),
57078
+ external_exports.object({
57079
+ text: external_exports.string().min(1),
57080
+ evidence: external_exports.array(external_exports.string().min(1)).optional(),
57081
+ caveat: external_exports.string().optional(),
57082
+ severity: external_exports.enum(["info", "warning"]).optional()
57083
+ })
57084
+ ]);
56809
57085
  var chartSpecSchema = external_exports.object({
56810
57086
  id: external_exports.string().min(1).optional(),
56811
57087
  type: external_exports.enum(MVP_CHART_TYPES),
@@ -56833,7 +57109,7 @@ var reportSpecSchema = external_exports.object({
56833
57109
  interactions: external_exports.object({
56834
57110
  globalFilters: external_exports.array(globalFilterSchema).optional()
56835
57111
  }).optional(),
56836
- insights: external_exports.array(external_exports.string()).optional(),
57112
+ insights: external_exports.array(insightSchema).optional(),
56837
57113
  charts: external_exports.array(chartSpecSchema).min(1)
56838
57114
  });
56839
57115
  var singleOrReportSpecSchema = external_exports.union([
@@ -56889,12 +57165,23 @@ function resolveEvidencePath(evidence, id, path) {
56889
57165
  }
56890
57166
  return { found: false, value: void 0 };
56891
57167
  }
57168
+ function resolveDirectives(text, evidence) {
57169
+ return text.replace(new RegExp(EVIDENCE_RE.source, "g"), (_, id, path) => {
57170
+ const { found, value } = resolveEvidencePath(evidence, id, path);
57171
+ return found ? String(value) : `[?${id}.${path}]`;
57172
+ });
57173
+ }
56892
57174
 
56893
57175
  // src/chart-catalog.ts
56894
57176
  var CHART_CATALOG = [
56895
57177
  {
56896
57178
  id: "bar",
56897
57179
  displayName: "Bar Chart",
57180
+ compactFor: "rank,compare",
57181
+ requires: "dim(2-30)+measure",
57182
+ transformRecipe: "agg>sort(desc)>limit(10)",
57183
+ avoid: "dim>30,time>=3",
57184
+ insightPattern: "top {dimension} by {measure}",
56898
57185
  requiredEncodings: ["x", "y"],
56899
57186
  allowedTransforms: ["aggregate", "sort", "limit", "derive-month"],
56900
57187
  bestFor: ["ranking by category", "comparison across dimensions", "top-N with limit"],
@@ -56917,6 +57204,25 @@ var CHART_CATALOG = [
56917
57204
  return null;
56918
57205
  }
56919
57206
  },
57207
+ {
57208
+ code: "BAR_NO_AGGREGATE",
57209
+ severity: "warning",
57210
+ expression: "encoding.y.aggregate not set and no aggregate transform",
57211
+ message: "bar will plot one bar per raw row \u2014 unsorted and unaggregated. Add encoding.y.aggregate or data.transform aggregate + sort + limit.",
57212
+ validate: (chart) => {
57213
+ const hasAggTransform = (chart.data?.transform ?? []).some((t) => t.type === "aggregate");
57214
+ const hasEncodingAgg = !!chart.encoding?.y?.aggregate;
57215
+ if (!hasAggTransform && !hasEncodingAgg) {
57216
+ return {
57217
+ code: "BAR_NO_AGGREGATE",
57218
+ severity: "warning",
57219
+ message: `bar${chart.id ? ` '${chart.id}'` : ""}: no aggregation \u2014 will plot one bar per raw row (unsorted, unaggregated). Add encoding.y.aggregate (sum/avg/count) or data.transform: aggregate + sort + limit.`,
57220
+ chartId: chart.id
57221
+ };
57222
+ }
57223
+ return null;
57224
+ }
57225
+ },
56920
57226
  {
56921
57227
  code: "TOO_MANY_CATEGORIES",
56922
57228
  severity: "warning",
@@ -56943,6 +57249,11 @@ var CHART_CATALOG = [
56943
57249
  {
56944
57250
  id: "line",
56945
57251
  displayName: "Line Chart",
57252
+ compactFor: "trend",
57253
+ requires: "time(>=3)+measure",
57254
+ transformRecipe: "agg(time)>sort(asc)",
57255
+ avoid: "time<3,nominal_x",
57256
+ insightPattern: "{measure} over {time}",
56946
57257
  requiredEncodings: ["x", "y"],
56947
57258
  allowedTransforms: ["aggregate", "sort", "derive-month"],
56948
57259
  bestFor: ["time series trends", "continuous data over ordered axis"],
@@ -56992,6 +57303,10 @@ var CHART_CATALOG = [
56992
57303
  {
56993
57304
  id: "area",
56994
57305
  displayName: "Area Chart",
57306
+ compactFor: "trend,magnitude",
57307
+ requires: "time(>=3)+measure",
57308
+ transformRecipe: "agg(time)>sort(asc)",
57309
+ avoid: "time<3,negative_values,nominal_x",
56995
57310
  requiredEncodings: ["x", "y"],
56996
57311
  allowedTransforms: ["aggregate", "sort", "derive-month"],
56997
57312
  bestFor: ["cumulative trends", "filled time series with visual mass"],
@@ -57041,11 +57356,35 @@ var CHART_CATALOG = [
57041
57356
  {
57042
57357
  id: "pie",
57043
57358
  displayName: "Pie Chart",
57359
+ compactFor: "share,composition",
57360
+ requires: "dim(2-7)+measure",
57361
+ transformRecipe: "agg>sort(desc)>limit(7)",
57362
+ avoid: "dim>7,non_whole_values",
57363
+ insightPattern: "{dimension} share of {measure}",
57044
57364
  requiredEncodings: ["label", "value"],
57045
57365
  allowedTransforms: ["aggregate", "sort", "limit"],
57046
57366
  bestFor: ["part-to-whole proportions", "share distribution with \u22647 categories"],
57047
57367
  antiPatterns: ["more than 7 slices (use bar)", "values that do not sum to a meaningful whole"],
57048
57368
  rules: [
57369
+ {
57370
+ code: "PIE_NO_AGGREGATE",
57371
+ severity: "warning",
57372
+ expression: "encoding.value.aggregate not set and no aggregate transform",
57373
+ message: "pie will show one slice per raw row. Add encoding.value.aggregate or data.transform aggregate + sort + limit.",
57374
+ validate: (chart) => {
57375
+ const hasAggTransform = (chart.data?.transform ?? []).some((t) => t.type === "aggregate");
57376
+ const hasEncodingAgg = !!chart.encoding?.value?.aggregate;
57377
+ if (!hasAggTransform && !hasEncodingAgg) {
57378
+ return {
57379
+ code: "PIE_NO_AGGREGATE",
57380
+ severity: "warning",
57381
+ message: `pie${chart.id ? ` '${chart.id}'` : ""}: no aggregation \u2014 will show one slice per raw row (too many slices, wrong values). Add encoding.value.aggregate (sum/avg/count) or data.transform: aggregate + sort + limit.`,
57382
+ chartId: chart.id
57383
+ };
57384
+ }
57385
+ return null;
57386
+ }
57387
+ },
57049
57388
  {
57050
57389
  code: "TOO_MANY_SLICES",
57051
57390
  severity: "warning",
@@ -57072,6 +57411,10 @@ var CHART_CATALOG = [
57072
57411
  {
57073
57412
  id: "scatter",
57074
57413
  displayName: "Scatter Chart",
57414
+ compactFor: "relationship,correlation",
57415
+ requires: "measure+measure",
57416
+ transformRecipe: "raw_or_limit",
57417
+ avoid: "single_measure,categorical_axis",
57075
57418
  requiredEncodings: ["x", "y"],
57076
57419
  allowedTransforms: ["sort", "limit"],
57077
57420
  bestFor: ["correlation between two measures", "distribution of two quantitative variables"],
@@ -57081,6 +57424,11 @@ var CHART_CATALOG = [
57081
57424
  {
57082
57425
  id: "histogram",
57083
57426
  displayName: "Histogram",
57427
+ compactFor: "distribution",
57428
+ requires: "measure+rows(>=20)",
57429
+ transformRecipe: "bin(numeric)>count",
57430
+ avoid: "rows<20,categorical_field",
57431
+ insightPattern: "{measure} distribution",
57084
57432
  requiredEncodings: ["x"],
57085
57433
  allowedTransforms: ["derive-month"],
57086
57434
  bestFor: ["distribution of a single numeric field", "frequency across value bins"],
@@ -57091,6 +57439,11 @@ var CHART_CATALOG = [
57091
57439
  {
57092
57440
  id: "heatmap",
57093
57441
  displayName: "Heatmap",
57442
+ compactFor: "matrix,density",
57443
+ requires: "dim+dim+measure",
57444
+ transformRecipe: "agg(dim,dim)>encode(value)",
57445
+ avoid: "single_dimension,sparse_matrix",
57446
+ insightPattern: "{measure} by {x_dimension} and {y_dimension}",
57094
57447
  requiredEncodings: ["x", "y", "value"],
57095
57448
  allowedTransforms: ["aggregate"],
57096
57449
  bestFor: ["matrix of two dimensions vs one measure", "calendar-style density maps"],
@@ -57100,6 +57453,10 @@ var CHART_CATALOG = [
57100
57453
  {
57101
57454
  id: "table",
57102
57455
  displayName: "Data Table",
57456
+ compactFor: "detail,high-cardinality,export",
57457
+ requires: "any_fields",
57458
+ transformRecipe: "sort_or_limit_optional",
57459
+ avoid: "too_many_columns_without_selection",
57103
57460
  requiredEncodings: [],
57104
57461
  allowedTransforms: ["aggregate", "sort", "limit", "derive-month"],
57105
57462
  bestFor: ["high-cardinality dimension detail", "multi-measure comparison", "data export"],
@@ -57109,17 +57466,60 @@ var CHART_CATALOG = [
57109
57466
  {
57110
57467
  id: "bigvalue",
57111
57468
  displayName: "Big Value (KPI Card)",
57469
+ compactFor: "kpi,summary",
57470
+ requires: "measure",
57471
+ transformRecipe: "agg(measure)>single_value",
57472
+ avoid: "raw_row_value,too_many_cards",
57473
+ insightPattern: "total {measure}",
57112
57474
  requiredEncodings: ["value"],
57113
57475
  allowedTransforms: ["aggregate"],
57114
57476
  bestFor: ["single top-level KPI", "summary metric with optional delta"],
57115
57477
  antiPatterns: ["more than 4 bigvalue cards per report (use kpigrid)", "showing a dimension value without a measure"],
57116
- rules: []
57478
+ rules: [
57479
+ {
57480
+ code: "BIGVALUE_NO_REDUCTION",
57481
+ severity: "warning",
57482
+ expression: "no aggregate transform and encoding.value.aggregate not set",
57483
+ message: "bigvalue will show rows[0] raw value. Add encoding.value.aggregate or data.transform aggregate + limit: 1.",
57484
+ validate: (chart) => {
57485
+ const hasAggTransform = (chart.data?.transform ?? []).some((t) => t.type === "aggregate");
57486
+ const hasEncodingAgg = !!chart.encoding?.value?.aggregate;
57487
+ if (!hasAggTransform && !hasEncodingAgg) {
57488
+ const field = chart.encoding?.value?.field ?? "value";
57489
+ return {
57490
+ code: "BIGVALUE_NO_REDUCTION",
57491
+ severity: "warning",
57492
+ message: `bigvalue${chart.id ? ` '${chart.id}'` : ""}: no aggregation \u2014 will display rows[0].${field} (a raw row value, almost always wrong). Add encoding.value.aggregate (max/sum/avg/min/count) or data.transform: aggregate + limit: 1.`,
57493
+ chartId: chart.id
57494
+ };
57495
+ }
57496
+ return null;
57497
+ }
57498
+ }
57499
+ ]
57117
57500
  }
57118
57501
  ];
57119
57502
  function getCatalogItem(chartType) {
57120
57503
  return CHART_CATALOG.find((c) => c.id === chartType);
57121
57504
  }
57122
57505
 
57506
+ // src/spec-utils.ts
57507
+ function findChartIndex(spec, chartId) {
57508
+ if (!chartId) return spec.charts.length === 1 ? 0 : -1;
57509
+ const byId = spec.charts.findIndex((c) => c.id === chartId);
57510
+ if (byId >= 0) return byId;
57511
+ return spec.charts.findIndex((c) => c.type === chartId);
57512
+ }
57513
+ function findLastChartIndexById(spec, id) {
57514
+ for (let i = spec.charts.length - 1; i >= 0; i--) {
57515
+ if (spec.charts[i].id === id) return i;
57516
+ }
57517
+ return -1;
57518
+ }
57519
+ function countChartsByType(spec, type) {
57520
+ return spec.charts.filter((c) => c.type === type).length;
57521
+ }
57522
+
57123
57523
  // src/spec-validator.ts
57124
57524
  var FORBIDDEN_WORDS = [
57125
57525
  { pattern: /\b(trend|趋势)\b/i, word: "trend/\u8D8B\u52BF" },
@@ -57174,6 +57574,8 @@ function validateReportSpec(spec, profile, formats = ["html"], context) {
57174
57574
  });
57175
57575
  }
57176
57576
  }
57577
+ const finalSchemaResult = validateEncodingFieldsInFinalSchema(chart);
57578
+ if (isAgentError(finalSchemaResult)) return finalSchemaResult;
57177
57579
  }
57178
57580
  return ok(parsed.data);
57179
57581
  }
@@ -57195,7 +57597,7 @@ function collectValidationWarnings(spec, profile, context) {
57195
57597
  `Report has ${spec.charts.length} charts (>6). Consider splitting into multiple sections or removing low-value charts.`
57196
57598
  );
57197
57599
  }
57198
- const bigvalueCount = spec.charts.filter((c) => c.type === "bigvalue").length;
57600
+ const bigvalueCount = countChartsByType(spec, "bigvalue");
57199
57601
  if (bigvalueCount > 4) {
57200
57602
  warnings.push(
57201
57603
  `Report has ${bigvalueCount} bigvalue cards (>4). Use kpigrid for 5+ KPI cards to avoid visual clutter.`
@@ -57235,8 +57637,20 @@ function collectValidationWarnings(spec, profile, context) {
57235
57637
  return warnings;
57236
57638
  }
57237
57639
  function validateEvidencePaths(spec, context) {
57640
+ const availableIds = context.evidence.map((e) => e.id);
57641
+ for (const insight of normalizeInsights(spec.insights)) {
57642
+ for (const evidenceId of insight.evidence) {
57643
+ if (!availableIds.includes(evidenceId)) {
57644
+ return agentError(
57645
+ "INSIGHT_EVIDENCE_NOT_FOUND",
57646
+ `insight.evidence id '${evidenceId}' not found in context.evidence. Available ids: ${availableIds.join(", ") || "(none)"}`,
57647
+ { evidenceId, availableIds }
57648
+ );
57649
+ }
57650
+ }
57651
+ }
57238
57652
  const texts = [
57239
- ...spec.insights ?? [],
57653
+ ...normalizeInsights(spec.insights).map((insight) => insight.text),
57240
57654
  ...spec.charts.map((c) => c.title).filter((t) => Boolean(t))
57241
57655
  ];
57242
57656
  for (const text of texts) {
@@ -57255,22 +57669,29 @@ function validateEvidencePaths(spec, context) {
57255
57669
  }
57256
57670
  function collectVerifyWarnings(spec, context) {
57257
57671
  const warnings = [];
57258
- const insights = spec.insights ?? [];
57259
- for (const text of insights) {
57672
+ const insights = normalizeInsights(spec.insights);
57673
+ for (const insight of insights) {
57260
57674
  for (const { pattern, word } of FORBIDDEN_WORDS) {
57261
- if (pattern.test(text)) {
57675
+ if (pattern.test(insight.text)) {
57262
57676
  warnings.push(
57263
- `insight contains forbidden word '${word}': "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}" \u2014 use only when backed by statistical evidence in context.evidence[]`
57677
+ `insight contains forbidden word '${word}': ${insightPreview(insight.text)} \u2014 use only when backed by statistical evidence in context.evidence[]`
57264
57678
  );
57265
57679
  }
57266
57680
  }
57681
+ if (typeof insight.original !== "string" && /\d/.test(insight.text) && insight.evidence.length === 0) {
57682
+ warnings.push(
57683
+ `insight contains numeric claim without structured evidence: ${insightPreview(insight.text)}`
57684
+ );
57685
+ }
57267
57686
  }
57268
57687
  if (context?.sampleWarnings.length) {
57269
57688
  const CAVEAT_PATTERNS = [
57270
57689
  /仅供参考|样本量|有限数据|based on.*rows?|N-row sample|limited data|small sample/i,
57271
57690
  /环比变化|period.over.period/i
57272
57691
  ];
57273
- const hasCaveat = insights.some((text) => CAVEAT_PATTERNS.some((p) => p.test(text)));
57692
+ const hasCaveat = insights.some(
57693
+ (insight) => Boolean(insight.caveat) || CAVEAT_PATTERNS.some((p) => p.test(insight.text))
57694
+ );
57274
57695
  if (insights.length > 0 && !hasCaveat) {
57275
57696
  const codes = context.sampleWarnings.map((w) => w.code).join(", ");
57276
57697
  warnings.push(
@@ -57280,6 +57701,24 @@ function collectVerifyWarnings(spec, context) {
57280
57701
  }
57281
57702
  return warnings;
57282
57703
  }
57704
+ function strictVerifyError(warnings) {
57705
+ if (warnings.length === 0) return ok(void 0);
57706
+ const issues = warnings.map(warningToVerifyIssue);
57707
+ return agentError(
57708
+ "STRICT_VERIFY_FAILED",
57709
+ `Strict verify failed with ${warnings.length} warning(s).`,
57710
+ { warnings, issues }
57711
+ );
57712
+ }
57713
+ function warningToVerifyIssue(message) {
57714
+ if (message.includes("forbidden word")) {
57715
+ return { code: "INSIGHT_FORBIDDEN_WORD_STRICT", message };
57716
+ }
57717
+ if (message.includes("numeric claim without structured evidence")) {
57718
+ return { code: "INSIGHT_NUMERIC_CLAIM_WITHOUT_EVIDENCE_STRICT", message };
57719
+ }
57720
+ return { code: "INSIGHT_MISSING_CAVEAT_STRICT", message };
57721
+ }
57283
57722
  function validateChartType(chart) {
57284
57723
  if (!MVP_CHART_TYPES.includes(chart.type)) {
57285
57724
  return agentError("UNSUPPORTED_CHART_TYPE", `Chart type '${chart.type}' is not supported in the MVP.`, {
@@ -57365,6 +57804,38 @@ function validateTransforms(chart) {
57365
57804
  }
57366
57805
  return ok(chart);
57367
57806
  }
57807
+ function simulateFinalSchema(chart) {
57808
+ let schema = null;
57809
+ for (const transform2 of chart.data?.transform ?? []) {
57810
+ if (transform2.type === "aggregate") {
57811
+ const next = /* @__PURE__ */ new Set();
57812
+ for (const field of transform2.groupBy ?? []) next.add(field);
57813
+ for (const measure of transform2.measures ?? []) next.add(measure.as);
57814
+ schema = next;
57815
+ } else if (transform2.type === "derive-month" && transform2.as && schema !== null) {
57816
+ schema.add(transform2.as);
57817
+ }
57818
+ }
57819
+ return schema;
57820
+ }
57821
+ function validateEncodingFieldsInFinalSchema(chart) {
57822
+ const finalSchema = simulateFinalSchema(chart);
57823
+ if (!finalSchema) return ok(chart);
57824
+ const chartLabel = chart.id ? `chart '${chart.id}'` : `${chart.type} chart`;
57825
+ const available = [...finalSchema].join(", ") || "(none)";
57826
+ for (const [channel, encoding] of Object.entries(chart.encoding ?? {})) {
57827
+ const field = encoding?.field;
57828
+ if (!field) continue;
57829
+ if (!finalSchema.has(field)) {
57830
+ return agentError(
57831
+ "ENCODING_FIELD_NOT_IN_FINAL_ROWS",
57832
+ `${chartLabel}: encoding.${channel}.field '${field}' does not exist in rows after transforms. Available fields after transforms: ${available}`,
57833
+ { chartId: chart.id, channel, field, availableAfterTransforms: [...finalSchema] }
57834
+ );
57835
+ }
57836
+ }
57837
+ return ok(chart);
57838
+ }
57368
57839
  function collectSourceFields(chart, derivedFields) {
57369
57840
  const fields = /* @__PURE__ */ new Set();
57370
57841
  for (const encoding of Object.values(chart.encoding ?? {})) {
@@ -57428,6 +57899,18 @@ var blockedBlockEntrySchema = external_exports.object({
57428
57899
  id: external_exports.string().min(1),
57429
57900
  reason: external_exports.string().min(1)
57430
57901
  });
57902
+ var catalogTemplateEntrySchema = external_exports.object({
57903
+ id: external_exports.string().min(1),
57904
+ score: external_exports.number().min(0).max(1),
57905
+ bestFor: external_exports.array(external_exports.string()),
57906
+ requires: external_exports.array(external_exports.enum(["measure", "dimension", "time"])),
57907
+ blocks: external_exports.array(external_exports.string()),
57908
+ density: external_exports.enum(["compact", "medium", "full"])
57909
+ });
57910
+ var blockedTemplateEntrySchema = external_exports.object({
57911
+ id: external_exports.string().min(1),
57912
+ reason: external_exports.string().min(1)
57913
+ });
57431
57914
  var analyzeCatalogSchema = external_exports.object({
57432
57915
  charts: external_exports.array(external_exports.string().min(1)),
57433
57916
  blockedCharts: external_exports.array(external_exports.object({
@@ -57439,7 +57922,9 @@ var analyzeCatalogSchema = external_exports.object({
57439
57922
  note: external_exports.string().optional()
57440
57923
  })),
57441
57924
  blocks: external_exports.array(catalogBlockEntrySchema).optional(),
57442
- blockedBlocks: external_exports.array(blockedBlockEntrySchema).optional()
57925
+ blockedBlocks: external_exports.array(blockedBlockEntrySchema).optional(),
57926
+ templates: external_exports.array(catalogTemplateEntrySchema).optional(),
57927
+ blockedTemplates: external_exports.array(blockedTemplateEntrySchema).optional()
57443
57928
  });
57444
57929
  var metricCandidateSchema = external_exports.object({
57445
57930
  id: external_exports.string().min(1),
@@ -57450,15 +57935,35 @@ var metricCandidateSchema = external_exports.object({
57450
57935
  confidence: external_exports.enum(["high", "medium"]),
57451
57936
  caveat: external_exports.string().optional()
57452
57937
  });
57938
+ var analyzeAssumptionSchema = external_exports.object({
57939
+ key: external_exports.enum(["primary_measure", "primary_dimension", "time_field"]),
57940
+ value: external_exports.string(),
57941
+ confidence: external_exports.number().min(0).max(1),
57942
+ alternatives: external_exports.array(external_exports.string()).optional(),
57943
+ reason: external_exports.string().optional()
57944
+ });
57945
+ var legacyAssumptionSchema = external_exports.string().transform((value) => ({
57946
+ key: value.includes("dimension") ? "primary_dimension" : value.includes("time") ? "time_field" : "primary_measure",
57947
+ value,
57948
+ confidence: 0.5,
57949
+ reason: "legacy string assumption"
57950
+ }));
57453
57951
  var analyzeIntentSchema = external_exports.object({
57454
57952
  raw: external_exports.string().min(1),
57455
57953
  coverage: external_exports.enum(["full", "partial"]),
57456
- assumptions: external_exports.array(external_exports.string())
57954
+ assumptions: external_exports.array(external_exports.union([analyzeAssumptionSchema, legacyAssumptionSchema]))
57457
57955
  });
57458
57956
  var analyzeSampleWarningSchema = external_exports.object({
57459
57957
  code: external_exports.string().min(1),
57460
57958
  message: external_exports.string().min(1)
57461
57959
  });
57960
+ var clarificationQuestionSchema = external_exports.object({
57961
+ id: external_exports.string().min(1),
57962
+ question: external_exports.string().min(1),
57963
+ options: external_exports.array(external_exports.string()),
57964
+ blocking: external_exports.boolean(),
57965
+ appliesTo: external_exports.enum(["measure", "dimension", "time", "template"])
57966
+ });
57462
57967
  var analyzeContextSchema = external_exports.object({
57463
57968
  intent: analyzeIntentSchema,
57464
57969
  fields: external_exports.array(analyzeFieldSchema),
@@ -57466,8 +57971,158 @@ var analyzeContextSchema = external_exports.object({
57466
57971
  catalog: analyzeCatalogSchema,
57467
57972
  sampleWarnings: external_exports.array(analyzeSampleWarningSchema),
57468
57973
  promptRules: external_exports.array(external_exports.string()),
57469
- metricCandidates: external_exports.array(metricCandidateSchema).optional()
57470
- });
57974
+ metricCandidates: external_exports.array(metricCandidateSchema).optional(),
57975
+ clarificationQuestions: external_exports.array(clarificationQuestionSchema).optional()
57976
+ });
57977
+ var compactAnalyzeContextSchema = external_exports.object({
57978
+ format: external_exports.literal("compact-v1"),
57979
+ intent: external_exports.object({
57980
+ raw: external_exports.string(),
57981
+ coverage: external_exports.enum(["full", "partial"])
57982
+ }),
57983
+ assumptions: external_exports.array(external_exports.tuple([
57984
+ external_exports.enum(["primary_measure", "primary_dimension", "time_field"]),
57985
+ external_exports.string(),
57986
+ external_exports.number(),
57987
+ external_exports.array(external_exports.string()).nullable().optional()
57988
+ ])),
57989
+ fields: external_exports.array(external_exports.tuple([
57990
+ external_exports.string(),
57991
+ external_exports.enum(["measure", "dimension", "time", "id", "status", "score", "unknown"]),
57992
+ external_exports.enum(["number", "string", "date", "boolean", "unknown"]),
57993
+ external_exports.number().nullable().optional(),
57994
+ external_exports.number().nullable().optional()
57995
+ ])),
57996
+ evidence: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.union([
57997
+ external_exports.record(external_exports.string(), external_exports.unknown()),
57998
+ external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown()))
57999
+ ])])),
58000
+ metricCandidates: external_exports.array(external_exports.tuple([
58001
+ external_exports.string(),
58002
+ external_exports.enum(["unit_average", "rate", "share", "period_change", "difference"]),
58003
+ external_exports.string(),
58004
+ external_exports.number().nullable().optional()
58005
+ ])),
58006
+ catalog: external_exports.object({
58007
+ charts: external_exports.array(external_exports.string()),
58008
+ blockedCharts: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()])),
58009
+ blocks: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.number(), external_exports.enum(["compact", "medium", "full"]), external_exports.array(external_exports.string())])).optional(),
58010
+ blockedBlocks: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()])).optional(),
58011
+ templates: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.number(), external_exports.enum(["compact", "medium", "full"]), external_exports.array(external_exports.string())])).optional(),
58012
+ blockedTemplates: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()])).optional()
58013
+ }),
58014
+ warnings: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()])),
58015
+ clarificationQuestions: external_exports.array(external_exports.tuple([
58016
+ external_exports.string(),
58017
+ external_exports.string(),
58018
+ external_exports.array(external_exports.string()),
58019
+ external_exports.boolean(),
58020
+ external_exports.enum(["measure", "dimension", "time", "template"])
58021
+ ]))
58022
+ });
58023
+ function toCompactAnalyzeContext(ctx) {
58024
+ return {
58025
+ format: "compact-v1",
58026
+ intent: { raw: ctx.intent.raw, coverage: ctx.intent.coverage },
58027
+ assumptions: ctx.intent.assumptions.map((a) => [a.key, a.value, a.confidence, a.alternatives]),
58028
+ fields: ctx.fields.map((f) => [f.name, f.role, f.type, f.distinctCount, f.timePeriods]),
58029
+ evidence: ctx.evidence.map((e) => [e.id, e.values ?? e.rows ?? {}]),
58030
+ metricCandidates: (ctx.metricCandidates ?? []).map((m) => [m.id, m.type, m.formula, m.value]),
58031
+ catalog: {
58032
+ charts: ctx.catalog.charts,
58033
+ blockedCharts: ctx.catalog.blockedCharts.map((c) => [c.type, c.reason]),
58034
+ blocks: ctx.catalog.blocks?.map((b) => [b.id, b.score, b.density, b.charts]),
58035
+ blockedBlocks: ctx.catalog.blockedBlocks?.map((b) => [b.id, b.reason]),
58036
+ templates: ctx.catalog.templates?.map((t) => [t.id, t.score, t.density, t.blocks]),
58037
+ blockedTemplates: ctx.catalog.blockedTemplates?.map((t) => [t.id, t.reason])
58038
+ },
58039
+ warnings: ctx.sampleWarnings.map((w) => [w.code, w.message]),
58040
+ clarificationQuestions: (ctx.clarificationQuestions ?? []).map((q) => [
58041
+ q.id,
58042
+ q.question,
58043
+ q.options,
58044
+ q.blocking,
58045
+ q.appliesTo
58046
+ ])
58047
+ };
58048
+ }
58049
+ function fromCompactAnalyzeContext(ctx) {
58050
+ return {
58051
+ intent: {
58052
+ raw: ctx.intent.raw,
58053
+ coverage: ctx.intent.coverage,
58054
+ assumptions: ctx.assumptions.map(([key, value, confidence, alternatives]) => ({
58055
+ key,
58056
+ value,
58057
+ confidence,
58058
+ alternatives: alternatives ?? void 0
58059
+ }))
58060
+ },
58061
+ fields: ctx.fields.map(([name, role, type, distinctCount, timePeriods]) => ({
58062
+ name,
58063
+ role,
58064
+ type,
58065
+ ...distinctCount !== void 0 && distinctCount !== null ? { distinctCount } : {},
58066
+ ...timePeriods !== void 0 && timePeriods !== null ? { timePeriods } : {}
58067
+ })),
58068
+ evidence: ctx.evidence.map(([id, value]) => ({
58069
+ id,
58070
+ query: `compact evidence: ${id}`,
58071
+ ...Array.isArray(value) ? { rows: value } : { values: value }
58072
+ })),
58073
+ catalog: {
58074
+ charts: ctx.catalog.charts,
58075
+ blockedCharts: ctx.catalog.blockedCharts.map(([type, reason]) => ({ type, reason })),
58076
+ recommendedPlan: [],
58077
+ blocks: ctx.catalog.blocks?.map(([id, score, density, charts]) => ({
58078
+ id,
58079
+ score,
58080
+ description: "",
58081
+ bestFor: [],
58082
+ density,
58083
+ examplePrompt: "",
58084
+ charts,
58085
+ variables: {},
58086
+ qualityChecks: []
58087
+ })),
58088
+ blockedBlocks: ctx.catalog.blockedBlocks?.map(([id, reason]) => ({ id, reason })),
58089
+ templates: ctx.catalog.templates?.map(([id, score, density, blocks]) => ({
58090
+ id,
58091
+ score,
58092
+ bestFor: [],
58093
+ requires: [],
58094
+ blocks,
58095
+ density
58096
+ })),
58097
+ blockedTemplates: ctx.catalog.blockedTemplates?.map(([id, reason]) => ({ id, reason }))
58098
+ },
58099
+ sampleWarnings: ctx.warnings.map(([code, message]) => ({ code, message })),
58100
+ promptRules: [],
58101
+ metricCandidates: ctx.metricCandidates.map(([id, type, formula, value]) => ({
58102
+ id,
58103
+ type,
58104
+ label: id,
58105
+ formula,
58106
+ ...value !== void 0 && value !== null ? { value } : {},
58107
+ confidence: "medium"
58108
+ })),
58109
+ clarificationQuestions: ctx.clarificationQuestions.map(([id, question, options, blocking, appliesTo]) => ({
58110
+ id,
58111
+ question,
58112
+ options,
58113
+ blocking,
58114
+ appliesTo
58115
+ }))
58116
+ };
58117
+ }
58118
+ function parseAnalyzeContext(value) {
58119
+ const unwrapped = value.ok === true ? value.value : value;
58120
+ const full = analyzeContextSchema.safeParse(unwrapped);
58121
+ if (full.success) return full.data;
58122
+ const compact = compactAnalyzeContextSchema.safeParse(unwrapped);
58123
+ if (compact.success) return fromCompactAnalyzeContext(compact.data);
58124
+ return null;
58125
+ }
57471
58126
 
57472
58127
  // src/deck-layouts.ts
57473
58128
  function withSize(chart, width, height) {
@@ -57961,13 +58616,13 @@ function validateChartFields(chart, sourceFields, path) {
57961
58616
  return deckFieldError(path, chart.type, `Chart type '${chart.type}' is not supported.`);
57962
58617
  }
57963
58618
  for (const encoding of REQUIRED_ENCODINGS[chart.type] ?? []) {
57964
- if (!chart.encoding[encoding]?.field) {
58619
+ if (!chart.encoding?.[encoding]?.field) {
57965
58620
  return deckFieldError(`${path}.encoding.${encoding}`, encoding, `Chart type '${chart.type}' requires encoding '${encoding}'.`);
57966
58621
  }
57967
58622
  }
57968
58623
  const available = applyTransforms(chart.data?.transform ?? [], sourceFields, path);
57969
58624
  if (isAgentError(available)) return available;
57970
- for (const [encoding, spec] of Object.entries(chart.encoding)) {
58625
+ for (const [encoding, spec] of Object.entries(chart.encoding ?? {})) {
57971
58626
  if (spec?.field && !available.value.has(spec.field)) {
57972
58627
  return deckFieldError(`${path}.encoding.${encoding}.field`, spec.field, `Field '${spec.field}' is not available for this chart encoding.`);
57973
58628
  }
@@ -58064,7 +58719,7 @@ var infographicSpecSchema = external_exports.object({
58064
58719
  inputFile: external_exports.string().default(""),
58065
58720
  generatedAt: external_exports.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
58066
58721
  wordCount: external_exports.number().int().min(0).default(0)
58067
- }).default({})
58722
+ }).default(() => ({ inputFile: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), wordCount: 0 }))
58068
58723
  });
58069
58724
  function loadInfographicSpec(file2) {
58070
58725
  let raw;
@@ -58768,6 +59423,178 @@ function toCatalogBlockEntry(resolver, decision, ctx) {
58768
59423
  };
58769
59424
  }
58770
59425
 
59426
+ // src/report-template-registry.ts
59427
+ var YAML = __toESM(require_dist(), 1);
59428
+ function scoreForRequirements(ctx, requires) {
59429
+ for (const role of requires) {
59430
+ if (role === "measure" && !ctx.fields.some((f) => f.role === "measure" || f.role === "score")) {
59431
+ return { ok: false, score: 0, reason: "missing required measure" };
59432
+ }
59433
+ if (role === "dimension" && !ctx.fields.some((f) => f.role === "dimension" || f.role === "status")) {
59434
+ return { ok: false, score: 0, reason: "missing required dimension" };
59435
+ }
59436
+ if (role === "time") {
59437
+ const time3 = ctx.fields.find((f) => f.role === "time");
59438
+ if (!time3) return { ok: false, score: 0, reason: "missing required time" };
59439
+ const periods = time3.timePeriods ?? 0;
59440
+ if (periods < 3) return { ok: false, score: 0, reason: `timePeriods=${periods} < 3` };
59441
+ }
59442
+ }
59443
+ return { ok: true, score: Math.min(0.65 + requires.length * 0.1 + (ctx.evidence.length > 0 ? 0.05 : 0), 1) };
59444
+ }
59445
+ function compileBlocks(blockIds, ctx) {
59446
+ const charts = [];
59447
+ for (const id of blockIds) {
59448
+ const block = getBlockById(id);
59449
+ if (!block) continue;
59450
+ const decision = block.canUse(ctx);
59451
+ if (!decision.ok) continue;
59452
+ const variables = block.defaultVariables(ctx);
59453
+ charts.push(...block.compile(variables, ctx).charts);
59454
+ }
59455
+ const seen = /* @__PURE__ */ new Set();
59456
+ return {
59457
+ title: "Miao Vision Report",
59458
+ insights: [],
59459
+ charts: charts.map((chart) => {
59460
+ if (!chart.id || !seen.has(chart.id)) {
59461
+ if (chart.id) seen.add(chart.id);
59462
+ return chart;
59463
+ }
59464
+ const next = { ...chart, id: `${chart.id}_${seen.size + 1}` };
59465
+ seen.add(next.id);
59466
+ return next;
59467
+ })
59468
+ };
59469
+ }
59470
+ var TEMPLATE_REGISTRY = [
59471
+ {
59472
+ id: "snapshot-overview",
59473
+ bestFor: ["static comparison", "category ranking", "no time axis"],
59474
+ requires: ["measure", "dimension"],
59475
+ blocks: ["kpi-summary", "snapshot-ranking"],
59476
+ density: "compact",
59477
+ qualityNotes: ["Use for static comparisons without requiring a trend.", "Add sample caveats when sampleWarnings are present."],
59478
+ canUse: (ctx) => scoreForRequirements(ctx, ["measure", "dimension"]),
59479
+ instantiate: (ctx) => compileBlocks(["kpi-summary", "snapshot-ranking"], ctx)
59480
+ },
59481
+ {
59482
+ id: "trend-ranking-overview",
59483
+ bestFor: ["executive trend with category ranking", "monthly review"],
59484
+ requires: ["measure", "dimension", "time"],
59485
+ blocks: ["trend-ranking"],
59486
+ density: "full",
59487
+ qualityNotes: ["Requires at least 3 time periods.", "Combines KPI, trend, and ranking views."],
59488
+ canUse: (ctx) => scoreForRequirements(ctx, ["measure", "dimension", "time"]),
59489
+ instantiate: (ctx) => compileBlocks(["trend-ranking"], ctx)
59490
+ },
59491
+ {
59492
+ id: "full-detail-report",
59493
+ bestFor: ["comprehensive business review", "trend plus ranking plus table"],
59494
+ requires: ["measure", "dimension", "time"],
59495
+ blocks: ["full-detail-report"],
59496
+ density: "full",
59497
+ qualityNotes: ["Use for comprehensive reviews where detail table is acceptable.", "Keep total chart count within report limits."],
59498
+ canUse: (ctx) => scoreForRequirements(ctx, ["measure", "dimension", "time"]),
59499
+ instantiate: (ctx) => compileBlocks(["full-detail-report"], ctx)
59500
+ },
59501
+ {
59502
+ id: "composition-review",
59503
+ bestFor: ["share analysis", "part-to-whole breakdown", "composition report"],
59504
+ requires: ["measure", "dimension"],
59505
+ blocks: ["kpi-summary", "comparison-breakdown"],
59506
+ density: "medium",
59507
+ qualityNotes: ["Use for share or composition analysis.", "Avoid when the primary dimension has too many categories for pie."],
59508
+ canUse: (ctx) => scoreForRequirements(ctx, ["measure", "dimension"]),
59509
+ instantiate: (ctx) => compileBlocks(["kpi-summary", "comparison-breakdown"], ctx)
59510
+ }
59511
+ ];
59512
+ function getTemplateById(id) {
59513
+ return TEMPLATE_REGISTRY.find((template) => template.id === id);
59514
+ }
59515
+ function templateInfo(template) {
59516
+ return {
59517
+ id: template.id,
59518
+ bestFor: template.bestFor,
59519
+ requires: template.requires,
59520
+ blocks: template.blocks,
59521
+ density: template.density,
59522
+ qualityNotes: template.qualityNotes
59523
+ };
59524
+ }
59525
+ function toCatalogTemplateEntry(resolver, decision) {
59526
+ return {
59527
+ id: resolver.id,
59528
+ score: decision.score,
59529
+ bestFor: resolver.bestFor,
59530
+ requires: resolver.requires,
59531
+ blocks: resolver.blocks,
59532
+ density: resolver.density
59533
+ };
59534
+ }
59535
+ function templateSpecToYaml(spec) {
59536
+ return YAML.stringify(spec);
59537
+ }
59538
+ function buildTemplateCatalog(ctx) {
59539
+ const templates = [];
59540
+ const blockedTemplates = [];
59541
+ for (const template of TEMPLATE_REGISTRY) {
59542
+ const decision = template.canUse(ctx);
59543
+ if (decision.ok && decision.score >= 0.5) {
59544
+ templates.push(toCatalogTemplateEntry(template, decision));
59545
+ } else {
59546
+ blockedTemplates.push({ id: template.id, reason: decision.reason ?? `score=${decision.score.toFixed(2)} < 0.5` });
59547
+ }
59548
+ }
59549
+ templates.sort((a, b) => b.score - a.score);
59550
+ return { templates, blockedTemplates };
59551
+ }
59552
+
59553
+ // src/analyze-clarifications.ts
59554
+ function buildClarificationQuestions(fields, intent = "") {
59555
+ const questions = [];
59556
+ const measures = fields.filter((f) => f.role === "measure" || f.role === "score");
59557
+ const dimensions = fields.filter((f) => f.role === "dimension" || f.role === "status");
59558
+ const times = fields.filter((f) => f.role === "time");
59559
+ const precisionMode = /precise|accurate|decision|executive|老板|决策|精准/i.test(intent);
59560
+ if (measures.length === 0) {
59561
+ questions.push({
59562
+ id: "primary_measure",
59563
+ question: "Which field should be treated as the primary measure?",
59564
+ options: fields.map((f) => f.name),
59565
+ blocking: true,
59566
+ appliesTo: "measure"
59567
+ });
59568
+ } else if (measures.length > 1) {
59569
+ questions.push({
59570
+ id: "primary_measure",
59571
+ question: `This dataset has multiple measures. Analyze ${measures.map((f) => f.name).join(", ")}?`,
59572
+ options: measures.map((f) => f.name),
59573
+ blocking: precisionMode,
59574
+ appliesTo: "measure"
59575
+ });
59576
+ }
59577
+ if (dimensions.length > 1) {
59578
+ questions.push({
59579
+ id: "primary_dimension",
59580
+ question: `Which dimension should drive the main comparison: ${dimensions.map((f) => f.name).join(", ")}?`,
59581
+ options: dimensions.map((f) => f.name),
59582
+ blocking: false,
59583
+ appliesTo: "dimension"
59584
+ });
59585
+ }
59586
+ if (times.length > 1) {
59587
+ questions.push({
59588
+ id: "time_field",
59589
+ question: `Which time field should drive trend views: ${times.map((f) => f.name).join(", ")}?`,
59590
+ options: times.map((f) => f.name),
59591
+ blocking: false,
59592
+ appliesTo: "time"
59593
+ });
59594
+ }
59595
+ return questions;
59596
+ }
59597
+
58771
59598
  // src/analyzer.ts
58772
59599
  function analyzeDataset(dataset, options = {}) {
58773
59600
  const profile = profileDataset(dataset);
@@ -58782,7 +59609,8 @@ function analyzeDataset(dataset, options = {}) {
58782
59609
  const catalog = buildCatalog(fields, sampleWarnings, profile.rows, evidence);
58783
59610
  const promptRules = buildPromptRules(catalog.charts, sampleWarnings);
58784
59611
  const metricCandidates = buildMetricCandidates(fields, evidence);
58785
- return { intent, fields, evidence, catalog, sampleWarnings, promptRules, metricCandidates };
59612
+ const clarificationQuestions = buildClarificationQuestions(fields, options.intent ?? "");
59613
+ return { intent, fields, evidence, catalog, sampleWarnings, promptRules, metricCandidates, clarificationQuestions };
58786
59614
  }
58787
59615
  function buildAnalyzeFields(columns) {
58788
59616
  return columns.map((col) => {
@@ -58816,7 +59644,6 @@ function refineRole(col) {
58816
59644
  if (col.type === "string") {
58817
59645
  const name = col.name.toLowerCase();
58818
59646
  if (/\b(status|state|phase|stage|flag|type|category|tier)\b/.test(name) && col.distinctCount <= 10) return "status";
58819
- if (col.role === "id") return "id";
58820
59647
  return "dimension";
58821
59648
  }
58822
59649
  return "unknown";
@@ -58838,27 +59665,50 @@ function parseIntent(raw, fields, correctAssumption) {
58838
59665
  const times = fields.filter((f) => f.role === "time");
58839
59666
  let primaryMeasure = measures[0]?.name;
58840
59667
  let primaryDimension = dimensions[0]?.name;
59668
+ let timeField = times[0]?.name;
58841
59669
  if (correctAssumption) {
58842
- const m = correctAssumption.match(/^primary_measure=(\w+)$/);
58843
- const d = correctAssumption.match(/^primary_dimension=(\w+)$/);
59670
+ const m = correctAssumption.match(/^primary_measure=([\w-]+)$/);
59671
+ const d = correctAssumption.match(/^primary_dimension=([\w-]+)$/);
59672
+ const t = correctAssumption.match(/^time_field=([\w-]+)$/);
58844
59673
  if (m) primaryMeasure = m[1];
58845
59674
  if (d) primaryDimension = d[1];
59675
+ if (t) timeField = t[1];
58846
59676
  }
58847
59677
  const assumptions = [];
58848
59678
  if (primaryMeasure) {
58849
- const verify = measures.length > 1 ? ` (verify: ${measures.map((f) => f.name).join(", ")} \u2014 choose with --correct-assumption primary_measure=<name>)` : "";
58850
- assumptions.push(`primary measure is "${primaryMeasure}"${verify}`);
59679
+ assumptions.push({
59680
+ key: "primary_measure",
59681
+ value: primaryMeasure,
59682
+ confidence: measures.length > 1 ? 0.62 : 0.9,
59683
+ alternatives: measures.filter((f) => f.name !== primaryMeasure).map((f) => f.name),
59684
+ reason: measures.length > 1 ? "multiple numeric measures detected" : "single clear numeric measure"
59685
+ });
58851
59686
  }
58852
59687
  if (primaryDimension) {
58853
- assumptions.push(`primary dimension is "${primaryDimension}"`);
59688
+ assumptions.push({
59689
+ key: "primary_dimension",
59690
+ value: primaryDimension,
59691
+ confidence: dimensions.length > 1 ? 0.72 : 0.9,
59692
+ alternatives: dimensions.filter((f) => f.name !== primaryDimension).map((f) => f.name),
59693
+ reason: dimensions.length > 1 ? "multiple dimensions detected" : "single clear dimension"
59694
+ });
58854
59695
  }
58855
59696
  if (times.length > 0) {
58856
- const t = times[0];
58857
- const verify = times.length > 1 ? ` (verify: ${times.map((f) => f.name).join(", ")})` : "";
58858
- assumptions.push(`time field is "${t.name}"${verify}`);
59697
+ assumptions.push({
59698
+ key: "time_field",
59699
+ value: timeField ?? times[0].name,
59700
+ confidence: times.length > 1 ? 0.7 : 0.9,
59701
+ alternatives: times.filter((f) => f.name !== timeField).map((f) => f.name),
59702
+ reason: times.length > 1 ? "multiple time fields detected" : "single clear time field"
59703
+ });
58859
59704
  }
58860
59705
  if (measures.length === 0) {
58861
- assumptions.push("no numeric measure detected \u2014 use --correct-assumption primary_measure=<col>");
59706
+ assumptions.push({
59707
+ key: "primary_measure",
59708
+ value: "",
59709
+ confidence: 0,
59710
+ reason: "no numeric measure detected"
59711
+ });
58862
59712
  }
58863
59713
  const rawLower = raw.toLowerCase();
58864
59714
  const wantsTrend = /trend|over time|by month|by year/.test(rawLower);
@@ -58877,7 +59727,7 @@ function runStandardQueries(dataset, fields, sampleWarnings) {
58877
59727
  if (measures.length > 0) {
58878
59728
  const measureExpr = measures.slice(0, 4).map((m) => `sum(${m.name}) as total_${m.name}`).concat(["count(*) as row_count"]).join(", ");
58879
59729
  const result = queryDataset(dataset.rows, { measure: measureExpr });
58880
- if (result && "rows" in result && result.rows.length > 0) {
59730
+ if (!isAgentError(result) && result.rows.length > 0) {
58881
59731
  evidence.push({
58882
59732
  id: "total",
58883
59733
  query: `Total aggregates: ${measureExpr}`,
@@ -58892,7 +59742,7 @@ function runStandardQueries(dataset, fields, sampleWarnings) {
58892
59742
  measure: measureExpr,
58893
59743
  orderby: `total_${primaryMeasure.name} desc`
58894
59744
  });
58895
- if (result && "rows" in result && result.rows.length > 0) {
59745
+ if (!isAgentError(result) && result.rows.length > 0) {
58896
59746
  const totalVal = result.rows.reduce((s, r) => s + Number(r[`total_${primaryMeasure.name}`] ?? 0), 0);
58897
59747
  const rows = result.rows.map((r) => {
58898
59748
  const val = Number(r[`total_${primaryMeasure.name}`] ?? 0);
@@ -58913,7 +59763,7 @@ function runStandardQueries(dataset, fields, sampleWarnings) {
58913
59763
  measure: measureExpr,
58914
59764
  orderby: `${primaryTime.name} asc`
58915
59765
  });
58916
- if (result && "rows" in result && result.rows.length > 0) {
59766
+ if (!isAgentError(result) && result.rows.length > 0) {
58917
59767
  evidence.push({
58918
59768
  id: "by_time",
58919
59769
  query: `${primaryMeasure.name} by ${primaryTime.name} (ascending)`,
@@ -58937,7 +59787,7 @@ function runExtraQuery(dataset, extraQuery, existingCount) {
58937
59787
  orderby: parts.orderby,
58938
59788
  limit: parts.limit ? Number(parts.limit) : void 0
58939
59789
  });
58940
- if (!result || !("rows" in result)) return null;
59790
+ if (isAgentError(result)) return null;
58941
59791
  return {
58942
59792
  id: `extra_${existingCount + 1}`,
58943
59793
  query: `Custom query: ${extraQuery}`,
@@ -59027,7 +59877,8 @@ function buildCatalog(fields, warnings, rowCount, evidence) {
59027
59877
  }
59028
59878
  }
59029
59879
  blocks.sort((a, b) => b.score - a.score);
59030
- return { charts, blockedCharts, recommendedPlan, blocks, blockedBlocks };
59880
+ const templateCatalog = buildTemplateCatalog(matchCtx);
59881
+ return { charts, blockedCharts, recommendedPlan, blocks, blockedBlocks, ...templateCatalog };
59031
59882
  }
59032
59883
  function buildRecommendedPlan(charts, fields) {
59033
59884
  const plan = [];
@@ -59127,7 +59978,7 @@ function generatePatchHints(error51, spec, catalogCharts) {
59127
59978
  switch (error51.code) {
59128
59979
  case "UNSUPPORTED_TRANSFORM": {
59129
59980
  const chartId = detail?.["chartId"];
59130
- const chartIndex = spec.charts.findIndex((c) => (c.id ?? c.type) === chartId);
59981
+ const chartIndex = findChartIndex(spec, chartId);
59131
59982
  if (chartIndex < 0) return void 0;
59132
59983
  const transforms = spec.charts[chartIndex].data?.transform ?? [];
59133
59984
  const tIdx = transforms.findIndex((t) => t.type === "filter");
@@ -59136,7 +59987,7 @@ function generatePatchHints(error51, spec, catalogCharts) {
59136
59987
  }
59137
59988
  case "BLOCKED_CHART_STRICT": {
59138
59989
  const chartType = detail?.["chartType"];
59139
- const chartIdx = spec.charts.findIndex((c) => c.type === chartType);
59990
+ const chartIdx = findChartIndex(spec, chartType);
59140
59991
  if (chartIdx < 0 || !catalogCharts?.length) return void 0;
59141
59992
  const suggestion = catalogCharts[0];
59142
59993
  return [{ op: "replace", path: `/charts/${chartIdx}/type`, value: suggestion }];
@@ -59144,20 +59995,14 @@ function generatePatchHints(error51, spec, catalogCharts) {
59144
59995
  case "DUPLICATE_CHART_ID": {
59145
59996
  const dupId = detail?.["chartId"];
59146
59997
  if (!dupId) return void 0;
59147
- let lastIdx = -1;
59148
- for (let i = spec.charts.length - 1; i >= 0; i--) {
59149
- if (spec.charts[i].id === dupId) {
59150
- lastIdx = i;
59151
- break;
59152
- }
59153
- }
59998
+ const lastIdx = findLastChartIndexById(spec, dupId);
59154
59999
  if (lastIdx < 0) return void 0;
59155
60000
  return [{ op: "replace", path: `/charts/${lastIdx}/id`, value: `${dupId}_2` }];
59156
60001
  }
59157
60002
  case "MISSING_ENCODING": {
59158
60003
  const chartType = detail?.["chartType"];
59159
60004
  const required2 = detail?.["requiredEncodings"];
59160
- const chartIdx = spec.charts.findIndex((c) => c.type === chartType);
60005
+ const chartIdx = findChartIndex(spec, chartType);
59161
60006
  if (chartIdx < 0 || !required2?.length) return void 0;
59162
60007
  const existing = Object.keys(spec.charts[chartIdx].encoding ?? {});
59163
60008
  const missing = required2.filter((enc) => !existing.includes(enc));
@@ -59203,12 +60048,6 @@ function collectWarningPatches(spec) {
59203
60048
  }
59204
60049
  return patches;
59205
60050
  }
59206
- function findChartIndex(spec, chartId) {
59207
- if (!chartId) return spec.charts.length === 1 ? 0 : -1;
59208
- const byId = spec.charts.findIndex((c) => c.id === chartId);
59209
- if (byId >= 0) return byId;
59210
- return spec.charts.findIndex((c) => c.type === chartId);
59211
- }
59212
60051
 
59213
60052
  // src/cli-help.ts
59214
60053
  var COMMAND_HELP = {
@@ -59220,8 +60059,10 @@ Outputs context.json: intent, fields, evidence, catalog (blockedCharts), sampleW
59220
60059
  Options:
59221
60060
  --intent <text> Natural language description of the report goal
59222
60061
  --output <file> Write context.json to this path (default: stdout)
60062
+ --compact Output compact context for agent consumption
60063
+ --verbose Keep full context output explicitly for debugging
59223
60064
  --extra-query <expr> Custom query: "groupby=col;measure=sum(x) as y;filter=col>=val"
59224
- --correct-assumption <expr> Override an assumption: "primary_measure=orders"
60065
+ --correct-assumption <expr> Override an assumption: "primary_measure=orders", "primary_dimension=region", or "time_field=date"
59225
60066
  --sheet <name> Sheet name (Excel only)
59226
60067
  --limit <n> Max rows to read
59227
60068
  `,
@@ -59246,13 +60087,28 @@ Options:
59246
60087
  --profile <file> Path to profile JSON (output of "profile")
59247
60088
  --context <file> Path to context.json (output of "analyze") for catalog compliance and
59248
60089
  $evidence path checks
59249
- --strict Treat blockedChart violations as hard errors (requires --context)
60090
+ --strict With --verify, treat verify warnings as hard errors; also hard-fails blockedChart violations
59250
60091
  --verify Also check for forbidden words and missing sampleWarning caveats
59251
60092
  --patch-hints Attach machine-readable JSON Patch hints to fixable errors
59252
60093
  `,
59253
60094
  catalog: `Usage: miao-viz catalog
59254
60095
 
59255
60096
  List all available chart types and their required fields.
60097
+ `,
60098
+ block: `Usage: miao-viz block instantiate <block-id> --context <context.json> [--output <file>]
60099
+
60100
+ Instantiate a report block using full or compact analyze context.
60101
+ `,
60102
+ template: `Usage:
60103
+ miao-viz template list
60104
+ miao-viz template inspect <template-id>
60105
+ miao-viz template instantiate <template-id> --context <context.json> [--output <file>]
60106
+
60107
+ List, inspect, or instantiate report templates using full or compact analyze context.
60108
+ `,
60109
+ inspect: `Usage: miao-viz inspect --input <file> --spec <file> --context <context.json> --output <file>
60110
+
60111
+ Inspect chart transform pipelines and evidence usage for debugging.
59256
60112
  `,
59257
60113
  render: `Usage: miao-viz render --input <file> --spec <file> --output <file> [options]
59258
60114
 
@@ -59264,6 +60120,7 @@ Options:
59264
60120
  --output <file> Output file path
59265
60121
  --format <fmt> Output format: html, svg (default: html)
59266
60122
  --theme <name> Theme: default, editorial, dark, minimal
60123
+ --context <file> Path to context.json (output of "analyze") \u2014 resolves $evidence: directives in insights[]
59267
60124
  --interactive Force interactive runtime for HTML output
59268
60125
  --no-interactive Force static HTML output
59269
60126
  --sheet <name> Sheet name (Excel only)
@@ -59323,6 +60180,9 @@ Commands:
59323
60180
  query Run an aggregation query to get real computed values
59324
60181
  validate Validate a vizspec against a data profile
59325
60182
  catalog List all available chart types
60183
+ block Instantiate report blocks from analyze context
60184
+ template List, inspect, or instantiate report templates
60185
+ inspect Inspect chart transforms and evidence usage
59326
60186
  render Render a vizspec to HTML or SVG
59327
60187
  deck Render a deck spec to HTML slides
59328
60188
  article Convert a local article to an infographic artifact
@@ -59332,12 +60192,12 @@ Run "miao-viz <command> --help" for command-specific options.
59332
60192
  }
59333
60193
 
59334
60194
  // src/cli-block.ts
59335
- var YAML2 = __toESM(require_dist(), 1);
60195
+ var YAML3 = __toESM(require_dist(), 1);
59336
60196
 
59337
60197
  // src/cli-utils.ts
59338
60198
  var import_node_fs3 = require("node:fs");
59339
60199
  var import_node_path3 = require("node:path");
59340
- var YAML = __toESM(require_dist(), 1);
60200
+ var YAML2 = __toESM(require_dist(), 1);
59341
60201
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
59342
60202
  "h",
59343
60203
  "help",
@@ -59348,7 +60208,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
59348
60208
  "strict",
59349
60209
  "patch-hints",
59350
60210
  "verify",
59351
- "for-llm"
60211
+ "for-llm",
60212
+ "compact",
60213
+ "verbose"
59352
60214
  ]);
59353
60215
  function parseArgs(argv) {
59354
60216
  const [command, ...rest] = argv;
@@ -59410,7 +60272,7 @@ function printJson(value) {
59410
60272
  function readSpec(file2) {
59411
60273
  const text = (0, import_node_fs3.readFileSync)(file2, "utf8");
59412
60274
  if (file2.endsWith(".json")) return JSON.parse(text);
59413
- return YAML.parse(text);
60275
+ return YAML2.parse(text);
59414
60276
  }
59415
60277
  function readJson(file2) {
59416
60278
  return JSON.parse((0, import_node_fs3.readFileSync)(file2, "utf8"));
@@ -59443,6 +60305,11 @@ function runCatalog(args) {
59443
60305
  const catalog = {
59444
60306
  charts: CHART_CATALOG.map((c) => ({
59445
60307
  id: c.id,
60308
+ compactFor: c.compactFor,
60309
+ requires: c.requires,
60310
+ transformRecipe: c.transformRecipe,
60311
+ avoid: c.avoid,
60312
+ insightPattern: c.insightPattern,
59446
60313
  requiredEncodings: c.requiredEncodings,
59447
60314
  allowedTransforms: c.allowedTransforms,
59448
60315
  rules: c.rules.map((r) => r.expression),
@@ -59474,11 +60341,11 @@ function runBlock(args) {
59474
60341
  if (isAgentError(contextPath)) return fail(contextPath);
59475
60342
  const raw = readJson(contextPath);
59476
60343
  const unwrapped = raw.ok === true ? raw.value : raw;
59477
- const parsed = analyzeContextSchema.safeParse(unwrapped);
59478
- if (!parsed.success) {
59479
- return fail(agentError("INVALID_CONTEXT", `context.json format is invalid: ${parsed.error.issues.map((i) => i.message).join("; ")}`, { contextPath }));
60344
+ const parsed = parseAnalyzeContext(unwrapped);
60345
+ if (!parsed) {
60346
+ return fail(agentError("INVALID_CONTEXT", "context.json format is invalid.", { contextPath }));
59480
60347
  }
59481
- const ctx = parsed.data;
60348
+ const ctx = parsed;
59482
60349
  const resolver = getBlockById(blockId);
59483
60350
  if (!resolver) {
59484
60351
  return fail(agentError("UNKNOWN_BLOCK_ID", `Block id '${blockId}' not found. Available: ${BLOCK_REGISTRY.map((b) => b.id).join(", ")}`, { blockId, availableIds: BLOCK_REGISTRY.map((b) => b.id) }));
@@ -59505,7 +60372,7 @@ function runBlock(args) {
59505
60372
  }
59506
60373
  function buildBlockDraft(resolver, variables, score, compiled) {
59507
60374
  const varSummary = Object.entries(variables).map(([k, v]) => `${k}=${v}`).join(", ");
59508
- const yamlBody = YAML2.stringify({ charts: compiled.charts, insights: compiled.insights ?? [] });
60375
+ const yamlBody = YAML3.stringify({ charts: compiled.charts, insights: compiled.insights ?? [] });
59509
60376
  const checks = resolver.qualityChecks.map((c) => `# [ ] ${c}`).join("\n");
59510
60377
  return [
59511
60378
  `# Generated by: miao-viz block instantiate ${resolver.id}`,
@@ -59519,6 +60386,140 @@ function buildBlockDraft(resolver, variables, score, compiled) {
59519
60386
  ].join("\n");
59520
60387
  }
59521
60388
 
60389
+ // src/cli-template.ts
60390
+ function runTemplate(args) {
60391
+ const subcommand = args.positional[0];
60392
+ if (subcommand === "list") {
60393
+ return {
60394
+ ok: true,
60395
+ value: TEMPLATE_REGISTRY.map(templateInfo)
60396
+ };
60397
+ }
60398
+ if (subcommand === "inspect") {
60399
+ const templateId = args.positional[1];
60400
+ if (!templateId) return fail(agentError("MISSING_INPUT", "Usage: miao-viz template inspect <template-id>"));
60401
+ const template = getTemplateById(templateId);
60402
+ if (!template) return fail(agentError("UNKNOWN_TEMPLATE_ID", `Template id '${templateId}' not found.`, { availableIds: TEMPLATE_REGISTRY.map((t) => t.id) }));
60403
+ return { ok: true, value: templateInfo(template) };
60404
+ }
60405
+ if (subcommand === "instantiate") {
60406
+ return runTemplateInstantiate(args);
60407
+ }
60408
+ return fail(agentError("UNKNOWN_SUBCOMMAND", `Unknown template subcommand '${subcommand ?? "(none)"}'. Supported: list, inspect, instantiate`, { subcommand }));
60409
+ }
60410
+ function runTemplateInstantiate(args) {
60411
+ const templateId = args.positional[1];
60412
+ if (!templateId) {
60413
+ return fail(agentError("MISSING_INPUT", "Usage: miao-viz template instantiate <template-id> --context <context.json> [--output <file>]"));
60414
+ }
60415
+ const contextPath = requiredFlag(args, "context");
60416
+ if (isAgentError(contextPath)) return fail(contextPath);
60417
+ const ctx = parseAnalyzeContext(readJson(contextPath));
60418
+ if (!ctx) return fail(agentError("INVALID_CONTEXT", "context.json format is invalid.", { contextPath }));
60419
+ const template = getTemplateById(templateId);
60420
+ if (!template) {
60421
+ return fail(agentError("UNKNOWN_TEMPLATE_ID", `Template id '${templateId}' not found.`, { templateId, availableIds: TEMPLATE_REGISTRY.map((t) => t.id) }));
60422
+ }
60423
+ const matchCtx = {
60424
+ fields: ctx.fields,
60425
+ evidence: ctx.evidence,
60426
+ catalog: ctx.catalog,
60427
+ sampleWarnings: ctx.sampleWarnings
60428
+ };
60429
+ const decision = template.canUse(matchCtx);
60430
+ if (!decision.ok) {
60431
+ return fail(agentError("TEMPLATE_NOT_APPLICABLE", `Template '${templateId}' cannot be used: ${decision.reason}`, { templateId, reason: decision.reason }));
60432
+ }
60433
+ const spec = template.instantiate(matchCtx);
60434
+ const yaml = [
60435
+ `# Generated by: miao-viz template instantiate ${template.id}`,
60436
+ `# Template: ${template.id} (score: ${decision.score.toFixed(2)})`,
60437
+ "# IMPORTANT: Review chart order and fill structured insights[] before validate",
60438
+ "",
60439
+ templateSpecToYaml(spec)
60440
+ ].join("\n");
60441
+ const outputPath = stringFlag(args, "output");
60442
+ if (outputPath) {
60443
+ writeOutput(outputPath, yaml);
60444
+ return { ok: true, value: { output: outputPath, templateId, score: decision.score } };
60445
+ }
60446
+ return { ok: true, value: { yaml, templateId, score: decision.score } };
60447
+ }
60448
+
60449
+ // src/cli-inspect.ts
60450
+ function runInspect(args) {
60451
+ const input = requiredFlag(args, "input");
60452
+ const specPath = requiredFlag(args, "spec");
60453
+ const contextPath = requiredFlag(args, "context");
60454
+ const output = requiredFlag(args, "output");
60455
+ if (isAgentError(input)) return fail(input);
60456
+ if (isAgentError(specPath)) return fail(specPath);
60457
+ if (isAgentError(contextPath)) return fail(contextPath);
60458
+ if (isAgentError(output)) return fail(output);
60459
+ const dataset = loadDataset(input);
60460
+ if (isAgentError(dataset)) return fail(dataset);
60461
+ const spec = normalizeSpec(readSpec(specPath));
60462
+ if (isAgentError(spec)) return fail(spec);
60463
+ const context = parseAnalyzeContext(readJson(contextPath));
60464
+ if (!context) return fail(agentError("INVALID_CONTEXT", "context.json format is invalid.", { contextPath }));
60465
+ const profile = profileDataset(dataset.value);
60466
+ const charts = spec.charts.map((chart, index) => {
60467
+ const inspected = inspectChartTransforms(dataset.value.rows, chart);
60468
+ return {
60469
+ id: chart.id ?? `chart-${index + 1}`,
60470
+ type: chart.type,
60471
+ transforms: inspected.transforms,
60472
+ encoding: Object.fromEntries(Object.entries(chart.encoding ?? {}).map(([channel, enc]) => {
60473
+ const field = enc?.field;
60474
+ const profileField = field ? profile.columns.find((c) => c.name === field) : void 0;
60475
+ const finalMatch = field ? inspected.rows.some((row) => Object.prototype.hasOwnProperty.call(row, field)) : false;
60476
+ const sourceFieldMatch = Boolean(profileField);
60477
+ return [channel, {
60478
+ field,
60479
+ specType: enc?.type,
60480
+ resolvedType: profileField?.type ?? (finalMatch ? inferFinalFieldType(inspected.rows, field) : void 0),
60481
+ sourceFieldMatch,
60482
+ finalRowsMatch: finalMatch,
60483
+ match: sourceFieldMatch || finalMatch
60484
+ }];
60485
+ }))
60486
+ };
60487
+ });
60488
+ function inferFinalFieldType(rows, field) {
60489
+ if (!field) return void 0;
60490
+ const values = rows.map((row) => row[field]).filter((value) => value !== null && value !== void 0);
60491
+ if (values.length === 0) return void 0;
60492
+ if (values.every((value) => typeof value === "number")) return "number";
60493
+ if (values.every((value) => typeof value === "boolean")) return "boolean";
60494
+ if (values.every((value) => value instanceof Date)) return "date";
60495
+ return "string";
60496
+ }
60497
+ const referenced = /* @__PURE__ */ new Set();
60498
+ for (const insight of normalizeInsights(spec.insights)) {
60499
+ for (const id of insight.evidence) referenced.add(id);
60500
+ for (const ref of parseEvidenceRefs(insight.text)) referenced.add(ref.id);
60501
+ }
60502
+ for (const chart of spec.charts) {
60503
+ if (!chart.title) continue;
60504
+ for (const ref of parseEvidenceRefs(chart.title)) referenced.add(ref.id);
60505
+ }
60506
+ const defined = context.evidence.map((e) => e.id);
60507
+ const result = {
60508
+ ok: true,
60509
+ value: {
60510
+ charts,
60511
+ evidence: {
60512
+ defined,
60513
+ referenced: [...referenced],
60514
+ unreferenced: defined.filter((id) => !referenced.has(id))
60515
+ }
60516
+ }
60517
+ };
60518
+ writeOutput(output, `${JSON.stringify(result.value, null, 2)}
60519
+ `);
60520
+ return result;
60521
+ }
60522
+
59522
60523
  // src/cli.ts
59523
60524
  async function main() {
59524
60525
  const args = parseArgs(process.argv.slice(2));
@@ -59547,6 +60548,14 @@ async function main() {
59547
60548
  printJson(runBlock(args));
59548
60549
  return;
59549
60550
  }
60551
+ if (args.command === "template") {
60552
+ printJson(runTemplate(args));
60553
+ return;
60554
+ }
60555
+ if (args.command === "inspect") {
60556
+ printJson(runInspect(args));
60557
+ return;
60558
+ }
59550
60559
  if (args.command === "render") {
59551
60560
  printJson(runRender(args));
59552
60561
  return;
@@ -59568,7 +60577,7 @@ async function main() {
59568
60577
  return;
59569
60578
  }
59570
60579
  printJson(agentError("UNKNOWN_COMMAND", `Unknown command: ${args.command ?? "(none)"}`, {
59571
- commands: ["profile", "validate", "catalog", "block", "render", "deck", "article", "query", "analyze"]
60580
+ commands: ["profile", "validate", "catalog", "block", "template", "inspect", "render", "deck", "article", "query", "analyze"]
59572
60581
  }));
59573
60582
  process.exitCode = 1;
59574
60583
  } catch (error51) {
@@ -59613,15 +60622,15 @@ function runValidate(args) {
59613
60622
  if (contextPath) {
59614
60623
  const raw = readJson(contextPath);
59615
60624
  const unwrapped = raw.ok === true ? raw.value : raw;
59616
- const parsed = analyzeContextSchema.safeParse(unwrapped);
59617
- if (!parsed.success) {
60625
+ const parsed = parseAnalyzeContext(unwrapped);
60626
+ if (!parsed) {
59618
60627
  return fail(agentError(
59619
60628
  "INVALID_CONTEXT",
59620
- `context.json format is invalid: ${parsed.error.issues.map((i) => i.message).join("; ")}`,
60629
+ "context.json format is invalid.",
59621
60630
  { contextPath }
59622
60631
  ));
59623
60632
  }
59624
- context = parsed.data;
60633
+ context = parsed;
59625
60634
  }
59626
60635
  const warnings = collectValidationWarnings(result.value, profile, context);
59627
60636
  if (args.flags["strict"] === true && context) {
@@ -59653,6 +60662,10 @@ function runValidate(args) {
59653
60662
  if (args.flags["verify"] === true) {
59654
60663
  const verifyWarnings = collectVerifyWarnings(result.value, context);
59655
60664
  warnings.push(...verifyWarnings);
60665
+ if (args.flags["strict"] === true) {
60666
+ const strictResult = strictVerifyError(verifyWarnings);
60667
+ if (isAgentError(strictResult)) return fail(strictResult);
60668
+ }
59656
60669
  }
59657
60670
  if (args.flags["patch-hints"] === true) {
59658
60671
  const warningPatches = collectWarningPatches(result.value);
@@ -59682,6 +60695,17 @@ function runRender(args) {
59682
60695
  if (isAgentError(normalized)) return fail(normalized);
59683
60696
  const validation = validateReportSpec(normalized, profile, formats);
59684
60697
  if (isAgentError(validation)) return fail(validation);
60698
+ const contextPath = stringFlag(args, "context");
60699
+ if (contextPath && validation.value.insights && validation.value.insights.length > 0) {
60700
+ const raw = readJson(contextPath);
60701
+ const unwrapped = raw.ok === true ? raw.value : raw;
60702
+ const parsed = parseAnalyzeContext(unwrapped);
60703
+ if (parsed) {
60704
+ validation.value.insights = validation.value.insights.map(
60705
+ (insight) => mapInsightText(insight, (text) => resolveDirectives(text, parsed.evidence))
60706
+ );
60707
+ }
60708
+ }
59685
60709
  const themeFlag = stringFlag(args, "theme");
59686
60710
  const interactive = args.flags["interactive"] === true ? true : args.flags["no-interactive"] === true ? false : void 0;
59687
60711
  const written = [];
@@ -59762,7 +60786,8 @@ async function runAnalyze(args) {
59762
60786
  extraQuery: stringFlag(args, "extra-query"),
59763
60787
  correctAssumption: stringFlag(args, "correct-assumption")
59764
60788
  });
59765
- const result = { ok: true, value: context };
60789
+ const value = args.flags["compact"] === true ? toCompactAnalyzeContext(context) : context;
60790
+ const result = { ok: true, value };
59766
60791
  const outputPath = stringFlag(args, "output");
59767
60792
  if (outputPath) {
59768
60793
  writeOutput(outputPath, `${JSON.stringify(result, null, 2)}