commit-whisper 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +339 -160
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1168,8 +1168,10 @@ function messageForError(err) {
1168
1168
  }
1169
1169
 
1170
1170
  // src/cli/interactive.ts
1171
+ import pc2 from "picocolors";
1171
1172
  import { isCancel, multiselect as clackMultiselect, select as clackSelect, text as clackText } from "@clack/prompts";
1172
1173
  var LAUNCHPAD_TAGLINE = "commit-whisper \xB7 \u{1F575}\uFE0F I know what you did last commit";
1174
+ var QUIT_MESSAGE = "Case closed. \u{1F575}\uFE0F Until your next commit \u2014 here's the cheatsheet for the road:";
1173
1175
  var FLAGS_CHEATSHEET = [
1174
1176
  "Common commands:",
1175
1177
  " commit-whisper . analyze the current repository",
@@ -1194,6 +1196,11 @@ var TIER_LABEL = {
1194
1196
  "single-device": "Single-device",
1195
1197
  unlimited: "Unlimited"
1196
1198
  };
1199
+ function tierSegment(tier, color) {
1200
+ const c = pc2.createColors(color);
1201
+ const label = TIER_LABEL[tier];
1202
+ return tier === "free" ? c.red(label) : c.green(label);
1203
+ }
1197
1204
  var OUTPUT_FORMAT_OPTIONS = [
1198
1205
  { value: "terminal", label: "terminal" },
1199
1206
  { value: "html", label: "html" },
@@ -1233,8 +1240,8 @@ function cwdSegment(state) {
1233
1240
  }
1234
1241
  return `${state.cwdLabel} (${state.branch ?? "detached"})`;
1235
1242
  }
1236
- function formatReadinessLine(state) {
1237
- return `${TIER_LABEL[state.tier]} \xB7 AI: ${aiSegment(state)} \xB7 cwd: ${cwdSegment(state)}`;
1243
+ function formatReadinessLine(state, color = false) {
1244
+ return `${tierSegment(state.tier, color)} \xB7 AI: ${aiSegment(state)} \xB7 cwd: ${cwdSegment(state)}`;
1238
1245
  }
1239
1246
  function buildLaunchpadOptions(state) {
1240
1247
  const options = [
@@ -1373,7 +1380,11 @@ function formatStatusReport(state, envVars, reachability, config) {
1373
1380
  if (reachability.kind === "not-configured") {
1374
1381
  lines.push("", NO_AI_FIX);
1375
1382
  }
1376
- return lines.join("\n");
1383
+ return paintStatusMarks(lines.join("\n"), config?.color ?? false);
1384
+ }
1385
+ function paintStatusMarks(text, color) {
1386
+ const c = pc2.createColors(color);
1387
+ return text.replaceAll("\u2713", c.green("\u2713")).replaceAll("\u2717", c.red("\u2717"));
1377
1388
  }
1378
1389
  var GUIDED_DATE = /^\d{4}-\d{2}-\d{2}/;
1379
1390
  function formatEquivalentCommand(target, flags) {
@@ -1792,6 +1803,8 @@ var DEFAULT_COFFEE_URL = "https://buymeacoffee.com/georgiosnikitas";
1792
1803
  async function dispatchAction(deps, action, output) {
1793
1804
  switch (action) {
1794
1805
  case "quit":
1806
+ writeLine(output, QUIT_MESSAGE);
1807
+ writeLine(output, "");
1795
1808
  writeLine(output, FLAGS_CHEATSHEET);
1796
1809
  return "quit";
1797
1810
  case "help":
@@ -1836,7 +1849,7 @@ async function runLaunchpad(deps) {
1836
1849
  const pause = deps.waitForKey ?? waitForKey;
1837
1850
  const writeHeader = () => {
1838
1851
  writeLine(output, LAUNCHPAD_TAGLINE);
1839
- writeLine(output, formatReadinessLine(deps.state));
1852
+ writeLine(output, formatReadinessLine(deps.state, deps.doctorConfig?.color));
1840
1853
  };
1841
1854
  if (!repaint) {
1842
1855
  writeHeader();
@@ -3954,7 +3967,7 @@ function narrationReason(err, secret) {
3954
3967
  }
3955
3968
 
3956
3969
  // src/render/terminal/terminal-renderer.ts
3957
- import pc2 from "picocolors";
3970
+ import pc3 from "picocolors";
3958
3971
 
3959
3972
  // src/render/render.port.ts
3960
3973
  function classifyReport(report) {
@@ -4030,6 +4043,196 @@ function classifyHealth(metric) {
4030
4043
  return classifier === void 0 ? "ok" : classifier(metric.value);
4031
4044
  }
4032
4045
 
4046
+ // src/render/html/shape.ts
4047
+ var TIME_BUCKET_KEYS = ["perDay", "perWeek", "perMonth", "perYear"];
4048
+ var DATE_KEY = /^\d{4}(-(\d\d|W\d\d))?(-\d\d)?$/;
4049
+ var RANGE_FIELD = /(pct|share|score)$/i;
4050
+ var LABEL_FIELDS = ["path", "file", "directory", "area", "name", "id", "label", "key"];
4051
+ function isObject(value) {
4052
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4053
+ }
4054
+ function timeBucket(value) {
4055
+ for (const key of TIME_BUCKET_KEYS) {
4056
+ const sub = value[key];
4057
+ if (isObject(sub)) {
4058
+ return sub;
4059
+ }
4060
+ }
4061
+ return void 0;
4062
+ }
4063
+ function isDateKeyedNumbers(value) {
4064
+ const entries = Object.entries(value);
4065
+ return entries.length > 0 && entries.every(([k, v]) => DATE_KEY.test(k) && typeof v === "number" && Number.isFinite(v));
4066
+ }
4067
+ function numericEntries(value) {
4068
+ return Object.entries(value).filter(([, v]) => typeof v === "number" && Number.isFinite(v)).map(([label, v]) => ({ label, value: v }));
4069
+ }
4070
+ function rangeField(value) {
4071
+ if (!isObject(value)) {
4072
+ return void 0;
4073
+ }
4074
+ for (const [k, v] of Object.entries(value)) {
4075
+ if (RANGE_FIELD.test(k) && typeof v === "number" && Number.isFinite(v)) {
4076
+ return { value: v, max: 100 };
4077
+ }
4078
+ }
4079
+ return void 0;
4080
+ }
4081
+ function detectShape(value) {
4082
+ if (typeof value === "number") {
4083
+ return Number.isFinite(value) ? "scalar" : "none";
4084
+ }
4085
+ if (Array.isArray(value)) {
4086
+ return extractSeries(value).length > 0 ? "distribution" : "none";
4087
+ }
4088
+ if (!isObject(value)) {
4089
+ return "none";
4090
+ }
4091
+ if (timeBucket(value) !== void 0 || isDateKeyedNumbers(value)) {
4092
+ return "timeseries";
4093
+ }
4094
+ if (rangeField(value) !== void 0) {
4095
+ return "scalar-range";
4096
+ }
4097
+ const nums = numericEntries(value);
4098
+ if (nums.length >= 2) {
4099
+ return "distribution";
4100
+ }
4101
+ if (nums.length === 1) {
4102
+ return "scalar";
4103
+ }
4104
+ return "none";
4105
+ }
4106
+ function pointFromElement(element, index) {
4107
+ if (typeof element === "number" && Number.isFinite(element)) {
4108
+ return { label: String(index + 1), value: element };
4109
+ }
4110
+ if (!isObject(element)) {
4111
+ return void 0;
4112
+ }
4113
+ const nums = numericEntries(element);
4114
+ const first = nums[0];
4115
+ if (first === void 0) {
4116
+ return void 0;
4117
+ }
4118
+ let label = String(index + 1);
4119
+ for (const fieldName of LABEL_FIELDS) {
4120
+ const candidate = element[fieldName];
4121
+ if (typeof candidate === "string" && candidate !== "") {
4122
+ label = candidate;
4123
+ break;
4124
+ }
4125
+ }
4126
+ return { label, value: first.value };
4127
+ }
4128
+ function extractSeries(value) {
4129
+ if (Array.isArray(value)) {
4130
+ return value.map((el, i) => pointFromElement(el, i)).filter((p) => p !== void 0);
4131
+ }
4132
+ if (!isObject(value)) {
4133
+ return [];
4134
+ }
4135
+ const bucket = timeBucket(value);
4136
+ if (bucket !== void 0) {
4137
+ return numericEntries(bucket);
4138
+ }
4139
+ if (isDateKeyedNumbers(value)) {
4140
+ return numericEntries(value);
4141
+ }
4142
+ return numericEntries(value);
4143
+ }
4144
+
4145
+ // src/render/value-tree.ts
4146
+ var LABEL_FIELDS2 = ["path", "file", "directory", "area", "name", "id", "label", "key"];
4147
+ var PRIMARY_NUMERIC_FIELDS = ["churn", "total", "value", "count", "sum", "score", "commitCount"];
4148
+ var MAX_STRING = 80;
4149
+ function isRecord(value) {
4150
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4151
+ }
4152
+ function isFiniteNumber(value) {
4153
+ return typeof value === "number" && Number.isFinite(value);
4154
+ }
4155
+ function formatScalar(value) {
4156
+ if (value === null || value === void 0) {
4157
+ return "\u2014";
4158
+ }
4159
+ if (typeof value === "string") {
4160
+ return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING - 1)}\u2026` : value;
4161
+ }
4162
+ if (typeof value === "boolean") {
4163
+ return String(value);
4164
+ }
4165
+ if (typeof value === "number") {
4166
+ return Number.isFinite(value) ? String(Math.round(value * 100) / 100) : "0";
4167
+ }
4168
+ return "";
4169
+ }
4170
+ function labelField(record) {
4171
+ for (const key of LABEL_FIELDS2) {
4172
+ const candidate = record[key];
4173
+ if (typeof candidate === "string" && candidate !== "") {
4174
+ return { key, value: candidate };
4175
+ }
4176
+ }
4177
+ return void 0;
4178
+ }
4179
+ function primaryNumeric(record) {
4180
+ for (const key of PRIMARY_NUMERIC_FIELDS) {
4181
+ const candidate = record[key];
4182
+ if (isFiniteNumber(candidate)) {
4183
+ return candidate;
4184
+ }
4185
+ }
4186
+ for (const candidate of Object.values(record)) {
4187
+ if (isFiniteNumber(candidate)) {
4188
+ return candidate;
4189
+ }
4190
+ }
4191
+ return void 0;
4192
+ }
4193
+ function arrayEntry(element, index) {
4194
+ if (isRecord(element)) {
4195
+ const label = labelField(element);
4196
+ const numerics = Object.entries(element).filter(([, v]) => isFiniteNumber(v));
4197
+ if (label !== void 0 && numerics.length === 1) {
4198
+ return { label: label.value, child: { kind: "scalar", text: formatScalar(numerics[0][1]) } };
4199
+ }
4200
+ if (label !== void 0) {
4201
+ const rest = {};
4202
+ for (const [k, v] of Object.entries(element)) {
4203
+ if (k !== label.key) {
4204
+ rest[k] = v;
4205
+ }
4206
+ }
4207
+ return { label: label.value, child: recordTree(rest) };
4208
+ }
4209
+ }
4210
+ return { label: String(index + 1), child: buildValueTree(element) };
4211
+ }
4212
+ function recordTree(record) {
4213
+ const entries = Object.entries(record);
4214
+ const everyHasPrimary = entries.length > 0 && entries.every(([, v]) => isRecord(v) && primaryNumeric(v) !== void 0);
4215
+ if (everyHasPrimary) {
4216
+ return {
4217
+ kind: "branch",
4218
+ entries: entries.map(([key, v]) => ({
4219
+ label: key,
4220
+ child: { kind: "scalar", text: formatScalar(primaryNumeric(v)) }
4221
+ }))
4222
+ };
4223
+ }
4224
+ return { kind: "branch", entries: entries.map(([key, v]) => ({ label: key, child: buildValueTree(v) })) };
4225
+ }
4226
+ function buildValueTree(value) {
4227
+ if (Array.isArray(value)) {
4228
+ return { kind: "branch", entries: value.map((element, i) => arrayEntry(element, i)) };
4229
+ }
4230
+ if (isRecord(value)) {
4231
+ return recordTree(value);
4232
+ }
4233
+ return { kind: "scalar", text: formatScalar(value) };
4234
+ }
4235
+
4033
4236
  // src/render/terminal/terminal-renderer.ts
4034
4237
  var GROUPS = [
4035
4238
  { id: "A", title: "Activity & Cadence", description: "How the project moves over time." },
@@ -4042,7 +4245,7 @@ var GROUPS = [
4042
4245
  var DEGRADED_BANNER = "\u26A0 Narrative unavailable \u2014 raw analysis below";
4043
4246
  var METRICS_ONLY_NOTE = "Metrics-only run \u2014 no AI narrative requested";
4044
4247
  function renderTerminal(report, opts = {}) {
4045
- const c = opts.color === void 0 ? pc2.createColors() : pc2.createColors(opts.color);
4248
+ const c = opts.color === void 0 ? pc3.createColors() : pc3.createColors(opts.color);
4046
4249
  const route = classifyReport(report);
4047
4250
  return route.kind === "showpiece" ? renderShowpiece(route.report, report.provenance, c) : renderSubstrate(route.analysis, route.framing, report.provenance, c);
4048
4251
  }
@@ -4193,7 +4396,35 @@ function valueBullet(metric, c) {
4193
4396
  const note = `not available${reason}`;
4194
4397
  return `${label} \u2014 ${c.dim(note)}`;
4195
4398
  }
4196
- return `${label} \u2014 ${formatValue2(metric.value)}`;
4399
+ const value = metric.value;
4400
+ if (value === null || typeof value !== "object") {
4401
+ return `${label} \u2014 ${formatValue2(value)}`;
4402
+ }
4403
+ const series = extractSeries(value);
4404
+ if (series.length === 1) {
4405
+ return `${label} \u2014 ${formatNumber(series[0].value)}`;
4406
+ }
4407
+ if (series.length > 1) {
4408
+ const rows = series.map((point) => ` ${c.dim("-")} ${point.label}: ${formatNumber(point.value)}`);
4409
+ return [label, ...rows].join("\n");
4410
+ }
4411
+ const tree = buildValueTree(value);
4412
+ if (tree.kind === "scalar") {
4413
+ return `${label} \u2014 ${tree.text}`;
4414
+ }
4415
+ return [label, ...treeLines(tree.entries, " ", c)].join("\n");
4416
+ }
4417
+ function treeLines(entries, indent, c) {
4418
+ return entries.flatMap((entry) => {
4419
+ if (entry.child.kind === "scalar") {
4420
+ const text = entry.child.text === "" ? entry.label : `${entry.label}: ${entry.child.text}`;
4421
+ return [`${indent}${c.dim("-")} ${text}`];
4422
+ }
4423
+ return [`${indent}${c.dim("-")} ${entry.label}`, ...treeLines(entry.child.entries, `${indent} `, c)];
4424
+ });
4425
+ }
4426
+ function formatNumber(n) {
4427
+ return Number.isFinite(n) ? String(Math.round(n * 100) / 100) : "0";
4197
4428
  }
4198
4429
  function facetBullets(explanation, c) {
4199
4430
  return [
@@ -4213,6 +4444,12 @@ function formatValue2(value) {
4213
4444
  if (value === void 0) {
4214
4445
  return "";
4215
4446
  }
4447
+ if (typeof value === "string") {
4448
+ return value.length > 60 ? `${value.slice(0, 59)}\u2026` : value;
4449
+ }
4450
+ if (typeof value === "number" || typeof value === "boolean") {
4451
+ return String(value);
4452
+ }
4216
4453
  const json = JSON.stringify(value);
4217
4454
  if (json === void 0) {
4218
4455
  return "";
@@ -4225,105 +4462,6 @@ function escapeHtml(text) {
4225
4462
  return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4226
4463
  }
4227
4464
 
4228
- // src/render/html/shape.ts
4229
- var TIME_BUCKET_KEYS = ["perDay", "perWeek", "perMonth", "perYear"];
4230
- var DATE_KEY = /^\d{4}(-(\d\d|W\d\d))?(-\d\d)?$/;
4231
- var RANGE_FIELD = /(pct|share|score)$/i;
4232
- var LABEL_FIELDS = ["path", "file", "directory", "area", "name", "id", "label", "key"];
4233
- function isObject(value) {
4234
- return value !== null && typeof value === "object" && !Array.isArray(value);
4235
- }
4236
- function timeBucket(value) {
4237
- for (const key of TIME_BUCKET_KEYS) {
4238
- const sub = value[key];
4239
- if (isObject(sub)) {
4240
- return sub;
4241
- }
4242
- }
4243
- return void 0;
4244
- }
4245
- function isDateKeyedNumbers(value) {
4246
- const entries = Object.entries(value);
4247
- return entries.length > 0 && entries.every(([k, v]) => DATE_KEY.test(k) && typeof v === "number" && Number.isFinite(v));
4248
- }
4249
- function numericEntries(value) {
4250
- return Object.entries(value).filter(([, v]) => typeof v === "number" && Number.isFinite(v)).map(([label, v]) => ({ label, value: v }));
4251
- }
4252
- function rangeField(value) {
4253
- if (!isObject(value)) {
4254
- return void 0;
4255
- }
4256
- for (const [k, v] of Object.entries(value)) {
4257
- if (RANGE_FIELD.test(k) && typeof v === "number" && Number.isFinite(v)) {
4258
- return { value: v, max: 100 };
4259
- }
4260
- }
4261
- return void 0;
4262
- }
4263
- function detectShape(value) {
4264
- if (typeof value === "number") {
4265
- return Number.isFinite(value) ? "scalar" : "none";
4266
- }
4267
- if (Array.isArray(value)) {
4268
- return extractSeries(value).length > 0 ? "distribution" : "none";
4269
- }
4270
- if (!isObject(value)) {
4271
- return "none";
4272
- }
4273
- if (timeBucket(value) !== void 0 || isDateKeyedNumbers(value)) {
4274
- return "timeseries";
4275
- }
4276
- if (rangeField(value) !== void 0) {
4277
- return "scalar-range";
4278
- }
4279
- const nums = numericEntries(value);
4280
- if (nums.length >= 2) {
4281
- return "distribution";
4282
- }
4283
- if (nums.length === 1) {
4284
- return "scalar";
4285
- }
4286
- return "none";
4287
- }
4288
- function pointFromElement(element, index) {
4289
- if (typeof element === "number" && Number.isFinite(element)) {
4290
- return { label: String(index + 1), value: element };
4291
- }
4292
- if (!isObject(element)) {
4293
- return void 0;
4294
- }
4295
- const nums = numericEntries(element);
4296
- const first = nums[0];
4297
- if (first === void 0) {
4298
- return void 0;
4299
- }
4300
- let label = String(index + 1);
4301
- for (const fieldName of LABEL_FIELDS) {
4302
- const candidate = element[fieldName];
4303
- if (typeof candidate === "string" && candidate !== "") {
4304
- label = candidate;
4305
- break;
4306
- }
4307
- }
4308
- return { label, value: first.value };
4309
- }
4310
- function extractSeries(value) {
4311
- if (Array.isArray(value)) {
4312
- return value.map((el, i) => pointFromElement(el, i)).filter((p) => p !== void 0);
4313
- }
4314
- if (!isObject(value)) {
4315
- return [];
4316
- }
4317
- const bucket = timeBucket(value);
4318
- if (bucket !== void 0) {
4319
- return numericEntries(bucket);
4320
- }
4321
- if (isDateKeyedNumbers(value)) {
4322
- return numericEntries(value);
4323
- }
4324
- return numericEntries(value);
4325
- }
4326
-
4327
4465
  // src/render/html/svg.ts
4328
4466
  function safe(n) {
4329
4467
  return Number.isFinite(n) ? n : 0;
@@ -4337,7 +4475,7 @@ function esc(text) {
4337
4475
  function hashId(text) {
4338
4476
  let h = 2166136261;
4339
4477
  for (let i = 0; i < text.length; i++) {
4340
- h ^= text.charCodeAt(i);
4478
+ h ^= text.codePointAt(i) ?? 0;
4341
4479
  h = Math.imul(h, 16777619);
4342
4480
  }
4343
4481
  return (h >>> 0).toString(36);
@@ -4372,7 +4510,11 @@ function niceStep(range, n) {
4372
4510
  const raw = range / n;
4373
4511
  const exp = Math.floor(Math.log10(raw));
4374
4512
  const f = raw / 10 ** exp;
4375
- const nf = f < 1.5 ? 1 : f < 3 ? 2 : f < 7 ? 5 : 10;
4513
+ let nf;
4514
+ if (f < 1.5) nf = 1;
4515
+ else if (f < 3) nf = 2;
4516
+ else if (f < 7) nf = 5;
4517
+ else nf = 10;
4376
4518
  return nf * 10 ** exp;
4377
4519
  }
4378
4520
  function valueTicks(max) {
@@ -4455,11 +4597,13 @@ function svgLine(series, label) {
4455
4597
  const yAt = (v) => y1 - Math.max(0, safe(v)) / top * (y1 - y0);
4456
4598
  const coords = series.map((p, i) => [r(xAt(i)), r(yAt(p.value))]);
4457
4599
  const line = smoothPath(coords);
4458
- const area = `${line} L ${r(coords[coords.length - 1][0])} ${y1} L ${r(coords[0][0])} ${y1} Z`;
4600
+ const last = coords.at(-1);
4601
+ const area = `${line} L ${r(last[0])} ${y1} L ${r(coords[0][0])} ${y1} Z`;
4459
4602
  const every = series.length > 12 ? Math.ceil(series.length / 12) : 1;
4460
4603
  const xLabels = series.map((p, i) => i % every === 0 ? `<text class="chart-label" x="${r(xAt(i))}" y="${H - 10}" text-anchor="middle">${esc(tickLabel(p.label))}</text>` : "").join("");
4461
4604
  const dot = coords.length === 1 ? `<circle class="chart-dot" cx="${r(coords[0][0])}" cy="${r(coords[0][1])}" r="4"/>` : "";
4462
- return `${open(label, "chart-line", `0 0 ${W} ${H}`)}${areaGradient(areaId)}${fillGradient(strokeId, false)}${valueGrid(ticks, top, x0, x1, y0, y1)}<line class="chart-axis" x1="${x0}" y1="${y1}" x2="${x1}" y2="${y1}"/><path class="chart-area" d="${area}" fill="url(#${areaId})"/><path class="chart-stroke" d="${line}" fill="none" stroke="url(#${strokeId})" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>${dot}${xLabels}</svg>`;
4605
+ const viewBox = `0 0 ${W} ${H}`;
4606
+ return `${open(label, "chart-line", viewBox)}${areaGradient(areaId)}${fillGradient(strokeId, false)}${valueGrid(ticks, top, x0, x1, y0, y1)}<line class="chart-axis" x1="${x0}" y1="${y1}" x2="${x1}" y2="${y1}"/><path class="chart-area" d="${area}" fill="url(#${areaId})"/><path class="chart-stroke" d="${line}" fill="none" stroke="url(#${strokeId})" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>${dot}${xLabels}</svg>`;
4463
4607
  }
4464
4608
  function svgBars(series, label) {
4465
4609
  if (series.length === 0) return empty(label, "chart-bars");
@@ -4484,7 +4628,8 @@ function svgBars(series, label) {
4484
4628
  return roundedTopRect(x, y1 - h, barW, h, rad, id);
4485
4629
  }).join("");
4486
4630
  const xLabels = series.map((p, i) => `<text class="chart-label" x="${r(x0 + i * slot + slot / 2)}" y="${H - 10}" text-anchor="middle">${esc(tickLabel(p.label))}</text>`).join("");
4487
- return `${open(label, "chart-bars", `0 0 ${W} ${H}`)}${fillGradient(id, true)}${valueGrid(ticks, top, x0, x1, y0, y1)}<line class="chart-axis" x1="${x0}" y1="${y1}" x2="${x1}" y2="${y1}"/>${bars}${xLabels}</svg>`;
4631
+ const viewBox = `0 0 ${W} ${H}`;
4632
+ return `${open(label, "chart-bars", viewBox)}${fillGradient(id, true)}${valueGrid(ticks, top, x0, x1, y0, y1)}<line class="chart-axis" x1="${x0}" y1="${y1}" x2="${x1}" y2="${y1}"/>${bars}${xLabels}</svg>`;
4488
4633
  }
4489
4634
  function svgHBars(series, label) {
4490
4635
  if (series.length === 0) return empty(label, "chart-hbars");
@@ -4513,7 +4658,8 @@ function svgHBars(series, label) {
4513
4658
  return roundedRightRect(x0, y, w, barH, Math.min(barH / 2, 5), id);
4514
4659
  }).join("");
4515
4660
  const yLabels = series.map((p, i) => `<text class="chart-label" x="${x0 - 8}" y="${r(y0 + i * rowH + rowH / 2 + 3.5)}" text-anchor="end">${esc(tickLabel(p.label))}</text>`).join("");
4516
- return `${open(label, "chart-hbars", `0 0 ${W} ${H}`)}${fillGradient(id, false)}${grid}<line class="chart-axis" x1="${x0}" y1="${y0}" x2="${x0}" y2="${y1}"/>${bars}${yLabels}</svg>`;
4661
+ const viewBox = `0 0 ${W} ${H}`;
4662
+ return `${open(label, "chart-hbars", viewBox)}${fillGradient(id, false)}${grid}<line class="chart-axis" x1="${x0}" y1="${y0}" x2="${x0}" y2="${y1}"/>${bars}${yLabels}</svg>`;
4517
4663
  }
4518
4664
  function svgRadar(points, max, label) {
4519
4665
  if (points.length < 3) {
@@ -4543,11 +4689,15 @@ function svgRadar(points, max, label) {
4543
4689
  const labels = points.map((p, i) => {
4544
4690
  const lx = cx + Math.cos(angle(i)) * (radius + 16);
4545
4691
  const ly = cy + Math.sin(angle(i)) * (radius + 16);
4546
- const anchor = lx > cx + 1 ? "start" : lx < cx - 1 ? "end" : "middle";
4692
+ let anchor;
4693
+ if (lx > cx + 1) anchor = "start";
4694
+ else if (lx < cx - 1) anchor = "end";
4695
+ else anchor = "middle";
4547
4696
  const name = p.label.length > 12 ? `${p.label.slice(0, 11)}\u2026` : p.label;
4548
4697
  return `<text class="radar-label" x="${r(lx)}" y="${r(ly + 3)}" text-anchor="${anchor}">${esc(name)}</text>`;
4549
4698
  }).join("");
4550
- return `${open(label, "chart-radar", `0 0 ${W} ${H}`)}${fillGradient(id, true)}${rings}${axes}<polygon class="radar-area" points="${dataPts}" fill="url(#${id})"/>${dots}${labels}</svg>`;
4699
+ const viewBox = `0 0 ${W} ${H}`;
4700
+ return `${open(label, "chart-radar", viewBox)}${fillGradient(id, true)}${rings}${axes}<polygon class="radar-area" points="${dataPts}" fill="url(#${id})"/>${dots}${labels}</svg>`;
4551
4701
  }
4552
4702
  function svgRadialGauge(value, max, label) {
4553
4703
  const id = `cw-rgauge-${hashId(label)}`;
@@ -4605,7 +4755,7 @@ var GROUP_DESCRIPTION = {
4605
4755
  F: "Overall repository health signals."
4606
4756
  };
4607
4757
  function dataTable(series, valueHeader, caption) {
4608
- const rows = series.map((p) => `<tr><th scope="row">${escapeHtml(p.label)}</th><td>${escapeHtml(formatNumber(p.value))}</td></tr>`).join("\n");
4758
+ const rows = series.map((p) => `<tr><th scope="row">${escapeHtml(p.label)}</th><td>${escapeHtml(formatNumber2(p.value))}</td></tr>`).join("\n");
4609
4759
  return `<details class="data-table" open>
4610
4760
  <summary>Show data table</summary>
4611
4761
  <table>
@@ -4617,7 +4767,7 @@ ${rows}
4617
4767
  </table>
4618
4768
  </details>`;
4619
4769
  }
4620
- function formatNumber(value) {
4770
+ function formatNumber2(value) {
4621
4771
  if (!Number.isFinite(value)) {
4622
4772
  return "\u2014";
4623
4773
  }
@@ -4660,7 +4810,23 @@ function renderSubChart(group, spec, metrics) {
4660
4810
  return subFigure(metric.title, svgRadialGauge(range.value, range.max, label), table);
4661
4811
  }
4662
4812
  const series = extractSeries(metric.value);
4663
- const svg = spec.kind === "line" ? svgLine(series, label) : spec.kind === "bars" ? svgBars(series, label) : spec.kind === "hbars" ? svgHBars(series, label) : spec.kind === "radar" ? svgRadar(series, 100, label) : svgDonut(series, label);
4813
+ let svg;
4814
+ switch (spec.kind) {
4815
+ case "line":
4816
+ svg = svgLine(series, label);
4817
+ break;
4818
+ case "bars":
4819
+ svg = svgBars(series, label);
4820
+ break;
4821
+ case "hbars":
4822
+ svg = svgHBars(series, label);
4823
+ break;
4824
+ case "radar":
4825
+ svg = svgRadar(series, 100, label);
4826
+ break;
4827
+ default:
4828
+ svg = svgDonut(series, label);
4829
+ }
4664
4830
  return subFigure(metric.title, svg, dataTable(series, "Value", metric.title));
4665
4831
  }
4666
4832
  function groupOverviewPanel(group, metrics) {
@@ -4923,19 +5089,20 @@ function metricStat(metric) {
4923
5089
  return `${fmtStat(range.value)}${range.max === 100 ? "%" : ""}`;
4924
5090
  }
4925
5091
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
4926
- const nums = Object.entries(value).filter((e) => typeof e[1] === "number" && Number.isFinite(e[1]));
4927
- for (const key of ["total", "count", "score", "busFactor", "value"]) {
4928
- const hit = nums.find(([k]) => k === key);
4929
- if (hit !== void 0) {
4930
- return fmtStat(hit[1]);
4931
- }
4932
- }
4933
- if (nums.length === 1) {
4934
- return fmtStat(nums[0][1]);
4935
- }
5092
+ return objectStat(value);
4936
5093
  }
4937
5094
  return "";
4938
5095
  }
5096
+ function objectStat(value) {
5097
+ const nums = Object.entries(value).filter((e) => typeof e[1] === "number" && Number.isFinite(e[1]));
5098
+ for (const key of ["total", "count", "score", "busFactor", "value"]) {
5099
+ const hit = nums.find(([k]) => k === key);
5100
+ if (hit !== void 0) {
5101
+ return fmtStat(hit[1]);
5102
+ }
5103
+ }
5104
+ return nums.length === 1 ? fmtStat(nums[0][1]) : "";
5105
+ }
4939
5106
  function fmtStat(n) {
4940
5107
  return Number.isInteger(n) ? String(n) : String(Math.round(n * 100) / 100);
4941
5108
  }
@@ -4972,7 +5139,7 @@ function formatCount2(n) {
4972
5139
  function isoDate2(iso) {
4973
5140
  return iso.slice(0, 10);
4974
5141
  }
4975
- var STYLE = `
5142
+ var STYLE = String.raw`
4976
5143
  :root {
4977
5144
  color-scheme: dark light;
4978
5145
  --bg: #0a0e14; --surface: #11161f; --surface-2: #161c28;
@@ -5031,7 +5198,7 @@ a:focus-visible, :focus-visible { outline: 2px solid var(--accent); outline-offs
5031
5198
  }
5032
5199
  .masthead h1 { margin: 0; font-size: 1.95rem; position: relative; display: flex; align-items: center; gap: 0.75rem; }
5033
5200
  .masthead h1::before {
5034
- content: "\\25D1"; display: inline-grid; place-items: center;
5201
+ content: "\25D1"; display: inline-grid; place-items: center;
5035
5202
  width: 2.7rem; height: 2.7rem; font-size: 1.45rem; color: #fff;
5036
5203
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
5037
5204
  border-radius: 0.72rem; box-shadow: 0 10px 26px -8px rgba(124,92,255,0.75);
@@ -5075,7 +5242,7 @@ a:focus-visible, :focus-visible { outline: 2px solid var(--accent); outline-offs
5075
5242
  .lead, .explanation p, .coaching-intro, .coaching-closing { color: var(--fg-soft); }
5076
5243
  .key-findings { list-style: none; padding: 0; margin: 1.2rem 0 0; display: grid; gap: 0.6rem; }
5077
5244
  .key-findings li { position: relative; padding: 0.8rem 1rem 0.8rem 2.5rem; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; }
5078
- .key-findings li::before { content: "\\203A"; position: absolute; left: 1rem; top: 0.7rem; color: var(--accent); font-weight: 800; font-size: 1.1rem; }
5245
+ .key-findings li::before { content: "\203A"; position: absolute; left: 1rem; top: 0.7rem; color: var(--accent); font-weight: 800; font-size: 1.1rem; }
5079
5246
  .chapter { background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--accent-2); border-radius: 14px; padding: 1.1rem 1.35rem; margin: 1rem 0; }
5080
5247
  .chapter h3 { margin: 0 0 0.6rem; }
5081
5248
  .chapter ol { margin: 0; padding-left: 1.2rem; display: grid; gap: 0.5rem; color: var(--fg-soft); }
@@ -5246,25 +5413,6 @@ function textBars(series) {
5246
5413
  });
5247
5414
  return ["```", ...rows, "```"].join("\n");
5248
5415
  }
5249
- function mermaidLabel(text) {
5250
- const cleaned = text.replaceAll(/["[\],\r\n\t]+/g, " ").replaceAll(/\s+/g, " ").trim();
5251
- return cleaned === "" ? "-" : cleaned;
5252
- }
5253
- function mermaidXychart(series, title2) {
5254
- if (series.length === 0) {
5255
- return "";
5256
- }
5257
- const axis = series.map((p) => `"${mermaidLabel(p.label)}"`).join(", ");
5258
- const values = series.map((p) => round2(p.value)).join(", ");
5259
- return [
5260
- "```mermaid",
5261
- "xychart-beta",
5262
- ` title "${mermaidLabel(title2)}"`,
5263
- ` x-axis [${axis}]`,
5264
- ` bar [${values}]`,
5265
- "```"
5266
- ].join("\n");
5267
- }
5268
5416
  function representativeSeries(metrics) {
5269
5417
  for (const metric of metrics) {
5270
5418
  if (metric.status !== "computed") {
@@ -5276,20 +5424,17 @@ function representativeSeries(metrics) {
5276
5424
  }
5277
5425
  const series = extractSeries(metric.value);
5278
5426
  if (series.length > 0) {
5279
- return { series, shape };
5427
+ return series;
5280
5428
  }
5281
5429
  }
5282
5430
  return void 0;
5283
5431
  }
5284
- function groupOverview(group, metrics) {
5285
- const rep = representativeSeries(metrics);
5286
- if (rep === void 0) {
5432
+ function groupOverview(_group, metrics) {
5433
+ const series = representativeSeries(metrics);
5434
+ if (series === void 0) {
5287
5435
  return GROUP_OVERVIEW_NONE;
5288
5436
  }
5289
- if (rep.shape === "timeseries") {
5290
- return mermaidXychart(rep.series, `Group ${group} overview`);
5291
- }
5292
- return textBars(rep.series);
5437
+ return textBars(series);
5293
5438
  }
5294
5439
  function scalarNumber(value) {
5295
5440
  if (typeof value === "number") {
@@ -5517,14 +5662,48 @@ function metricBullets(metric, explanations) {
5517
5662
  if (explanation === void 0) {
5518
5663
  return value;
5519
5664
  }
5520
- return [value, ...facetBullets2(explanation)].join("\n");
5665
+ const separator = value.includes("\n") ? "\n\n" : "\n";
5666
+ return [value, facetBullets2(explanation).join("\n")].join(separator);
5521
5667
  }
5522
5668
  function valueBullet2(metric) {
5523
5669
  if (metric.status !== "computed") {
5524
5670
  const reason = metric.reason === void 0 ? "" : ` \u2014 ${escapeCell(metric.reason)}`;
5525
5671
  return `- **Value** \u2014 _not available${reason}_`;
5526
5672
  }
5527
- return `- **Value** \u2014 ${escapeCell(formatValue3(metric.value))}`;
5673
+ const value = metric.value;
5674
+ if (value === null || typeof value !== "object") {
5675
+ return `- **Value** \u2014 ${escapeCell(formatValue3(value))}`;
5676
+ }
5677
+ const series = extractSeries(value);
5678
+ if (series.length === 0) {
5679
+ const tree = buildValueTree(value);
5680
+ if (tree.kind === "scalar") {
5681
+ return `- **Value** \u2014 ${escapeCell(tree.text)}`;
5682
+ }
5683
+ return [`- **Value**`, ...treeBullets(tree.entries, " ")].join("\n");
5684
+ }
5685
+ if (series.length === 1) {
5686
+ return `- **Value** \u2014 ${escapeCell(formatNumber3(series[0].value))}`;
5687
+ }
5688
+ return valueTable(value);
5689
+ }
5690
+ function treeBullets(entries, indent) {
5691
+ return entries.flatMap((entry) => {
5692
+ if (entry.child.kind === "scalar") {
5693
+ const text = entry.child.text === "" ? escapeCell(entry.label) : `${escapeCell(entry.label)}: ${escapeCell(entry.child.text)}`;
5694
+ return [`${indent}- ${text}`];
5695
+ }
5696
+ return [`${indent}- ${escapeCell(entry.label)}`, ...treeBullets(entry.child.entries, `${indent} `)];
5697
+ });
5698
+ }
5699
+ function formatNumber3(n) {
5700
+ return Number.isFinite(n) ? String(Math.round(n * 100) / 100) : "0";
5701
+ }
5702
+ function valueTable(value) {
5703
+ const shape = detectShape(value);
5704
+ const labelHeader = shape === "timeseries" ? "Period" : shape === "distribution" ? "Item" : "Field";
5705
+ const rows = extractSeries(value).map((point) => `| ${escapeCell(point.label)} | ${escapeCell(formatNumber3(point.value))} |`);
5706
+ return [`- **Value**`, "", `| ${labelHeader} | Value |`, "| --- | --- |", ...rows].join("\n");
5528
5707
  }
5529
5708
  function facetBullets2(explanation) {
5530
5709
  return [
@@ -6010,7 +6189,7 @@ function localName(target) {
6010
6189
  }
6011
6190
 
6012
6191
  // src/cli/version.ts
6013
- var VERSION = "1.1.0";
6192
+ var VERSION = "1.1.1";
6014
6193
 
6015
6194
  // src/cli/write-file.ts
6016
6195
  import { writeFile as fsWriteFile } from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-whisper",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Deterministic git history analysis with a grounded, BYOK AI narrative — terminal-native CLI.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",