@miao-vision/cli 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -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
  }
@@ -41482,7 +41719,19 @@ var defaultTheme = {
41482
41719
  labelColor: "#475569"
41483
41720
  },
41484
41721
  css: `
41485
- :root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; }
41722
+ :root {
41723
+ color-scheme: light;
41724
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
41725
+ --mv-paper: #f8fafc;
41726
+ --mv-surface: #ffffff;
41727
+ --mv-border: #e2e8f0;
41728
+ --mv-ink: #0f172a;
41729
+ --mv-muted: #64748b;
41730
+ --mv-soft: #475569;
41731
+ --mv-brand: #2563eb;
41732
+ --mv-serif: Georgia, "Times New Roman", serif;
41733
+ --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
41734
+ }
41486
41735
  body { margin: 0; background: #f8fafc; color: #0f172a; }
41487
41736
  .miao-viz-report { max-width: 1120px; margin: 0 auto; padding: 40px 24px 56px; }
41488
41737
  header { margin-bottom: 28px; }
@@ -41523,6 +41772,7 @@ var editorialTheme = {
41523
41772
  --mv-ink: #141413;
41524
41773
  --mv-muted: #6b6a64;
41525
41774
  --mv-soft-text: #504e49;
41775
+ --mv-soft: #504e49;
41526
41776
  --mv-brand: #1b365d;
41527
41777
  --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
41528
41778
  --mv-serif: Charter, Georgia, "Times New Roman", serif;
@@ -41587,6 +41837,7 @@ var darkTheme = {
41587
41837
  --mv-ink: #e2e0d8;
41588
41838
  --mv-muted: #737069;
41589
41839
  --mv-soft-text: #a8a69f;
41840
+ --mv-soft: #a8a69f;
41590
41841
  --mv-brand: #7eb8f7;
41591
41842
  --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
41592
41843
  --mv-serif: Charter, Georgia, "Times New Roman", serif;
@@ -41644,7 +41895,19 @@ var minimalTheme = {
41644
41895
  labelColor: "#6b7280"
41645
41896
  },
41646
41897
  css: `
41647
- :root { color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
41898
+ :root {
41899
+ color-scheme: light;
41900
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
41901
+ --mv-paper: #ffffff;
41902
+ --mv-surface: #ffffff;
41903
+ --mv-border: #e5e7eb;
41904
+ --mv-ink: #111111;
41905
+ --mv-muted: #6b7280;
41906
+ --mv-soft: #374151;
41907
+ --mv-brand: #1d4ed8;
41908
+ --mv-serif: Georgia, "Times New Roman", serif;
41909
+ --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
41910
+ }
41648
41911
  body { margin: 0; background: #fff; color: #111; }
41649
41912
  .miao-viz-report { max-width: 960px; margin: 0 auto; padding: 48px 24px 64px; }
41650
41913
  header { margin-bottom: 32px; }
@@ -42117,12 +42380,38 @@ function escapeScriptJson(value) {
42117
42380
  return JSON.stringify(value).replace(/</g, "\\u003c");
42118
42381
  }
42119
42382
 
42383
+ // src/insight-utils.ts
42384
+ function normalizeInsight(insight) {
42385
+ if (typeof insight === "string") {
42386
+ return { text: insight, evidence: [], original: insight };
42387
+ }
42388
+ return {
42389
+ text: insight.text,
42390
+ evidence: insight.evidence ?? [],
42391
+ caveat: insight.caveat,
42392
+ severity: insight.severity,
42393
+ original: insight
42394
+ };
42395
+ }
42396
+ function normalizeInsights(insights) {
42397
+ return (insights ?? []).map(normalizeInsight);
42398
+ }
42399
+ function mapInsightText(insight, mapText) {
42400
+ if (typeof insight === "string") return mapText(insight);
42401
+ return { ...insight, text: mapText(insight.text) };
42402
+ }
42403
+ function insightPreview(text) {
42404
+ return `"${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"`;
42405
+ }
42406
+
42120
42407
  // src/html-export.ts
42121
42408
  var INSIGHTS_CSS = `
42122
42409
  .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
42410
  .insights-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.45; margin: 0 0 8px; }
42124
42411
  .insights-list { margin: 0; padding: 0 0 0 18px; }
42125
42412
  .insights-list li { margin: 5px 0; font-size: 13px; line-height: 1.55; opacity: 0.75; }
42413
+ .insight-warning { color: #8a4b00; }
42414
+ .insight-caveat { display: block; margin-top: 2px; font-size: 11px; opacity: 0.58; }
42126
42415
  `;
42127
42416
  function renderStaticHtml(spec, profile, rows, themeOverride, interactiveOptions = {}) {
42128
42417
  const theme = getTheme(themeOverride ?? spec.theme);
@@ -42229,7 +42518,11 @@ function chartIdFor(chart, index) {
42229
42518
  return chart.id ?? `chart-${index + 1}`;
42230
42519
  }
42231
42520
  function renderInsights(insights) {
42232
- const items = insights.map((s) => `<li>${escapeHtml(s)}</li>`).join("\n ");
42521
+ const items = normalizeInsights(insights).map((insight) => {
42522
+ const className = insight.severity === "warning" ? ' class="insight-warning"' : "";
42523
+ const caveat = insight.caveat ? `<span class="insight-caveat">${escapeHtml(insight.caveat)}</span>` : "";
42524
+ return `<li${className}>${escapeHtml(insight.text)}${caveat}</li>`;
42525
+ }).join("\n ");
42233
42526
  return `<section class="report-insights">
42234
42527
  <p class="insights-label">Key Observations</p>
42235
42528
  <ul class="insights-list">
@@ -42239,9 +42532,9 @@ function renderInsights(insights) {
42239
42532
  }
42240
42533
  function buildCaption(chart) {
42241
42534
  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}`);
42535
+ if (chart.encoding?.x?.field) parts.push(`x: ${chart.encoding.x.field}`);
42536
+ if (chart.encoding?.y?.field) parts.push(`y: ${chart.encoding.y.field}`);
42537
+ if (chart.encoding?.value?.field) parts.push(`value: ${chart.encoding.value.field}`);
42245
42538
  const transforms = chart.data?.transform ?? [];
42246
42539
  const agg = transforms.find((t) => t.type === "aggregate");
42247
42540
  if (agg?.groupBy?.length) parts.push(`grouped by ${agg.groupBy.join(", ")}`);
@@ -56806,6 +57099,15 @@ var chartInteractionSchema = external_exports.object({
56806
57099
  tooltip: external_exports.boolean().optional(),
56807
57100
  select: external_exports.enum(["filter", "detail"]).optional()
56808
57101
  });
57102
+ var insightSchema = external_exports.union([
57103
+ external_exports.string(),
57104
+ external_exports.object({
57105
+ text: external_exports.string().min(1),
57106
+ evidence: external_exports.array(external_exports.string().min(1)).optional(),
57107
+ caveat: external_exports.string().optional(),
57108
+ severity: external_exports.enum(["info", "warning"]).optional()
57109
+ })
57110
+ ]);
56809
57111
  var chartSpecSchema = external_exports.object({
56810
57112
  id: external_exports.string().min(1).optional(),
56811
57113
  type: external_exports.enum(MVP_CHART_TYPES),
@@ -56833,7 +57135,7 @@ var reportSpecSchema = external_exports.object({
56833
57135
  interactions: external_exports.object({
56834
57136
  globalFilters: external_exports.array(globalFilterSchema).optional()
56835
57137
  }).optional(),
56836
- insights: external_exports.array(external_exports.string()).optional(),
57138
+ insights: external_exports.array(insightSchema).optional(),
56837
57139
  charts: external_exports.array(chartSpecSchema).min(1)
56838
57140
  });
56839
57141
  var singleOrReportSpecSchema = external_exports.union([
@@ -56889,12 +57191,23 @@ function resolveEvidencePath(evidence, id, path) {
56889
57191
  }
56890
57192
  return { found: false, value: void 0 };
56891
57193
  }
57194
+ function resolveDirectives(text, evidence) {
57195
+ return text.replace(new RegExp(EVIDENCE_RE.source, "g"), (_, id, path) => {
57196
+ const { found, value } = resolveEvidencePath(evidence, id, path);
57197
+ return found ? String(value) : `[?${id}.${path}]`;
57198
+ });
57199
+ }
56892
57200
 
56893
57201
  // src/chart-catalog.ts
56894
57202
  var CHART_CATALOG = [
56895
57203
  {
56896
57204
  id: "bar",
56897
57205
  displayName: "Bar Chart",
57206
+ compactFor: "rank,compare",
57207
+ requires: "dim(2-30)+measure",
57208
+ transformRecipe: "agg>sort(desc)>limit(10)",
57209
+ avoid: "dim>30,time>=3",
57210
+ insightPattern: "top {dimension} by {measure}",
56898
57211
  requiredEncodings: ["x", "y"],
56899
57212
  allowedTransforms: ["aggregate", "sort", "limit", "derive-month"],
56900
57213
  bestFor: ["ranking by category", "comparison across dimensions", "top-N with limit"],
@@ -56917,6 +57230,25 @@ var CHART_CATALOG = [
56917
57230
  return null;
56918
57231
  }
56919
57232
  },
57233
+ {
57234
+ code: "BAR_NO_AGGREGATE",
57235
+ severity: "warning",
57236
+ expression: "encoding.y.aggregate not set and no aggregate transform",
57237
+ message: "bar will plot one bar per raw row \u2014 unsorted and unaggregated. Add encoding.y.aggregate or data.transform aggregate + sort + limit.",
57238
+ validate: (chart) => {
57239
+ const hasAggTransform = (chart.data?.transform ?? []).some((t) => t.type === "aggregate");
57240
+ const hasEncodingAgg = !!chart.encoding?.y?.aggregate;
57241
+ if (!hasAggTransform && !hasEncodingAgg) {
57242
+ return {
57243
+ code: "BAR_NO_AGGREGATE",
57244
+ severity: "warning",
57245
+ 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.`,
57246
+ chartId: chart.id
57247
+ };
57248
+ }
57249
+ return null;
57250
+ }
57251
+ },
56920
57252
  {
56921
57253
  code: "TOO_MANY_CATEGORIES",
56922
57254
  severity: "warning",
@@ -56943,6 +57275,11 @@ var CHART_CATALOG = [
56943
57275
  {
56944
57276
  id: "line",
56945
57277
  displayName: "Line Chart",
57278
+ compactFor: "trend",
57279
+ requires: "time(>=3)+measure",
57280
+ transformRecipe: "agg(time)>sort(asc)",
57281
+ avoid: "time<3,nominal_x",
57282
+ insightPattern: "{measure} over {time}",
56946
57283
  requiredEncodings: ["x", "y"],
56947
57284
  allowedTransforms: ["aggregate", "sort", "derive-month"],
56948
57285
  bestFor: ["time series trends", "continuous data over ordered axis"],
@@ -56992,6 +57329,10 @@ var CHART_CATALOG = [
56992
57329
  {
56993
57330
  id: "area",
56994
57331
  displayName: "Area Chart",
57332
+ compactFor: "trend,magnitude",
57333
+ requires: "time(>=3)+measure",
57334
+ transformRecipe: "agg(time)>sort(asc)",
57335
+ avoid: "time<3,negative_values,nominal_x",
56995
57336
  requiredEncodings: ["x", "y"],
56996
57337
  allowedTransforms: ["aggregate", "sort", "derive-month"],
56997
57338
  bestFor: ["cumulative trends", "filled time series with visual mass"],
@@ -57041,11 +57382,35 @@ var CHART_CATALOG = [
57041
57382
  {
57042
57383
  id: "pie",
57043
57384
  displayName: "Pie Chart",
57385
+ compactFor: "share,composition",
57386
+ requires: "dim(2-7)+measure",
57387
+ transformRecipe: "agg>sort(desc)>limit(7)",
57388
+ avoid: "dim>7,non_whole_values",
57389
+ insightPattern: "{dimension} share of {measure}",
57044
57390
  requiredEncodings: ["label", "value"],
57045
57391
  allowedTransforms: ["aggregate", "sort", "limit"],
57046
57392
  bestFor: ["part-to-whole proportions", "share distribution with \u22647 categories"],
57047
57393
  antiPatterns: ["more than 7 slices (use bar)", "values that do not sum to a meaningful whole"],
57048
57394
  rules: [
57395
+ {
57396
+ code: "PIE_NO_AGGREGATE",
57397
+ severity: "warning",
57398
+ expression: "encoding.value.aggregate not set and no aggregate transform",
57399
+ message: "pie will show one slice per raw row. Add encoding.value.aggregate or data.transform aggregate + sort + limit.",
57400
+ validate: (chart) => {
57401
+ const hasAggTransform = (chart.data?.transform ?? []).some((t) => t.type === "aggregate");
57402
+ const hasEncodingAgg = !!chart.encoding?.value?.aggregate;
57403
+ if (!hasAggTransform && !hasEncodingAgg) {
57404
+ return {
57405
+ code: "PIE_NO_AGGREGATE",
57406
+ severity: "warning",
57407
+ 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.`,
57408
+ chartId: chart.id
57409
+ };
57410
+ }
57411
+ return null;
57412
+ }
57413
+ },
57049
57414
  {
57050
57415
  code: "TOO_MANY_SLICES",
57051
57416
  severity: "warning",
@@ -57072,6 +57437,10 @@ var CHART_CATALOG = [
57072
57437
  {
57073
57438
  id: "scatter",
57074
57439
  displayName: "Scatter Chart",
57440
+ compactFor: "relationship,correlation",
57441
+ requires: "measure+measure",
57442
+ transformRecipe: "raw_or_limit",
57443
+ avoid: "single_measure,categorical_axis",
57075
57444
  requiredEncodings: ["x", "y"],
57076
57445
  allowedTransforms: ["sort", "limit"],
57077
57446
  bestFor: ["correlation between two measures", "distribution of two quantitative variables"],
@@ -57081,6 +57450,11 @@ var CHART_CATALOG = [
57081
57450
  {
57082
57451
  id: "histogram",
57083
57452
  displayName: "Histogram",
57453
+ compactFor: "distribution",
57454
+ requires: "measure+rows(>=20)",
57455
+ transformRecipe: "bin(numeric)>count",
57456
+ avoid: "rows<20,categorical_field",
57457
+ insightPattern: "{measure} distribution",
57084
57458
  requiredEncodings: ["x"],
57085
57459
  allowedTransforms: ["derive-month"],
57086
57460
  bestFor: ["distribution of a single numeric field", "frequency across value bins"],
@@ -57091,6 +57465,11 @@ var CHART_CATALOG = [
57091
57465
  {
57092
57466
  id: "heatmap",
57093
57467
  displayName: "Heatmap",
57468
+ compactFor: "matrix,density",
57469
+ requires: "dim+dim+measure",
57470
+ transformRecipe: "agg(dim,dim)>encode(value)",
57471
+ avoid: "single_dimension,sparse_matrix",
57472
+ insightPattern: "{measure} by {x_dimension} and {y_dimension}",
57094
57473
  requiredEncodings: ["x", "y", "value"],
57095
57474
  allowedTransforms: ["aggregate"],
57096
57475
  bestFor: ["matrix of two dimensions vs one measure", "calendar-style density maps"],
@@ -57100,6 +57479,10 @@ var CHART_CATALOG = [
57100
57479
  {
57101
57480
  id: "table",
57102
57481
  displayName: "Data Table",
57482
+ compactFor: "detail,high-cardinality,export",
57483
+ requires: "any_fields",
57484
+ transformRecipe: "sort_or_limit_optional",
57485
+ avoid: "too_many_columns_without_selection",
57103
57486
  requiredEncodings: [],
57104
57487
  allowedTransforms: ["aggregate", "sort", "limit", "derive-month"],
57105
57488
  bestFor: ["high-cardinality dimension detail", "multi-measure comparison", "data export"],
@@ -57109,17 +57492,60 @@ var CHART_CATALOG = [
57109
57492
  {
57110
57493
  id: "bigvalue",
57111
57494
  displayName: "Big Value (KPI Card)",
57495
+ compactFor: "kpi,summary",
57496
+ requires: "measure",
57497
+ transformRecipe: "agg(measure)>single_value",
57498
+ avoid: "raw_row_value,too_many_cards",
57499
+ insightPattern: "total {measure}",
57112
57500
  requiredEncodings: ["value"],
57113
57501
  allowedTransforms: ["aggregate"],
57114
57502
  bestFor: ["single top-level KPI", "summary metric with optional delta"],
57115
57503
  antiPatterns: ["more than 4 bigvalue cards per report (use kpigrid)", "showing a dimension value without a measure"],
57116
- rules: []
57504
+ rules: [
57505
+ {
57506
+ code: "BIGVALUE_NO_REDUCTION",
57507
+ severity: "warning",
57508
+ expression: "no aggregate transform and encoding.value.aggregate not set",
57509
+ message: "bigvalue will show rows[0] raw value. Add encoding.value.aggregate or data.transform aggregate + limit: 1.",
57510
+ validate: (chart) => {
57511
+ const hasAggTransform = (chart.data?.transform ?? []).some((t) => t.type === "aggregate");
57512
+ const hasEncodingAgg = !!chart.encoding?.value?.aggregate;
57513
+ if (!hasAggTransform && !hasEncodingAgg) {
57514
+ const field = chart.encoding?.value?.field ?? "value";
57515
+ return {
57516
+ code: "BIGVALUE_NO_REDUCTION",
57517
+ severity: "warning",
57518
+ 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.`,
57519
+ chartId: chart.id
57520
+ };
57521
+ }
57522
+ return null;
57523
+ }
57524
+ }
57525
+ ]
57117
57526
  }
57118
57527
  ];
57119
57528
  function getCatalogItem(chartType) {
57120
57529
  return CHART_CATALOG.find((c) => c.id === chartType);
57121
57530
  }
57122
57531
 
57532
+ // src/spec-utils.ts
57533
+ function findChartIndex(spec, chartId) {
57534
+ if (!chartId) return spec.charts.length === 1 ? 0 : -1;
57535
+ const byId = spec.charts.findIndex((c) => c.id === chartId);
57536
+ if (byId >= 0) return byId;
57537
+ return spec.charts.findIndex((c) => c.type === chartId);
57538
+ }
57539
+ function findLastChartIndexById(spec, id) {
57540
+ for (let i = spec.charts.length - 1; i >= 0; i--) {
57541
+ if (spec.charts[i].id === id) return i;
57542
+ }
57543
+ return -1;
57544
+ }
57545
+ function countChartsByType(spec, type) {
57546
+ return spec.charts.filter((c) => c.type === type).length;
57547
+ }
57548
+
57123
57549
  // src/spec-validator.ts
57124
57550
  var FORBIDDEN_WORDS = [
57125
57551
  { pattern: /\b(trend|趋势)\b/i, word: "trend/\u8D8B\u52BF" },
@@ -57174,6 +57600,8 @@ function validateReportSpec(spec, profile, formats = ["html"], context) {
57174
57600
  });
57175
57601
  }
57176
57602
  }
57603
+ const finalSchemaResult = validateEncodingFieldsInFinalSchema(chart);
57604
+ if (isAgentError(finalSchemaResult)) return finalSchemaResult;
57177
57605
  }
57178
57606
  return ok(parsed.data);
57179
57607
  }
@@ -57195,7 +57623,7 @@ function collectValidationWarnings(spec, profile, context) {
57195
57623
  `Report has ${spec.charts.length} charts (>6). Consider splitting into multiple sections or removing low-value charts.`
57196
57624
  );
57197
57625
  }
57198
- const bigvalueCount = spec.charts.filter((c) => c.type === "bigvalue").length;
57626
+ const bigvalueCount = countChartsByType(spec, "bigvalue");
57199
57627
  if (bigvalueCount > 4) {
57200
57628
  warnings.push(
57201
57629
  `Report has ${bigvalueCount} bigvalue cards (>4). Use kpigrid for 5+ KPI cards to avoid visual clutter.`
@@ -57235,8 +57663,20 @@ function collectValidationWarnings(spec, profile, context) {
57235
57663
  return warnings;
57236
57664
  }
57237
57665
  function validateEvidencePaths(spec, context) {
57666
+ const availableIds = context.evidence.map((e) => e.id);
57667
+ for (const insight of normalizeInsights(spec.insights)) {
57668
+ for (const evidenceId of insight.evidence) {
57669
+ if (!availableIds.includes(evidenceId)) {
57670
+ return agentError(
57671
+ "INSIGHT_EVIDENCE_NOT_FOUND",
57672
+ `insight.evidence id '${evidenceId}' not found in context.evidence. Available ids: ${availableIds.join(", ") || "(none)"}`,
57673
+ { evidenceId, availableIds }
57674
+ );
57675
+ }
57676
+ }
57677
+ }
57238
57678
  const texts = [
57239
- ...spec.insights ?? [],
57679
+ ...normalizeInsights(spec.insights).map((insight) => insight.text),
57240
57680
  ...spec.charts.map((c) => c.title).filter((t) => Boolean(t))
57241
57681
  ];
57242
57682
  for (const text of texts) {
@@ -57255,22 +57695,29 @@ function validateEvidencePaths(spec, context) {
57255
57695
  }
57256
57696
  function collectVerifyWarnings(spec, context) {
57257
57697
  const warnings = [];
57258
- const insights = spec.insights ?? [];
57259
- for (const text of insights) {
57698
+ const insights = normalizeInsights(spec.insights);
57699
+ for (const insight of insights) {
57260
57700
  for (const { pattern, word } of FORBIDDEN_WORDS) {
57261
- if (pattern.test(text)) {
57701
+ if (pattern.test(insight.text)) {
57262
57702
  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[]`
57703
+ `insight contains forbidden word '${word}': ${insightPreview(insight.text)} \u2014 use only when backed by statistical evidence in context.evidence[]`
57264
57704
  );
57265
57705
  }
57266
57706
  }
57707
+ if (typeof insight.original !== "string" && /\d/.test(insight.text) && insight.evidence.length === 0) {
57708
+ warnings.push(
57709
+ `insight contains numeric claim without structured evidence: ${insightPreview(insight.text)}`
57710
+ );
57711
+ }
57267
57712
  }
57268
57713
  if (context?.sampleWarnings.length) {
57269
57714
  const CAVEAT_PATTERNS = [
57270
57715
  /仅供参考|样本量|有限数据|based on.*rows?|N-row sample|limited data|small sample/i,
57271
57716
  /环比变化|period.over.period/i
57272
57717
  ];
57273
- const hasCaveat = insights.some((text) => CAVEAT_PATTERNS.some((p) => p.test(text)));
57718
+ const hasCaveat = insights.some(
57719
+ (insight) => Boolean(insight.caveat) || CAVEAT_PATTERNS.some((p) => p.test(insight.text))
57720
+ );
57274
57721
  if (insights.length > 0 && !hasCaveat) {
57275
57722
  const codes = context.sampleWarnings.map((w) => w.code).join(", ");
57276
57723
  warnings.push(
@@ -57280,6 +57727,24 @@ function collectVerifyWarnings(spec, context) {
57280
57727
  }
57281
57728
  return warnings;
57282
57729
  }
57730
+ function strictVerifyError(warnings) {
57731
+ if (warnings.length === 0) return ok(void 0);
57732
+ const issues = warnings.map(warningToVerifyIssue);
57733
+ return agentError(
57734
+ "STRICT_VERIFY_FAILED",
57735
+ `Strict verify failed with ${warnings.length} warning(s).`,
57736
+ { warnings, issues }
57737
+ );
57738
+ }
57739
+ function warningToVerifyIssue(message) {
57740
+ if (message.includes("forbidden word")) {
57741
+ return { code: "INSIGHT_FORBIDDEN_WORD_STRICT", message };
57742
+ }
57743
+ if (message.includes("numeric claim without structured evidence")) {
57744
+ return { code: "INSIGHT_NUMERIC_CLAIM_WITHOUT_EVIDENCE_STRICT", message };
57745
+ }
57746
+ return { code: "INSIGHT_MISSING_CAVEAT_STRICT", message };
57747
+ }
57283
57748
  function validateChartType(chart) {
57284
57749
  if (!MVP_CHART_TYPES.includes(chart.type)) {
57285
57750
  return agentError("UNSUPPORTED_CHART_TYPE", `Chart type '${chart.type}' is not supported in the MVP.`, {
@@ -57365,6 +57830,38 @@ function validateTransforms(chart) {
57365
57830
  }
57366
57831
  return ok(chart);
57367
57832
  }
57833
+ function simulateFinalSchema(chart) {
57834
+ let schema = null;
57835
+ for (const transform2 of chart.data?.transform ?? []) {
57836
+ if (transform2.type === "aggregate") {
57837
+ const next = /* @__PURE__ */ new Set();
57838
+ for (const field of transform2.groupBy ?? []) next.add(field);
57839
+ for (const measure of transform2.measures ?? []) next.add(measure.as);
57840
+ schema = next;
57841
+ } else if (transform2.type === "derive-month" && transform2.as && schema !== null) {
57842
+ schema.add(transform2.as);
57843
+ }
57844
+ }
57845
+ return schema;
57846
+ }
57847
+ function validateEncodingFieldsInFinalSchema(chart) {
57848
+ const finalSchema = simulateFinalSchema(chart);
57849
+ if (!finalSchema) return ok(chart);
57850
+ const chartLabel = chart.id ? `chart '${chart.id}'` : `${chart.type} chart`;
57851
+ const available = [...finalSchema].join(", ") || "(none)";
57852
+ for (const [channel, encoding] of Object.entries(chart.encoding ?? {})) {
57853
+ const field = encoding?.field;
57854
+ if (!field) continue;
57855
+ if (!finalSchema.has(field)) {
57856
+ return agentError(
57857
+ "ENCODING_FIELD_NOT_IN_FINAL_ROWS",
57858
+ `${chartLabel}: encoding.${channel}.field '${field}' does not exist in rows after transforms. Available fields after transforms: ${available}`,
57859
+ { chartId: chart.id, channel, field, availableAfterTransforms: [...finalSchema] }
57860
+ );
57861
+ }
57862
+ }
57863
+ return ok(chart);
57864
+ }
57368
57865
  function collectSourceFields(chart, derivedFields) {
57369
57866
  const fields = /* @__PURE__ */ new Set();
57370
57867
  for (const encoding of Object.values(chart.encoding ?? {})) {
@@ -57428,6 +57925,18 @@ var blockedBlockEntrySchema = external_exports.object({
57428
57925
  id: external_exports.string().min(1),
57429
57926
  reason: external_exports.string().min(1)
57430
57927
  });
57928
+ var catalogTemplateEntrySchema = external_exports.object({
57929
+ id: external_exports.string().min(1),
57930
+ score: external_exports.number().min(0).max(1),
57931
+ bestFor: external_exports.array(external_exports.string()),
57932
+ requires: external_exports.array(external_exports.enum(["measure", "dimension", "time"])),
57933
+ blocks: external_exports.array(external_exports.string()),
57934
+ density: external_exports.enum(["compact", "medium", "full"])
57935
+ });
57936
+ var blockedTemplateEntrySchema = external_exports.object({
57937
+ id: external_exports.string().min(1),
57938
+ reason: external_exports.string().min(1)
57939
+ });
57431
57940
  var analyzeCatalogSchema = external_exports.object({
57432
57941
  charts: external_exports.array(external_exports.string().min(1)),
57433
57942
  blockedCharts: external_exports.array(external_exports.object({
@@ -57439,7 +57948,9 @@ var analyzeCatalogSchema = external_exports.object({
57439
57948
  note: external_exports.string().optional()
57440
57949
  })),
57441
57950
  blocks: external_exports.array(catalogBlockEntrySchema).optional(),
57442
- blockedBlocks: external_exports.array(blockedBlockEntrySchema).optional()
57951
+ blockedBlocks: external_exports.array(blockedBlockEntrySchema).optional(),
57952
+ templates: external_exports.array(catalogTemplateEntrySchema).optional(),
57953
+ blockedTemplates: external_exports.array(blockedTemplateEntrySchema).optional()
57443
57954
  });
57444
57955
  var metricCandidateSchema = external_exports.object({
57445
57956
  id: external_exports.string().min(1),
@@ -57450,15 +57961,35 @@ var metricCandidateSchema = external_exports.object({
57450
57961
  confidence: external_exports.enum(["high", "medium"]),
57451
57962
  caveat: external_exports.string().optional()
57452
57963
  });
57964
+ var analyzeAssumptionSchema = external_exports.object({
57965
+ key: external_exports.enum(["primary_measure", "primary_dimension", "time_field"]),
57966
+ value: external_exports.string(),
57967
+ confidence: external_exports.number().min(0).max(1),
57968
+ alternatives: external_exports.array(external_exports.string()).optional(),
57969
+ reason: external_exports.string().optional()
57970
+ });
57971
+ var legacyAssumptionSchema = external_exports.string().transform((value) => ({
57972
+ key: value.includes("dimension") ? "primary_dimension" : value.includes("time") ? "time_field" : "primary_measure",
57973
+ value,
57974
+ confidence: 0.5,
57975
+ reason: "legacy string assumption"
57976
+ }));
57453
57977
  var analyzeIntentSchema = external_exports.object({
57454
57978
  raw: external_exports.string().min(1),
57455
57979
  coverage: external_exports.enum(["full", "partial"]),
57456
- assumptions: external_exports.array(external_exports.string())
57980
+ assumptions: external_exports.array(external_exports.union([analyzeAssumptionSchema, legacyAssumptionSchema]))
57457
57981
  });
57458
57982
  var analyzeSampleWarningSchema = external_exports.object({
57459
57983
  code: external_exports.string().min(1),
57460
57984
  message: external_exports.string().min(1)
57461
57985
  });
57986
+ var clarificationQuestionSchema = external_exports.object({
57987
+ id: external_exports.string().min(1),
57988
+ question: external_exports.string().min(1),
57989
+ options: external_exports.array(external_exports.string()),
57990
+ blocking: external_exports.boolean(),
57991
+ appliesTo: external_exports.enum(["measure", "dimension", "time", "template"])
57992
+ });
57462
57993
  var analyzeContextSchema = external_exports.object({
57463
57994
  intent: analyzeIntentSchema,
57464
57995
  fields: external_exports.array(analyzeFieldSchema),
@@ -57466,13 +57997,199 @@ var analyzeContextSchema = external_exports.object({
57466
57997
  catalog: analyzeCatalogSchema,
57467
57998
  sampleWarnings: external_exports.array(analyzeSampleWarningSchema),
57468
57999
  promptRules: external_exports.array(external_exports.string()),
57469
- metricCandidates: external_exports.array(metricCandidateSchema).optional()
57470
- });
58000
+ metricCandidates: external_exports.array(metricCandidateSchema).optional(),
58001
+ clarificationQuestions: external_exports.array(clarificationQuestionSchema).optional()
58002
+ });
58003
+ var compactAnalyzeContextSchema = external_exports.object({
58004
+ format: external_exports.literal("compact-v1"),
58005
+ intent: external_exports.object({
58006
+ raw: external_exports.string(),
58007
+ coverage: external_exports.enum(["full", "partial"])
58008
+ }),
58009
+ assumptions: external_exports.array(external_exports.tuple([
58010
+ external_exports.enum(["primary_measure", "primary_dimension", "time_field"]),
58011
+ external_exports.string(),
58012
+ external_exports.number(),
58013
+ external_exports.array(external_exports.string()).nullable().optional()
58014
+ ])),
58015
+ fields: external_exports.array(external_exports.tuple([
58016
+ external_exports.string(),
58017
+ external_exports.enum(["measure", "dimension", "time", "id", "status", "score", "unknown"]),
58018
+ external_exports.enum(["number", "string", "date", "boolean", "unknown"]),
58019
+ external_exports.number().nullable().optional(),
58020
+ external_exports.number().nullable().optional()
58021
+ ])),
58022
+ evidence: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.union([
58023
+ external_exports.record(external_exports.string(), external_exports.unknown()),
58024
+ external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown()))
58025
+ ])])),
58026
+ metricCandidates: external_exports.array(external_exports.tuple([
58027
+ external_exports.string(),
58028
+ external_exports.enum(["unit_average", "rate", "share", "period_change", "difference"]),
58029
+ external_exports.string(),
58030
+ external_exports.number().nullable().optional()
58031
+ ])),
58032
+ catalog: external_exports.object({
58033
+ charts: external_exports.array(external_exports.string()),
58034
+ blockedCharts: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()])),
58035
+ 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(),
58036
+ blockedBlocks: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()])).optional(),
58037
+ 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(),
58038
+ blockedTemplates: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()])).optional()
58039
+ }),
58040
+ warnings: external_exports.array(external_exports.tuple([external_exports.string(), external_exports.string()])),
58041
+ clarificationQuestions: external_exports.array(external_exports.tuple([
58042
+ external_exports.string(),
58043
+ external_exports.string(),
58044
+ external_exports.array(external_exports.string()),
58045
+ external_exports.boolean(),
58046
+ external_exports.enum(["measure", "dimension", "time", "template"])
58047
+ ]))
58048
+ });
58049
+ function toCompactAnalyzeContext(ctx) {
58050
+ return {
58051
+ format: "compact-v1",
58052
+ intent: { raw: ctx.intent.raw, coverage: ctx.intent.coverage },
58053
+ assumptions: ctx.intent.assumptions.map((a) => [a.key, a.value, a.confidence, a.alternatives]),
58054
+ fields: ctx.fields.map((f) => [f.name, f.role, f.type, f.distinctCount, f.timePeriods]),
58055
+ evidence: ctx.evidence.map((e) => [e.id, e.values ?? e.rows ?? {}]),
58056
+ metricCandidates: (ctx.metricCandidates ?? []).map((m) => [m.id, m.type, m.formula, m.value]),
58057
+ catalog: {
58058
+ charts: ctx.catalog.charts,
58059
+ blockedCharts: ctx.catalog.blockedCharts.map((c) => [c.type, c.reason]),
58060
+ blocks: ctx.catalog.blocks?.map((b) => [b.id, b.score, b.density, b.charts]),
58061
+ blockedBlocks: ctx.catalog.blockedBlocks?.map((b) => [b.id, b.reason]),
58062
+ templates: ctx.catalog.templates?.map((t) => [t.id, t.score, t.density, t.blocks]),
58063
+ blockedTemplates: ctx.catalog.blockedTemplates?.map((t) => [t.id, t.reason])
58064
+ },
58065
+ warnings: ctx.sampleWarnings.map((w) => [w.code, w.message]),
58066
+ clarificationQuestions: (ctx.clarificationQuestions ?? []).map((q) => [
58067
+ q.id,
58068
+ q.question,
58069
+ q.options,
58070
+ q.blocking,
58071
+ q.appliesTo
58072
+ ])
58073
+ };
58074
+ }
58075
+ function fromCompactAnalyzeContext(ctx) {
58076
+ return {
58077
+ intent: {
58078
+ raw: ctx.intent.raw,
58079
+ coverage: ctx.intent.coverage,
58080
+ assumptions: ctx.assumptions.map(([key, value, confidence, alternatives]) => ({
58081
+ key,
58082
+ value,
58083
+ confidence,
58084
+ alternatives: alternatives ?? void 0
58085
+ }))
58086
+ },
58087
+ fields: ctx.fields.map(([name, role, type, distinctCount, timePeriods]) => ({
58088
+ name,
58089
+ role,
58090
+ type,
58091
+ ...distinctCount !== void 0 && distinctCount !== null ? { distinctCount } : {},
58092
+ ...timePeriods !== void 0 && timePeriods !== null ? { timePeriods } : {}
58093
+ })),
58094
+ evidence: ctx.evidence.map(([id, value]) => ({
58095
+ id,
58096
+ query: `compact evidence: ${id}`,
58097
+ ...Array.isArray(value) ? { rows: value } : { values: value }
58098
+ })),
58099
+ catalog: {
58100
+ charts: ctx.catalog.charts,
58101
+ blockedCharts: ctx.catalog.blockedCharts.map(([type, reason]) => ({ type, reason })),
58102
+ recommendedPlan: [],
58103
+ blocks: ctx.catalog.blocks?.map(([id, score, density, charts]) => ({
58104
+ id,
58105
+ score,
58106
+ description: "",
58107
+ bestFor: [],
58108
+ density,
58109
+ examplePrompt: "",
58110
+ charts,
58111
+ variables: {},
58112
+ qualityChecks: []
58113
+ })),
58114
+ blockedBlocks: ctx.catalog.blockedBlocks?.map(([id, reason]) => ({ id, reason })),
58115
+ templates: ctx.catalog.templates?.map(([id, score, density, blocks]) => ({
58116
+ id,
58117
+ score,
58118
+ bestFor: [],
58119
+ requires: [],
58120
+ blocks,
58121
+ density
58122
+ })),
58123
+ blockedTemplates: ctx.catalog.blockedTemplates?.map(([id, reason]) => ({ id, reason }))
58124
+ },
58125
+ sampleWarnings: ctx.warnings.map(([code, message]) => ({ code, message })),
58126
+ promptRules: [],
58127
+ metricCandidates: ctx.metricCandidates.map(([id, type, formula, value]) => ({
58128
+ id,
58129
+ type,
58130
+ label: id,
58131
+ formula,
58132
+ ...value !== void 0 && value !== null ? { value } : {},
58133
+ confidence: "medium"
58134
+ })),
58135
+ clarificationQuestions: ctx.clarificationQuestions.map(([id, question, options, blocking, appliesTo]) => ({
58136
+ id,
58137
+ question,
58138
+ options,
58139
+ blocking,
58140
+ appliesTo
58141
+ }))
58142
+ };
58143
+ }
58144
+ function parseAnalyzeContext(value) {
58145
+ const unwrapped = value.ok === true ? value.value : value;
58146
+ const full = analyzeContextSchema.safeParse(unwrapped);
58147
+ if (full.success) return full.data;
58148
+ const compact = compactAnalyzeContextSchema.safeParse(unwrapped);
58149
+ if (compact.success) return fromCompactAnalyzeContext(compact.data);
58150
+ return null;
58151
+ }
57471
58152
 
57472
58153
  // src/deck-layouts.ts
57473
58154
  function withSize(chart, width, height) {
57474
58155
  return { ...chart, style: { ...chart.style, width, height } };
57475
58156
  }
58157
+ function formatMetricValue(num, fmt) {
58158
+ const hasCurrency = fmt.includes("$");
58159
+ const hasPercent = fmt.includes("%");
58160
+ const hasThousands = fmt.includes(",");
58161
+ const hasSiPrefix = fmt.includes("s");
58162
+ const precisionMatch = fmt.match(/\.(\d+)/);
58163
+ let precision = precisionMatch ? parseInt(precisionMatch[1], 10) : void 0;
58164
+ let value = num;
58165
+ let suffix = "";
58166
+ if (hasPercent) {
58167
+ value = num < 2 ? num * 100 : num;
58168
+ suffix = "%";
58169
+ if (precision === void 0) precision = 1;
58170
+ }
58171
+ if (hasSiPrefix) {
58172
+ const abs = Math.abs(value);
58173
+ if (abs >= 1e9) {
58174
+ value /= 1e9;
58175
+ suffix = "B" + suffix;
58176
+ } else if (abs >= 1e6) {
58177
+ value /= 1e6;
58178
+ suffix = "M" + suffix;
58179
+ } else if (abs >= 1e3) {
58180
+ value /= 1e3;
58181
+ suffix = "K" + suffix;
58182
+ }
58183
+ if (precision === void 0) precision = 0;
58184
+ }
58185
+ if (precision === void 0 && hasThousands) precision = 0;
58186
+ const fixed = precision !== void 0 ? value.toFixed(precision) : Math.round(value).toString();
58187
+ const [intPart, decPart] = fixed.split(".");
58188
+ const formattedInt = hasThousands ? intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : intPart;
58189
+ const formatted = decPart !== void 0 ? `${formattedInt}.${decPart}` : formattedInt;
58190
+ const prefix = hasCurrency ? "$" : "";
58191
+ return prefix + formatted + suffix;
58192
+ }
57476
58193
  function resolveMetricValue(metric, rows) {
57477
58194
  let raw = metric.value;
57478
58195
  if (raw === void 0 && metric.data?.transform?.length) {
@@ -57482,10 +58199,7 @@ function resolveMetricValue(metric, rows) {
57482
58199
  }
57483
58200
  const num = Number(raw);
57484
58201
  if (!Number.isFinite(num)) return String(raw ?? "\u2014");
57485
- const fmt = metric.format ?? "";
57486
- if (fmt.includes("$")) return "$" + Math.round(num).toLocaleString();
57487
- if (fmt.includes("%")) return (num < 2 ? (num * 100).toFixed(1) : num.toFixed(1)) + "%";
57488
- return Math.round(num).toLocaleString();
58202
+ return formatMetricValue(num, metric.format ?? "");
57489
58203
  }
57490
58204
  function renderEyebrow(text) {
57491
58205
  return `<div class="slide-eyebrow">${escapeHtml(text)}</div>`;
@@ -57511,21 +58225,22 @@ function pageFooter(index, _total, mark) {
57511
58225
  <div class="slide-page-num">${String(index + 1).padStart(2, "0")}</div>`;
57512
58226
  }
57513
58227
  var COVER_DECO = `<svg viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg" style="width:260px;opacity:0.9">
57514
- <circle cx="160" cy="160" r="140" fill="none" stroke="#e5e3d8" stroke-width="1"/>
57515
- <circle cx="160" cy="160" r="100" fill="none" stroke="#e5e3d8" stroke-width="1"/>
57516
- <circle cx="160" cy="160" r="60" fill="#eef2f7" stroke="#1b365d" stroke-width="1.2"/>
57517
- <path d="M160 20 A140 140 0 0 1 300 160" fill="none" stroke="#1b365d" stroke-width="2.2" stroke-linecap="round"/>
57518
- <path d="M300 160 A140 140 0 0 1 160 300" fill="none" stroke="#6b6a64" stroke-width="1" stroke-linecap="round"/>
57519
- <path d="M160 300 A140 140 0 0 1 20 160" fill="none" stroke="#6b6a64" stroke-width="1" stroke-linecap="round"/>
57520
- <path d="M20 160 A140 140 0 0 1 160 20" fill="none" stroke="#6b6a64" stroke-width="1" stroke-linecap="round"/>
58228
+ <circle cx="160" cy="160" r="140" fill="none" stroke="var(--mv-border, #e5e3d8)" stroke-width="1"/>
58229
+ <circle cx="160" cy="160" r="100" fill="none" stroke="var(--mv-border, #e5e3d8)" stroke-width="1"/>
58230
+ <circle cx="160" cy="160" r="60" fill="var(--mv-surface, #eef2f7)" stroke="var(--mv-brand, #1b365d)" stroke-width="1.2"/>
58231
+ <path d="M160 20 A140 140 0 0 1 300 160" fill="none" stroke="var(--mv-brand, #1b365d)" stroke-width="2.2" stroke-linecap="round"/>
58232
+ <path d="M300 160 A140 140 0 0 1 160 300" fill="none" stroke="var(--mv-muted, #6b6a64)" stroke-width="1" stroke-linecap="round"/>
58233
+ <path d="M160 300 A140 140 0 0 1 20 160" fill="none" stroke="var(--mv-muted, #6b6a64)" stroke-width="1" stroke-linecap="round"/>
58234
+ <path d="M20 160 A140 140 0 0 1 160 20" fill="none" stroke="var(--mv-muted, #6b6a64)" stroke-width="1" stroke-linecap="round"/>
57521
58235
  </svg>`;
57522
- function renderCoverSlide(slide, _rows, _svg, index, total) {
58236
+ function renderCoverSlide(slide, _rows, _svg, index, total, deckDescription) {
58237
+ const subtitle = slide.claim ?? deckDescription;
57523
58238
  return `<div class="slide">
57524
58239
  <div class="slide-cover">
57525
58240
  <div class="slide-cover-left">
57526
58241
  <div class="slide-mark">miao-vision</div>
57527
58242
  <h1>${escapeHtml(slide.title ?? "Presentation")}</h1>
57528
- ${slide.claim ? `<div class="sub">${escapeHtml(slide.claim)}</div>` : ""}
58243
+ ${subtitle ? `<div class="sub">${escapeHtml(subtitle)}</div>` : ""}
57529
58244
  <div class="line"></div>
57530
58245
  <div class="meta">${escapeHtml(slide.eyebrow ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10))}</div>
57531
58246
  </div>
@@ -57611,18 +58326,6 @@ function renderEndingSlide(slide, _rows, _svg, index, total) {
57611
58326
 
57612
58327
  // src/deck-renderer.ts
57613
58328
  var SLIDE_CSS = `
57614
- :root {
57615
- --mv-paper: #f5f4ed;
57616
- --mv-surface: #faf9f5;
57617
- --mv-brand: #1b365d;
57618
- --mv-ink: #141413;
57619
- --mv-muted: #6b6a64;
57620
- --mv-soft: #504e49;
57621
- --mv-border: #e5e3d8;
57622
- --mv-serif: Charter, Georgia, "Times New Roman", serif;
57623
- --mv-mono: "SF Mono", "JetBrains Mono", Consolas, monospace;
57624
- --slide-scale: 1;
57625
- }
57626
58329
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
57627
58330
  body { font-family: var(--mv-serif); -webkit-font-smoothing: antialiased; }
57628
58331
 
@@ -57714,7 +58417,7 @@ var SLIDE_CSS = `
57714
58417
  .slide:last-child { break-after: auto; page-break-after: auto; }
57715
58418
  .slide-nav { display: none !important; }
57716
58419
  }
57717
- @page { size: A4 landscape; margin: 0; background: #f5f4ed; }
58420
+ @page { size: A4 landscape; margin: 0; background: var(--mv-paper, #f5f4ed); }
57718
58421
  `;
57719
58422
  var SLIDE_JS = `
57720
58423
  (function() {
@@ -57766,10 +58469,14 @@ var SLIDE_JS = `
57766
58469
  goTo(0);
57767
58470
  })();
57768
58471
  `;
57769
- function renderSlide(slide, rows, svgTheme, index, total) {
58472
+ function extractRootVars(css) {
58473
+ const match = css.match(/:root\s*\{([\s\S]*?)\}/);
58474
+ return match ? `:root {${match[1]}}` : "";
58475
+ }
58476
+ function renderSlide(slide, rows, svgTheme, index, total, deckDescription) {
57770
58477
  switch (slide.layout) {
57771
58478
  case "cover":
57772
- return renderCoverSlide(slide, rows, svgTheme, index, total);
58479
+ return renderCoverSlide(slide, rows, svgTheme, index, total, deckDescription);
57773
58480
  case "title-only":
57774
58481
  return renderTitleOnlySlide(slide, rows, svgTheme, index, total);
57775
58482
  case "text-points":
@@ -57788,16 +58495,19 @@ function renderSlide(slide, rows, svgTheme, index, total) {
57788
58495
  }
57789
58496
  function renderDeckHtml(spec, rows, themeOverride) {
57790
58497
  const theme = getTheme(themeOverride ?? spec.theme ?? "editorial");
58498
+ const themeRootVars = extractRootVars(theme.css);
57791
58499
  const title = spec.title ?? "Presentation";
57792
58500
  const total = spec.slides.length;
57793
- const slidesHtml = spec.slides.map((slide, i) => renderSlide(slide, rows, theme.svg, i, total)).join("\n");
58501
+ const slidesHtml = spec.slides.map((slide, i) => renderSlide(slide, rows, theme.svg, i, total, spec.description)).join("\n");
57794
58502
  return `<!doctype html>
57795
58503
  <html lang="en">
57796
58504
  <head>
57797
58505
  <meta charset="utf-8" />
57798
58506
  <meta name="viewport" content="width=device-width, initial-scale=1" />
58507
+ ${spec.description ? `<meta name="description" content="${escapeHtml(spec.description)}" />` : ""}
57799
58508
  <title>${escapeHtml(title)}</title>
57800
58509
  <style>${SLIDE_CSS}</style>
58510
+ ${themeRootVars ? `<style id="miao-deck-theme">${themeRootVars}</style>` : ""}
57801
58511
  </head>
57802
58512
  <body class="present-mode">
57803
58513
  <div class="slide-viewport">
@@ -57919,6 +58629,14 @@ function validateDeckSpecSemantics(spec) {
57919
58629
  hint: hintForIssue(path, "at most 4 metrics")
57920
58630
  });
57921
58631
  }
58632
+ if (slide.charts && slide.charts.length > 1) {
58633
+ const path = `slides[${index}].charts`;
58634
+ errors.push({
58635
+ path,
58636
+ message: `${path}: A slide can include at most 1 chart.`,
58637
+ hint: hintForIssue(path, "at most 1 chart")
58638
+ });
58639
+ }
57922
58640
  if (slide.layout === "table-full" && slide.charts?.[0] && slide.charts[0].type !== "table") {
57923
58641
  const path = `slides[${index}].charts[0].type`;
57924
58642
  errors.push({
@@ -57961,13 +58679,13 @@ function validateChartFields(chart, sourceFields, path) {
57961
58679
  return deckFieldError(path, chart.type, `Chart type '${chart.type}' is not supported.`);
57962
58680
  }
57963
58681
  for (const encoding of REQUIRED_ENCODINGS[chart.type] ?? []) {
57964
- if (!chart.encoding[encoding]?.field) {
58682
+ if (!chart.encoding?.[encoding]?.field) {
57965
58683
  return deckFieldError(`${path}.encoding.${encoding}`, encoding, `Chart type '${chart.type}' requires encoding '${encoding}'.`);
57966
58684
  }
57967
58685
  }
57968
58686
  const available = applyTransforms(chart.data?.transform ?? [], sourceFields, path);
57969
58687
  if (isAgentError(available)) return available;
57970
- for (const [encoding, spec] of Object.entries(chart.encoding)) {
58688
+ for (const [encoding, spec] of Object.entries(chart.encoding ?? {})) {
57971
58689
  if (spec?.field && !available.value.has(spec.field)) {
57972
58690
  return deckFieldError(`${path}.encoding.${encoding}.field`, spec.field, `Field '${spec.field}' is not available for this chart encoding.`);
57973
58691
  }
@@ -58031,6 +58749,7 @@ function hintForIssue(path, message) {
58031
58749
  if (message.includes("requires at least one chart")) return `Add a chart under ${path}.`;
58032
58750
  if (message.includes("requires at least one metric")) return `Add one to four metrics under ${path}.`;
58033
58751
  if (message.includes("at most 4 metrics")) return `Reduce ${path} to four metrics or split them across multiple slides.`;
58752
+ if (message.includes("at most 1 chart")) return `Reduce ${path} to a single chart or spread multiple charts across separate slides.`;
58034
58753
  if (message.includes("only accepts a table chart")) return `Change ${path} to 'table' or use a chart-focused layout.`;
58035
58754
  return `Check ${path} in the DeckSpec.`;
58036
58755
  }
@@ -58064,7 +58783,7 @@ var infographicSpecSchema = external_exports.object({
58064
58783
  inputFile: external_exports.string().default(""),
58065
58784
  generatedAt: external_exports.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
58066
58785
  wordCount: external_exports.number().int().min(0).default(0)
58067
- }).default({})
58786
+ }).default(() => ({ inputFile: "", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), wordCount: 0 }))
58068
58787
  });
58069
58788
  function loadInfographicSpec(file2) {
58070
58789
  let raw;
@@ -58768,6 +59487,178 @@ function toCatalogBlockEntry(resolver, decision, ctx) {
58768
59487
  };
58769
59488
  }
58770
59489
 
59490
+ // src/report-template-registry.ts
59491
+ var YAML = __toESM(require_dist(), 1);
59492
+ function scoreForRequirements(ctx, requires) {
59493
+ for (const role of requires) {
59494
+ if (role === "measure" && !ctx.fields.some((f) => f.role === "measure" || f.role === "score")) {
59495
+ return { ok: false, score: 0, reason: "missing required measure" };
59496
+ }
59497
+ if (role === "dimension" && !ctx.fields.some((f) => f.role === "dimension" || f.role === "status")) {
59498
+ return { ok: false, score: 0, reason: "missing required dimension" };
59499
+ }
59500
+ if (role === "time") {
59501
+ const time3 = ctx.fields.find((f) => f.role === "time");
59502
+ if (!time3) return { ok: false, score: 0, reason: "missing required time" };
59503
+ const periods = time3.timePeriods ?? 0;
59504
+ if (periods < 3) return { ok: false, score: 0, reason: `timePeriods=${periods} < 3` };
59505
+ }
59506
+ }
59507
+ return { ok: true, score: Math.min(0.65 + requires.length * 0.1 + (ctx.evidence.length > 0 ? 0.05 : 0), 1) };
59508
+ }
59509
+ function compileBlocks(blockIds, ctx) {
59510
+ const charts = [];
59511
+ for (const id of blockIds) {
59512
+ const block = getBlockById(id);
59513
+ if (!block) continue;
59514
+ const decision = block.canUse(ctx);
59515
+ if (!decision.ok) continue;
59516
+ const variables = block.defaultVariables(ctx);
59517
+ charts.push(...block.compile(variables, ctx).charts);
59518
+ }
59519
+ const seen = /* @__PURE__ */ new Set();
59520
+ return {
59521
+ title: "Miao Vision Report",
59522
+ insights: [],
59523
+ charts: charts.map((chart) => {
59524
+ if (!chart.id || !seen.has(chart.id)) {
59525
+ if (chart.id) seen.add(chart.id);
59526
+ return chart;
59527
+ }
59528
+ const next = { ...chart, id: `${chart.id}_${seen.size + 1}` };
59529
+ seen.add(next.id);
59530
+ return next;
59531
+ })
59532
+ };
59533
+ }
59534
+ var TEMPLATE_REGISTRY = [
59535
+ {
59536
+ id: "snapshot-overview",
59537
+ bestFor: ["static comparison", "category ranking", "no time axis"],
59538
+ requires: ["measure", "dimension"],
59539
+ blocks: ["kpi-summary", "snapshot-ranking"],
59540
+ density: "compact",
59541
+ qualityNotes: ["Use for static comparisons without requiring a trend.", "Add sample caveats when sampleWarnings are present."],
59542
+ canUse: (ctx) => scoreForRequirements(ctx, ["measure", "dimension"]),
59543
+ instantiate: (ctx) => compileBlocks(["kpi-summary", "snapshot-ranking"], ctx)
59544
+ },
59545
+ {
59546
+ id: "trend-ranking-overview",
59547
+ bestFor: ["executive trend with category ranking", "monthly review"],
59548
+ requires: ["measure", "dimension", "time"],
59549
+ blocks: ["trend-ranking"],
59550
+ density: "full",
59551
+ qualityNotes: ["Requires at least 3 time periods.", "Combines KPI, trend, and ranking views."],
59552
+ canUse: (ctx) => scoreForRequirements(ctx, ["measure", "dimension", "time"]),
59553
+ instantiate: (ctx) => compileBlocks(["trend-ranking"], ctx)
59554
+ },
59555
+ {
59556
+ id: "full-detail-report",
59557
+ bestFor: ["comprehensive business review", "trend plus ranking plus table"],
59558
+ requires: ["measure", "dimension", "time"],
59559
+ blocks: ["full-detail-report"],
59560
+ density: "full",
59561
+ qualityNotes: ["Use for comprehensive reviews where detail table is acceptable.", "Keep total chart count within report limits."],
59562
+ canUse: (ctx) => scoreForRequirements(ctx, ["measure", "dimension", "time"]),
59563
+ instantiate: (ctx) => compileBlocks(["full-detail-report"], ctx)
59564
+ },
59565
+ {
59566
+ id: "composition-review",
59567
+ bestFor: ["share analysis", "part-to-whole breakdown", "composition report"],
59568
+ requires: ["measure", "dimension"],
59569
+ blocks: ["kpi-summary", "comparison-breakdown"],
59570
+ density: "medium",
59571
+ qualityNotes: ["Use for share or composition analysis.", "Avoid when the primary dimension has too many categories for pie."],
59572
+ canUse: (ctx) => scoreForRequirements(ctx, ["measure", "dimension"]),
59573
+ instantiate: (ctx) => compileBlocks(["kpi-summary", "comparison-breakdown"], ctx)
59574
+ }
59575
+ ];
59576
+ function getTemplateById(id) {
59577
+ return TEMPLATE_REGISTRY.find((template) => template.id === id);
59578
+ }
59579
+ function templateInfo(template) {
59580
+ return {
59581
+ id: template.id,
59582
+ bestFor: template.bestFor,
59583
+ requires: template.requires,
59584
+ blocks: template.blocks,
59585
+ density: template.density,
59586
+ qualityNotes: template.qualityNotes
59587
+ };
59588
+ }
59589
+ function toCatalogTemplateEntry(resolver, decision) {
59590
+ return {
59591
+ id: resolver.id,
59592
+ score: decision.score,
59593
+ bestFor: resolver.bestFor,
59594
+ requires: resolver.requires,
59595
+ blocks: resolver.blocks,
59596
+ density: resolver.density
59597
+ };
59598
+ }
59599
+ function templateSpecToYaml(spec) {
59600
+ return YAML.stringify(spec);
59601
+ }
59602
+ function buildTemplateCatalog(ctx) {
59603
+ const templates = [];
59604
+ const blockedTemplates = [];
59605
+ for (const template of TEMPLATE_REGISTRY) {
59606
+ const decision = template.canUse(ctx);
59607
+ if (decision.ok && decision.score >= 0.5) {
59608
+ templates.push(toCatalogTemplateEntry(template, decision));
59609
+ } else {
59610
+ blockedTemplates.push({ id: template.id, reason: decision.reason ?? `score=${decision.score.toFixed(2)} < 0.5` });
59611
+ }
59612
+ }
59613
+ templates.sort((a, b) => b.score - a.score);
59614
+ return { templates, blockedTemplates };
59615
+ }
59616
+
59617
+ // src/analyze-clarifications.ts
59618
+ function buildClarificationQuestions(fields, intent = "") {
59619
+ const questions = [];
59620
+ const measures = fields.filter((f) => f.role === "measure" || f.role === "score");
59621
+ const dimensions = fields.filter((f) => f.role === "dimension" || f.role === "status");
59622
+ const times = fields.filter((f) => f.role === "time");
59623
+ const precisionMode = /precise|accurate|decision|executive|老板|决策|精准/i.test(intent);
59624
+ if (measures.length === 0) {
59625
+ questions.push({
59626
+ id: "primary_measure",
59627
+ question: "Which field should be treated as the primary measure?",
59628
+ options: fields.map((f) => f.name),
59629
+ blocking: true,
59630
+ appliesTo: "measure"
59631
+ });
59632
+ } else if (measures.length > 1) {
59633
+ questions.push({
59634
+ id: "primary_measure",
59635
+ question: `This dataset has multiple measures. Analyze ${measures.map((f) => f.name).join(", ")}?`,
59636
+ options: measures.map((f) => f.name),
59637
+ blocking: precisionMode,
59638
+ appliesTo: "measure"
59639
+ });
59640
+ }
59641
+ if (dimensions.length > 1) {
59642
+ questions.push({
59643
+ id: "primary_dimension",
59644
+ question: `Which dimension should drive the main comparison: ${dimensions.map((f) => f.name).join(", ")}?`,
59645
+ options: dimensions.map((f) => f.name),
59646
+ blocking: false,
59647
+ appliesTo: "dimension"
59648
+ });
59649
+ }
59650
+ if (times.length > 1) {
59651
+ questions.push({
59652
+ id: "time_field",
59653
+ question: `Which time field should drive trend views: ${times.map((f) => f.name).join(", ")}?`,
59654
+ options: times.map((f) => f.name),
59655
+ blocking: false,
59656
+ appliesTo: "time"
59657
+ });
59658
+ }
59659
+ return questions;
59660
+ }
59661
+
58771
59662
  // src/analyzer.ts
58772
59663
  function analyzeDataset(dataset, options = {}) {
58773
59664
  const profile = profileDataset(dataset);
@@ -58782,7 +59673,8 @@ function analyzeDataset(dataset, options = {}) {
58782
59673
  const catalog = buildCatalog(fields, sampleWarnings, profile.rows, evidence);
58783
59674
  const promptRules = buildPromptRules(catalog.charts, sampleWarnings);
58784
59675
  const metricCandidates = buildMetricCandidates(fields, evidence);
58785
- return { intent, fields, evidence, catalog, sampleWarnings, promptRules, metricCandidates };
59676
+ const clarificationQuestions = buildClarificationQuestions(fields, options.intent ?? "");
59677
+ return { intent, fields, evidence, catalog, sampleWarnings, promptRules, metricCandidates, clarificationQuestions };
58786
59678
  }
58787
59679
  function buildAnalyzeFields(columns) {
58788
59680
  return columns.map((col) => {
@@ -58816,7 +59708,6 @@ function refineRole(col) {
58816
59708
  if (col.type === "string") {
58817
59709
  const name = col.name.toLowerCase();
58818
59710
  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
59711
  return "dimension";
58821
59712
  }
58822
59713
  return "unknown";
@@ -58838,27 +59729,50 @@ function parseIntent(raw, fields, correctAssumption) {
58838
59729
  const times = fields.filter((f) => f.role === "time");
58839
59730
  let primaryMeasure = measures[0]?.name;
58840
59731
  let primaryDimension = dimensions[0]?.name;
59732
+ let timeField = times[0]?.name;
58841
59733
  if (correctAssumption) {
58842
- const m = correctAssumption.match(/^primary_measure=(\w+)$/);
58843
- const d = correctAssumption.match(/^primary_dimension=(\w+)$/);
59734
+ const m = correctAssumption.match(/^primary_measure=([\w-]+)$/);
59735
+ const d = correctAssumption.match(/^primary_dimension=([\w-]+)$/);
59736
+ const t = correctAssumption.match(/^time_field=([\w-]+)$/);
58844
59737
  if (m) primaryMeasure = m[1];
58845
59738
  if (d) primaryDimension = d[1];
59739
+ if (t) timeField = t[1];
58846
59740
  }
58847
59741
  const assumptions = [];
58848
59742
  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}`);
59743
+ assumptions.push({
59744
+ key: "primary_measure",
59745
+ value: primaryMeasure,
59746
+ confidence: measures.length > 1 ? 0.62 : 0.9,
59747
+ alternatives: measures.filter((f) => f.name !== primaryMeasure).map((f) => f.name),
59748
+ reason: measures.length > 1 ? "multiple numeric measures detected" : "single clear numeric measure"
59749
+ });
58851
59750
  }
58852
59751
  if (primaryDimension) {
58853
- assumptions.push(`primary dimension is "${primaryDimension}"`);
59752
+ assumptions.push({
59753
+ key: "primary_dimension",
59754
+ value: primaryDimension,
59755
+ confidence: dimensions.length > 1 ? 0.72 : 0.9,
59756
+ alternatives: dimensions.filter((f) => f.name !== primaryDimension).map((f) => f.name),
59757
+ reason: dimensions.length > 1 ? "multiple dimensions detected" : "single clear dimension"
59758
+ });
58854
59759
  }
58855
59760
  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}`);
59761
+ assumptions.push({
59762
+ key: "time_field",
59763
+ value: timeField ?? times[0].name,
59764
+ confidence: times.length > 1 ? 0.7 : 0.9,
59765
+ alternatives: times.filter((f) => f.name !== timeField).map((f) => f.name),
59766
+ reason: times.length > 1 ? "multiple time fields detected" : "single clear time field"
59767
+ });
58859
59768
  }
58860
59769
  if (measures.length === 0) {
58861
- assumptions.push("no numeric measure detected \u2014 use --correct-assumption primary_measure=<col>");
59770
+ assumptions.push({
59771
+ key: "primary_measure",
59772
+ value: "",
59773
+ confidence: 0,
59774
+ reason: "no numeric measure detected"
59775
+ });
58862
59776
  }
58863
59777
  const rawLower = raw.toLowerCase();
58864
59778
  const wantsTrend = /trend|over time|by month|by year/.test(rawLower);
@@ -58877,7 +59791,7 @@ function runStandardQueries(dataset, fields, sampleWarnings) {
58877
59791
  if (measures.length > 0) {
58878
59792
  const measureExpr = measures.slice(0, 4).map((m) => `sum(${m.name}) as total_${m.name}`).concat(["count(*) as row_count"]).join(", ");
58879
59793
  const result = queryDataset(dataset.rows, { measure: measureExpr });
58880
- if (result && "rows" in result && result.rows.length > 0) {
59794
+ if (!isAgentError(result) && result.rows.length > 0) {
58881
59795
  evidence.push({
58882
59796
  id: "total",
58883
59797
  query: `Total aggregates: ${measureExpr}`,
@@ -58892,7 +59806,7 @@ function runStandardQueries(dataset, fields, sampleWarnings) {
58892
59806
  measure: measureExpr,
58893
59807
  orderby: `total_${primaryMeasure.name} desc`
58894
59808
  });
58895
- if (result && "rows" in result && result.rows.length > 0) {
59809
+ if (!isAgentError(result) && result.rows.length > 0) {
58896
59810
  const totalVal = result.rows.reduce((s, r) => s + Number(r[`total_${primaryMeasure.name}`] ?? 0), 0);
58897
59811
  const rows = result.rows.map((r) => {
58898
59812
  const val = Number(r[`total_${primaryMeasure.name}`] ?? 0);
@@ -58913,7 +59827,7 @@ function runStandardQueries(dataset, fields, sampleWarnings) {
58913
59827
  measure: measureExpr,
58914
59828
  orderby: `${primaryTime.name} asc`
58915
59829
  });
58916
- if (result && "rows" in result && result.rows.length > 0) {
59830
+ if (!isAgentError(result) && result.rows.length > 0) {
58917
59831
  evidence.push({
58918
59832
  id: "by_time",
58919
59833
  query: `${primaryMeasure.name} by ${primaryTime.name} (ascending)`,
@@ -58937,7 +59851,7 @@ function runExtraQuery(dataset, extraQuery, existingCount) {
58937
59851
  orderby: parts.orderby,
58938
59852
  limit: parts.limit ? Number(parts.limit) : void 0
58939
59853
  });
58940
- if (!result || !("rows" in result)) return null;
59854
+ if (isAgentError(result)) return null;
58941
59855
  return {
58942
59856
  id: `extra_${existingCount + 1}`,
58943
59857
  query: `Custom query: ${extraQuery}`,
@@ -59027,7 +59941,8 @@ function buildCatalog(fields, warnings, rowCount, evidence) {
59027
59941
  }
59028
59942
  }
59029
59943
  blocks.sort((a, b) => b.score - a.score);
59030
- return { charts, blockedCharts, recommendedPlan, blocks, blockedBlocks };
59944
+ const templateCatalog = buildTemplateCatalog(matchCtx);
59945
+ return { charts, blockedCharts, recommendedPlan, blocks, blockedBlocks, ...templateCatalog };
59031
59946
  }
59032
59947
  function buildRecommendedPlan(charts, fields) {
59033
59948
  const plan = [];
@@ -59127,7 +60042,7 @@ function generatePatchHints(error51, spec, catalogCharts) {
59127
60042
  switch (error51.code) {
59128
60043
  case "UNSUPPORTED_TRANSFORM": {
59129
60044
  const chartId = detail?.["chartId"];
59130
- const chartIndex = spec.charts.findIndex((c) => (c.id ?? c.type) === chartId);
60045
+ const chartIndex = findChartIndex(spec, chartId);
59131
60046
  if (chartIndex < 0) return void 0;
59132
60047
  const transforms = spec.charts[chartIndex].data?.transform ?? [];
59133
60048
  const tIdx = transforms.findIndex((t) => t.type === "filter");
@@ -59136,7 +60051,7 @@ function generatePatchHints(error51, spec, catalogCharts) {
59136
60051
  }
59137
60052
  case "BLOCKED_CHART_STRICT": {
59138
60053
  const chartType = detail?.["chartType"];
59139
- const chartIdx = spec.charts.findIndex((c) => c.type === chartType);
60054
+ const chartIdx = findChartIndex(spec, chartType);
59140
60055
  if (chartIdx < 0 || !catalogCharts?.length) return void 0;
59141
60056
  const suggestion = catalogCharts[0];
59142
60057
  return [{ op: "replace", path: `/charts/${chartIdx}/type`, value: suggestion }];
@@ -59144,20 +60059,14 @@ function generatePatchHints(error51, spec, catalogCharts) {
59144
60059
  case "DUPLICATE_CHART_ID": {
59145
60060
  const dupId = detail?.["chartId"];
59146
60061
  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
- }
60062
+ const lastIdx = findLastChartIndexById(spec, dupId);
59154
60063
  if (lastIdx < 0) return void 0;
59155
60064
  return [{ op: "replace", path: `/charts/${lastIdx}/id`, value: `${dupId}_2` }];
59156
60065
  }
59157
60066
  case "MISSING_ENCODING": {
59158
60067
  const chartType = detail?.["chartType"];
59159
60068
  const required2 = detail?.["requiredEncodings"];
59160
- const chartIdx = spec.charts.findIndex((c) => c.type === chartType);
60069
+ const chartIdx = findChartIndex(spec, chartType);
59161
60070
  if (chartIdx < 0 || !required2?.length) return void 0;
59162
60071
  const existing = Object.keys(spec.charts[chartIdx].encoding ?? {});
59163
60072
  const missing = required2.filter((enc) => !existing.includes(enc));
@@ -59203,12 +60112,6 @@ function collectWarningPatches(spec) {
59203
60112
  }
59204
60113
  return patches;
59205
60114
  }
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
60115
 
59213
60116
  // src/cli-help.ts
59214
60117
  var COMMAND_HELP = {
@@ -59220,8 +60123,10 @@ Outputs context.json: intent, fields, evidence, catalog (blockedCharts), sampleW
59220
60123
  Options:
59221
60124
  --intent <text> Natural language description of the report goal
59222
60125
  --output <file> Write context.json to this path (default: stdout)
60126
+ --compact Output compact context for agent consumption
60127
+ --verbose Keep full context output explicitly for debugging
59223
60128
  --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"
60129
+ --correct-assumption <expr> Override an assumption: "primary_measure=orders", "primary_dimension=region", or "time_field=date"
59225
60130
  --sheet <name> Sheet name (Excel only)
59226
60131
  --limit <n> Max rows to read
59227
60132
  `,
@@ -59246,13 +60151,28 @@ Options:
59246
60151
  --profile <file> Path to profile JSON (output of "profile")
59247
60152
  --context <file> Path to context.json (output of "analyze") for catalog compliance and
59248
60153
  $evidence path checks
59249
- --strict Treat blockedChart violations as hard errors (requires --context)
60154
+ --strict With --verify, treat verify warnings as hard errors; also hard-fails blockedChart violations
59250
60155
  --verify Also check for forbidden words and missing sampleWarning caveats
59251
60156
  --patch-hints Attach machine-readable JSON Patch hints to fixable errors
59252
60157
  `,
59253
60158
  catalog: `Usage: miao-viz catalog
59254
60159
 
59255
60160
  List all available chart types and their required fields.
60161
+ `,
60162
+ block: `Usage: miao-viz block instantiate <block-id> --context <context.json> [--output <file>]
60163
+
60164
+ Instantiate a report block using full or compact analyze context.
60165
+ `,
60166
+ template: `Usage:
60167
+ miao-viz template list
60168
+ miao-viz template inspect <template-id>
60169
+ miao-viz template instantiate <template-id> --context <context.json> [--output <file>]
60170
+
60171
+ List, inspect, or instantiate report templates using full or compact analyze context.
60172
+ `,
60173
+ inspect: `Usage: miao-viz inspect --input <file> --spec <file> --context <context.json> --output <file>
60174
+
60175
+ Inspect chart transform pipelines and evidence usage for debugging.
59256
60176
  `,
59257
60177
  render: `Usage: miao-viz render --input <file> --spec <file> --output <file> [options]
59258
60178
 
@@ -59264,6 +60184,7 @@ Options:
59264
60184
  --output <file> Output file path
59265
60185
  --format <fmt> Output format: html, svg (default: html)
59266
60186
  --theme <name> Theme: default, editorial, dark, minimal
60187
+ --context <file> Path to context.json (output of "analyze") \u2014 resolves $evidence: directives in insights[]
59267
60188
  --interactive Force interactive runtime for HTML output
59268
60189
  --no-interactive Force static HTML output
59269
60190
  --sheet <name> Sheet name (Excel only)
@@ -59323,6 +60244,9 @@ Commands:
59323
60244
  query Run an aggregation query to get real computed values
59324
60245
  validate Validate a vizspec against a data profile
59325
60246
  catalog List all available chart types
60247
+ block Instantiate report blocks from analyze context
60248
+ template List, inspect, or instantiate report templates
60249
+ inspect Inspect chart transforms and evidence usage
59326
60250
  render Render a vizspec to HTML or SVG
59327
60251
  deck Render a deck spec to HTML slides
59328
60252
  article Convert a local article to an infographic artifact
@@ -59332,12 +60256,12 @@ Run "miao-viz <command> --help" for command-specific options.
59332
60256
  }
59333
60257
 
59334
60258
  // src/cli-block.ts
59335
- var YAML2 = __toESM(require_dist(), 1);
60259
+ var YAML3 = __toESM(require_dist(), 1);
59336
60260
 
59337
60261
  // src/cli-utils.ts
59338
60262
  var import_node_fs3 = require("node:fs");
59339
60263
  var import_node_path3 = require("node:path");
59340
- var YAML = __toESM(require_dist(), 1);
60264
+ var YAML2 = __toESM(require_dist(), 1);
59341
60265
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
59342
60266
  "h",
59343
60267
  "help",
@@ -59348,7 +60272,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
59348
60272
  "strict",
59349
60273
  "patch-hints",
59350
60274
  "verify",
59351
- "for-llm"
60275
+ "for-llm",
60276
+ "compact",
60277
+ "verbose"
59352
60278
  ]);
59353
60279
  function parseArgs(argv) {
59354
60280
  const [command, ...rest] = argv;
@@ -59410,7 +60336,7 @@ function printJson(value) {
59410
60336
  function readSpec(file2) {
59411
60337
  const text = (0, import_node_fs3.readFileSync)(file2, "utf8");
59412
60338
  if (file2.endsWith(".json")) return JSON.parse(text);
59413
- return YAML.parse(text);
60339
+ return YAML2.parse(text);
59414
60340
  }
59415
60341
  function readJson(file2) {
59416
60342
  return JSON.parse((0, import_node_fs3.readFileSync)(file2, "utf8"));
@@ -59443,6 +60369,11 @@ function runCatalog(args) {
59443
60369
  const catalog = {
59444
60370
  charts: CHART_CATALOG.map((c) => ({
59445
60371
  id: c.id,
60372
+ compactFor: c.compactFor,
60373
+ requires: c.requires,
60374
+ transformRecipe: c.transformRecipe,
60375
+ avoid: c.avoid,
60376
+ insightPattern: c.insightPattern,
59446
60377
  requiredEncodings: c.requiredEncodings,
59447
60378
  allowedTransforms: c.allowedTransforms,
59448
60379
  rules: c.rules.map((r) => r.expression),
@@ -59474,11 +60405,11 @@ function runBlock(args) {
59474
60405
  if (isAgentError(contextPath)) return fail(contextPath);
59475
60406
  const raw = readJson(contextPath);
59476
60407
  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 }));
60408
+ const parsed = parseAnalyzeContext(unwrapped);
60409
+ if (!parsed) {
60410
+ return fail(agentError("INVALID_CONTEXT", "context.json format is invalid.", { contextPath }));
59480
60411
  }
59481
- const ctx = parsed.data;
60412
+ const ctx = parsed;
59482
60413
  const resolver = getBlockById(blockId);
59483
60414
  if (!resolver) {
59484
60415
  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 +60436,7 @@ function runBlock(args) {
59505
60436
  }
59506
60437
  function buildBlockDraft(resolver, variables, score, compiled) {
59507
60438
  const varSummary = Object.entries(variables).map(([k, v]) => `${k}=${v}`).join(", ");
59508
- const yamlBody = YAML2.stringify({ charts: compiled.charts, insights: compiled.insights ?? [] });
60439
+ const yamlBody = YAML3.stringify({ charts: compiled.charts, insights: compiled.insights ?? [] });
59509
60440
  const checks = resolver.qualityChecks.map((c) => `# [ ] ${c}`).join("\n");
59510
60441
  return [
59511
60442
  `# Generated by: miao-viz block instantiate ${resolver.id}`,
@@ -59519,6 +60450,140 @@ function buildBlockDraft(resolver, variables, score, compiled) {
59519
60450
  ].join("\n");
59520
60451
  }
59521
60452
 
60453
+ // src/cli-template.ts
60454
+ function runTemplate(args) {
60455
+ const subcommand = args.positional[0];
60456
+ if (subcommand === "list") {
60457
+ return {
60458
+ ok: true,
60459
+ value: TEMPLATE_REGISTRY.map(templateInfo)
60460
+ };
60461
+ }
60462
+ if (subcommand === "inspect") {
60463
+ const templateId = args.positional[1];
60464
+ if (!templateId) return fail(agentError("MISSING_INPUT", "Usage: miao-viz template inspect <template-id>"));
60465
+ const template = getTemplateById(templateId);
60466
+ if (!template) return fail(agentError("UNKNOWN_TEMPLATE_ID", `Template id '${templateId}' not found.`, { availableIds: TEMPLATE_REGISTRY.map((t) => t.id) }));
60467
+ return { ok: true, value: templateInfo(template) };
60468
+ }
60469
+ if (subcommand === "instantiate") {
60470
+ return runTemplateInstantiate(args);
60471
+ }
60472
+ return fail(agentError("UNKNOWN_SUBCOMMAND", `Unknown template subcommand '${subcommand ?? "(none)"}'. Supported: list, inspect, instantiate`, { subcommand }));
60473
+ }
60474
+ function runTemplateInstantiate(args) {
60475
+ const templateId = args.positional[1];
60476
+ if (!templateId) {
60477
+ return fail(agentError("MISSING_INPUT", "Usage: miao-viz template instantiate <template-id> --context <context.json> [--output <file>]"));
60478
+ }
60479
+ const contextPath = requiredFlag(args, "context");
60480
+ if (isAgentError(contextPath)) return fail(contextPath);
60481
+ const ctx = parseAnalyzeContext(readJson(contextPath));
60482
+ if (!ctx) return fail(agentError("INVALID_CONTEXT", "context.json format is invalid.", { contextPath }));
60483
+ const template = getTemplateById(templateId);
60484
+ if (!template) {
60485
+ return fail(agentError("UNKNOWN_TEMPLATE_ID", `Template id '${templateId}' not found.`, { templateId, availableIds: TEMPLATE_REGISTRY.map((t) => t.id) }));
60486
+ }
60487
+ const matchCtx = {
60488
+ fields: ctx.fields,
60489
+ evidence: ctx.evidence,
60490
+ catalog: ctx.catalog,
60491
+ sampleWarnings: ctx.sampleWarnings
60492
+ };
60493
+ const decision = template.canUse(matchCtx);
60494
+ if (!decision.ok) {
60495
+ return fail(agentError("TEMPLATE_NOT_APPLICABLE", `Template '${templateId}' cannot be used: ${decision.reason}`, { templateId, reason: decision.reason }));
60496
+ }
60497
+ const spec = template.instantiate(matchCtx);
60498
+ const yaml = [
60499
+ `# Generated by: miao-viz template instantiate ${template.id}`,
60500
+ `# Template: ${template.id} (score: ${decision.score.toFixed(2)})`,
60501
+ "# IMPORTANT: Review chart order and fill structured insights[] before validate",
60502
+ "",
60503
+ templateSpecToYaml(spec)
60504
+ ].join("\n");
60505
+ const outputPath = stringFlag(args, "output");
60506
+ if (outputPath) {
60507
+ writeOutput(outputPath, yaml);
60508
+ return { ok: true, value: { output: outputPath, templateId, score: decision.score } };
60509
+ }
60510
+ return { ok: true, value: { yaml, templateId, score: decision.score } };
60511
+ }
60512
+
60513
+ // src/cli-inspect.ts
60514
+ function runInspect(args) {
60515
+ const input = requiredFlag(args, "input");
60516
+ const specPath = requiredFlag(args, "spec");
60517
+ const contextPath = requiredFlag(args, "context");
60518
+ const output = requiredFlag(args, "output");
60519
+ if (isAgentError(input)) return fail(input);
60520
+ if (isAgentError(specPath)) return fail(specPath);
60521
+ if (isAgentError(contextPath)) return fail(contextPath);
60522
+ if (isAgentError(output)) return fail(output);
60523
+ const dataset = loadDataset(input);
60524
+ if (isAgentError(dataset)) return fail(dataset);
60525
+ const spec = normalizeSpec(readSpec(specPath));
60526
+ if (isAgentError(spec)) return fail(spec);
60527
+ const context = parseAnalyzeContext(readJson(contextPath));
60528
+ if (!context) return fail(agentError("INVALID_CONTEXT", "context.json format is invalid.", { contextPath }));
60529
+ const profile = profileDataset(dataset.value);
60530
+ const charts = spec.charts.map((chart, index) => {
60531
+ const inspected = inspectChartTransforms(dataset.value.rows, chart);
60532
+ return {
60533
+ id: chart.id ?? `chart-${index + 1}`,
60534
+ type: chart.type,
60535
+ transforms: inspected.transforms,
60536
+ encoding: Object.fromEntries(Object.entries(chart.encoding ?? {}).map(([channel, enc]) => {
60537
+ const field = enc?.field;
60538
+ const profileField = field ? profile.columns.find((c) => c.name === field) : void 0;
60539
+ const finalMatch = field ? inspected.rows.some((row) => Object.prototype.hasOwnProperty.call(row, field)) : false;
60540
+ const sourceFieldMatch = Boolean(profileField);
60541
+ return [channel, {
60542
+ field,
60543
+ specType: enc?.type,
60544
+ resolvedType: profileField?.type ?? (finalMatch ? inferFinalFieldType(inspected.rows, field) : void 0),
60545
+ sourceFieldMatch,
60546
+ finalRowsMatch: finalMatch,
60547
+ match: sourceFieldMatch || finalMatch
60548
+ }];
60549
+ }))
60550
+ };
60551
+ });
60552
+ function inferFinalFieldType(rows, field) {
60553
+ if (!field) return void 0;
60554
+ const values = rows.map((row) => row[field]).filter((value) => value !== null && value !== void 0);
60555
+ if (values.length === 0) return void 0;
60556
+ if (values.every((value) => typeof value === "number")) return "number";
60557
+ if (values.every((value) => typeof value === "boolean")) return "boolean";
60558
+ if (values.every((value) => value instanceof Date)) return "date";
60559
+ return "string";
60560
+ }
60561
+ const referenced = /* @__PURE__ */ new Set();
60562
+ for (const insight of normalizeInsights(spec.insights)) {
60563
+ for (const id of insight.evidence) referenced.add(id);
60564
+ for (const ref of parseEvidenceRefs(insight.text)) referenced.add(ref.id);
60565
+ }
60566
+ for (const chart of spec.charts) {
60567
+ if (!chart.title) continue;
60568
+ for (const ref of parseEvidenceRefs(chart.title)) referenced.add(ref.id);
60569
+ }
60570
+ const defined = context.evidence.map((e) => e.id);
60571
+ const result = {
60572
+ ok: true,
60573
+ value: {
60574
+ charts,
60575
+ evidence: {
60576
+ defined,
60577
+ referenced: [...referenced],
60578
+ unreferenced: defined.filter((id) => !referenced.has(id))
60579
+ }
60580
+ }
60581
+ };
60582
+ writeOutput(output, `${JSON.stringify(result.value, null, 2)}
60583
+ `);
60584
+ return result;
60585
+ }
60586
+
59522
60587
  // src/cli.ts
59523
60588
  async function main() {
59524
60589
  const args = parseArgs(process.argv.slice(2));
@@ -59547,6 +60612,14 @@ async function main() {
59547
60612
  printJson(runBlock(args));
59548
60613
  return;
59549
60614
  }
60615
+ if (args.command === "template") {
60616
+ printJson(runTemplate(args));
60617
+ return;
60618
+ }
60619
+ if (args.command === "inspect") {
60620
+ printJson(runInspect(args));
60621
+ return;
60622
+ }
59550
60623
  if (args.command === "render") {
59551
60624
  printJson(runRender(args));
59552
60625
  return;
@@ -59568,7 +60641,7 @@ async function main() {
59568
60641
  return;
59569
60642
  }
59570
60643
  printJson(agentError("UNKNOWN_COMMAND", `Unknown command: ${args.command ?? "(none)"}`, {
59571
- commands: ["profile", "validate", "catalog", "block", "render", "deck", "article", "query", "analyze"]
60644
+ commands: ["profile", "validate", "catalog", "block", "template", "inspect", "render", "deck", "article", "query", "analyze"]
59572
60645
  }));
59573
60646
  process.exitCode = 1;
59574
60647
  } catch (error51) {
@@ -59613,15 +60686,15 @@ function runValidate(args) {
59613
60686
  if (contextPath) {
59614
60687
  const raw = readJson(contextPath);
59615
60688
  const unwrapped = raw.ok === true ? raw.value : raw;
59616
- const parsed = analyzeContextSchema.safeParse(unwrapped);
59617
- if (!parsed.success) {
60689
+ const parsed = parseAnalyzeContext(unwrapped);
60690
+ if (!parsed) {
59618
60691
  return fail(agentError(
59619
60692
  "INVALID_CONTEXT",
59620
- `context.json format is invalid: ${parsed.error.issues.map((i) => i.message).join("; ")}`,
60693
+ "context.json format is invalid.",
59621
60694
  { contextPath }
59622
60695
  ));
59623
60696
  }
59624
- context = parsed.data;
60697
+ context = parsed;
59625
60698
  }
59626
60699
  const warnings = collectValidationWarnings(result.value, profile, context);
59627
60700
  if (args.flags["strict"] === true && context) {
@@ -59653,6 +60726,10 @@ function runValidate(args) {
59653
60726
  if (args.flags["verify"] === true) {
59654
60727
  const verifyWarnings = collectVerifyWarnings(result.value, context);
59655
60728
  warnings.push(...verifyWarnings);
60729
+ if (args.flags["strict"] === true) {
60730
+ const strictResult = strictVerifyError(verifyWarnings);
60731
+ if (isAgentError(strictResult)) return fail(strictResult);
60732
+ }
59656
60733
  }
59657
60734
  if (args.flags["patch-hints"] === true) {
59658
60735
  const warningPatches = collectWarningPatches(result.value);
@@ -59682,6 +60759,17 @@ function runRender(args) {
59682
60759
  if (isAgentError(normalized)) return fail(normalized);
59683
60760
  const validation = validateReportSpec(normalized, profile, formats);
59684
60761
  if (isAgentError(validation)) return fail(validation);
60762
+ const contextPath = stringFlag(args, "context");
60763
+ if (contextPath && validation.value.insights && validation.value.insights.length > 0) {
60764
+ const raw = readJson(contextPath);
60765
+ const unwrapped = raw.ok === true ? raw.value : raw;
60766
+ const parsed = parseAnalyzeContext(unwrapped);
60767
+ if (parsed) {
60768
+ validation.value.insights = validation.value.insights.map(
60769
+ (insight) => mapInsightText(insight, (text) => resolveDirectives(text, parsed.evidence))
60770
+ );
60771
+ }
60772
+ }
59685
60773
  const themeFlag = stringFlag(args, "theme");
59686
60774
  const interactive = args.flags["interactive"] === true ? true : args.flags["no-interactive"] === true ? false : void 0;
59687
60775
  const written = [];
@@ -59762,7 +60850,8 @@ async function runAnalyze(args) {
59762
60850
  extraQuery: stringFlag(args, "extra-query"),
59763
60851
  correctAssumption: stringFlag(args, "correct-assumption")
59764
60852
  });
59765
- const result = { ok: true, value: context };
60853
+ const value = args.flags["compact"] === true ? toCompactAnalyzeContext(context) : context;
60854
+ const result = { ok: true, value };
59766
60855
  const outputPath = stringFlag(args, "output");
59767
60856
  if (outputPath) {
59768
60857
  writeOutput(outputPath, `${JSON.stringify(result, null, 2)}