commit-whisper 1.1.0 → 1.1.2

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 +482 -188
  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. Until your next commit \u2014 here's the cheatsheet for the road \u{1F6E3}\uFE0F";
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) {
@@ -1461,12 +1472,12 @@ async function waitForKey(output) {
1461
1472
  const wasRaw = stdin.isRaw === true;
1462
1473
  stdin.setRawMode?.(true);
1463
1474
  stdin.resume();
1464
- return await new Promise((resolve) => {
1475
+ return await new Promise((resolve2) => {
1465
1476
  stdin.once("data", (data) => {
1466
1477
  stdin.setRawMode?.(wasRaw);
1467
1478
  stdin.pause();
1468
1479
  const key = data.toString("utf8");
1469
- resolve(key === "" || key === "\x1B" ? "quit" : "continue");
1480
+ resolve2(key === "" || key === "\x1B" ? "quit" : "continue");
1470
1481
  });
1471
1482
  });
1472
1483
  }
@@ -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();
@@ -3914,7 +3927,7 @@ function assertNeverProvider(provider) {
3914
3927
 
3915
3928
  // src/narrate/narrate.ts
3916
3929
  function createNarrate(deps = {}) {
3917
- const resolve = deps.resolveModel ?? resolveModel;
3930
+ const resolve2 = deps.resolveModel ?? resolveModel;
3918
3931
  const generate = deps.generate ?? generateNarrative;
3919
3932
  const generateExpl = deps.generateExplanations ?? generateExplanations;
3920
3933
  return async (analysis, config) => {
@@ -3922,7 +3935,7 @@ function createNarrate(deps = {}) {
3922
3935
  return { kind: "skipped" };
3923
3936
  }
3924
3937
  try {
3925
- const model = resolve(config);
3938
+ const model = resolve2(config);
3926
3939
  const [parts, explanations] = await Promise.all([
3927
3940
  generate(model, analysis),
3928
3941
  generateExpl(model, analysis)
@@ -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)}`;
@@ -4575,6 +4725,13 @@ function svgDonut(series, label) {
4575
4725
  const a0 = (-90 + cum * 360) * Math.PI / 180;
4576
4726
  const a1 = (-90 + (cum + frac) * 360) * Math.PI / 180;
4577
4727
  cum += frac;
4728
+ if (frac <= 0) {
4729
+ return "";
4730
+ }
4731
+ if (frac >= 0.999) {
4732
+ const ring = `M ${cx} ${cy - rOuter} A ${rOuter} ${rOuter} 0 1 1 ${cx} ${cy + rOuter} A ${rOuter} ${rOuter} 0 1 1 ${cx} ${cy - rOuter} Z M ${cx} ${cy - rInner} A ${rInner} ${rInner} 0 1 0 ${cx} ${cy + rInner} A ${rInner} ${rInner} 0 1 0 ${cx} ${cy - rInner} Z`;
4733
+ return `<path class="donut-seg slice-${i % 6}" d="${ring}"/>`;
4734
+ }
4578
4735
  const large = frac > 0.5 ? 1 : 0;
4579
4736
  const x0o = r(cx + rOuter * Math.cos(a0));
4580
4737
  const y0o = r(cy + rOuter * Math.sin(a0));
@@ -4605,7 +4762,7 @@ var GROUP_DESCRIPTION = {
4605
4762
  F: "Overall repository health signals."
4606
4763
  };
4607
4764
  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");
4765
+ const rows = series.map((p) => `<tr><th scope="row">${escapeHtml(p.label)}</th><td>${escapeHtml(formatNumber2(p.value))}</td></tr>`).join("\n");
4609
4766
  return `<details class="data-table" open>
4610
4767
  <summary>Show data table</summary>
4611
4768
  <table>
@@ -4617,26 +4774,127 @@ ${rows}
4617
4774
  </table>
4618
4775
  </details>`;
4619
4776
  }
4620
- function formatNumber(value) {
4777
+ function formatNumber2(value) {
4621
4778
  if (!Number.isFinite(value)) {
4622
4779
  return "\u2014";
4623
4780
  }
4624
4781
  return Number.isInteger(value) ? String(value) : String(Math.round(value * 100) / 100);
4625
4782
  }
4626
- var GROUP_CHARTS = {
4627
- A: [{ kind: "line", pick: "timeseries", index: 0 }, { kind: "bars", pick: "timeseries", index: 1 }],
4628
- B: [{ kind: "donut", pick: "distribution", index: 0 }, { kind: "gauge", pick: "range", index: 0 }],
4629
- C: [{ kind: "bars", pick: "distribution", index: 0 }, { kind: "gauge", pick: "range", index: 0 }],
4630
- D: [{ kind: "line", pick: "timeseries", index: 0 }, { kind: "gauge", pick: "range", index: 0 }],
4631
- E: [{ kind: "hbars", pick: "distribution", index: 0 }, { kind: "line", pick: "timeseries", index: 0 }],
4632
- F: [{ kind: "radar", pick: "distribution", index: 0 }, { kind: "gauge", pick: "range", index: 0 }]
4633
- };
4634
- function metricsOfShape(metrics, shape) {
4635
- return metrics.filter((m) => m.status === "computed" && detectShape(m.value) === shape && extractSeries(m.value).length > 0);
4783
+ function asRecord(value) {
4784
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
4785
+ }
4786
+ function objectSeries(value) {
4787
+ return Object.entries(asRecord(value)).filter(([, v]) => typeof v === "number" && Number.isFinite(v)).map(([label, v]) => ({ label, value: v }));
4788
+ }
4789
+ function pickFields(value, fields) {
4790
+ const obj = asRecord(value);
4791
+ const out = [];
4792
+ for (const pair of fields) {
4793
+ const key = pair[0];
4794
+ if (typeof key !== "string") {
4795
+ continue;
4796
+ }
4797
+ const v = obj[key];
4798
+ if (typeof v === "number" && Number.isFinite(v)) {
4799
+ out.push({ label: pair[1] ?? key, value: v });
4800
+ }
4801
+ }
4802
+ return out;
4636
4803
  }
4637
- function rangeMetrics(metrics) {
4638
- return metrics.filter((m) => m.status === "computed" && rangeField(m.value) !== void 0);
4804
+ function pctField(value, key) {
4805
+ const v = asRecord(value)[key];
4806
+ return typeof v === "number" && Number.isFinite(v) ? { value: v, max: 100 } : void 0;
4639
4807
  }
4808
+ function baseName(path) {
4809
+ const parts = path.split("/").filter((p) => p !== "");
4810
+ return parts.at(-1) ?? path;
4811
+ }
4812
+ function rowsSeries(value, key, field2, limit) {
4813
+ const rows = asRecord(value)[key];
4814
+ if (!Array.isArray(rows)) {
4815
+ return [];
4816
+ }
4817
+ const out = [];
4818
+ for (const row of rows.slice(0, limit)) {
4819
+ const obj = asRecord(row);
4820
+ const v = obj[field2];
4821
+ if (typeof v === "number" && Number.isFinite(v) && typeof obj.path === "string") {
4822
+ out.push({ label: baseName(obj.path), value: v });
4823
+ }
4824
+ }
4825
+ return out;
4826
+ }
4827
+ function contributorSplit(value) {
4828
+ const obj = asRecord(value);
4829
+ const active = typeof obj.active === "number" ? obj.active : 0;
4830
+ const total = typeof obj.total === "number" ? obj.total : active;
4831
+ return [
4832
+ { label: "active", value: active },
4833
+ { label: "inactive", value: Math.max(0, total - active) }
4834
+ ];
4835
+ }
4836
+ function hygieneDimensions(value) {
4837
+ const obj = asRecord(value);
4838
+ const out = [];
4839
+ for (const key of ["strengths", "weaknesses"]) {
4840
+ const arr = obj[key];
4841
+ if (!Array.isArray(arr)) {
4842
+ continue;
4843
+ }
4844
+ for (const entry of arr) {
4845
+ const dim = asRecord(entry);
4846
+ if (typeof dim.name === "string" && typeof dim.subScore === "number" && Number.isFinite(dim.subScore)) {
4847
+ out.push({ label: dim.name, value: dim.subScore });
4848
+ }
4849
+ }
4850
+ }
4851
+ return out;
4852
+ }
4853
+ function churnByMonth(value) {
4854
+ const out = [];
4855
+ for (const [label, bucket] of Object.entries(asRecord(asRecord(value).perMonth))) {
4856
+ const churn = asRecord(bucket).churn;
4857
+ if (typeof churn === "number" && Number.isFinite(churn)) {
4858
+ out.push({ label, value: churn });
4859
+ }
4860
+ }
4861
+ return out;
4862
+ }
4863
+ function subjectLengthSeries(value) {
4864
+ return pickFields(asRecord(value).subjectLength, [
4865
+ ["min", "Min"],
4866
+ ["median", "Median"],
4867
+ ["mean", "Mean"],
4868
+ ["p90", "p90"],
4869
+ ["max", "Max"]
4870
+ ]);
4871
+ }
4872
+ var CHART_PLAN = {
4873
+ A: [
4874
+ { title: "Commit volume over time", sourceId: "a-commit-volume", kind: "line", series: (v) => objectSeries(asRecord(v).perMonth) },
4875
+ { title: "Commit frequency / cadence", sourceId: "a-commit-volume", kind: "bars", series: (v) => objectSeries(asRecord(v).perWeek) }
4876
+ ],
4877
+ B: [
4878
+ { title: "Contributor count", sourceId: "b-contributor-count", kind: "donut", series: contributorSplit },
4879
+ { title: "Contribution distribution", sourceId: "b-contribution-distribution", kind: "gauge", gauge: (v) => pctField(v, "topCommitSharePct") }
4880
+ ],
4881
+ C: [
4882
+ { title: "Message length distribution", sourceId: "c-message-length-distribution", kind: "bars", series: subjectLengthSeries },
4883
+ { title: "Conventional Commits adherence", sourceId: "c-conventional-commits", kind: "gauge", gauge: (v) => pctField(v, "adherenceSharePct") }
4884
+ ],
4885
+ D: [
4886
+ { title: "Branch/merge topology summary", sourceId: "d-topology-summary", kind: "bars", series: (v) => pickFields(v, [["regularCommitCount", "Regular"], ["mergeCommitCount", "Merges"], ["rootCommitCount", "Root"]]) },
4887
+ { title: "Direct-to-default-branch rate", sourceId: "d-direct-to-default", kind: "gauge", gauge: (v) => pctField(v, "directToDefaultSharePct") }
4888
+ ],
4889
+ E: [
4890
+ { title: "Most-changed files / directories", sourceId: "e-most-changed", kind: "hbars", series: (v) => rowsSeries(v, "topFiles", "touchCount", 8) },
4891
+ { title: "Churn rate over time", sourceId: "e-churn-over-time", kind: "line", series: churnByMonth }
4892
+ ],
4893
+ F: [
4894
+ { title: "Hygiene strengths & weaknesses", sourceId: "f-strengths-weaknesses", kind: "radar", series: hygieneDimensions },
4895
+ { title: "Overall hygiene score", sourceId: "f-hygiene-score", kind: "gauge", gauge: (v) => pctField(v, "score") }
4896
+ ]
4897
+ };
4640
4898
  function subFigure(title2, svg, table) {
4641
4899
  return `<div class="chart-sub">
4642
4900
  <h4>${escapeHtml(title2)}</h4>
@@ -4644,29 +4902,48 @@ ${svg}
4644
4902
  ${table}
4645
4903
  </div>`;
4646
4904
  }
4647
- function renderSubChart(group, spec, metrics) {
4648
- const pool = spec.pick === "range" ? rangeMetrics(metrics) : metricsOfShape(metrics, spec.pick);
4649
- const metric = pool[spec.index];
4650
- if (metric === void 0) {
4905
+ function renderChartSpec(group, spec, byId) {
4906
+ const metric = byId.get(spec.sourceId);
4907
+ if (metric?.status !== "computed") {
4651
4908
  return void 0;
4652
4909
  }
4653
- const label = `Group ${group} \u2014 ${metric.title}`;
4910
+ const label = `Group ${group} \u2014 ${spec.title}`;
4654
4911
  if (spec.kind === "gauge") {
4655
- const range = rangeField(metric.value);
4912
+ const range = spec.gauge?.(metric.value);
4656
4913
  if (range === void 0) {
4657
4914
  return void 0;
4658
4915
  }
4659
- const table = dataTable([{ label: metric.title, value: range.value }], "Value", metric.title);
4660
- return subFigure(metric.title, svgRadialGauge(range.value, range.max, label), table);
4916
+ const table = dataTable([{ label: spec.title, value: range.value }], "Value", spec.title);
4917
+ return subFigure(spec.title, svgRadialGauge(range.value, range.max, label), table);
4918
+ }
4919
+ const series = spec.series?.(metric.value) ?? [];
4920
+ if (series.length === 0) {
4921
+ return void 0;
4922
+ }
4923
+ let svg;
4924
+ switch (spec.kind) {
4925
+ case "line":
4926
+ svg = svgLine(series, label);
4927
+ break;
4928
+ case "bars":
4929
+ svg = svgBars(series, label);
4930
+ break;
4931
+ case "hbars":
4932
+ svg = svgHBars(series, label);
4933
+ break;
4934
+ case "radar":
4935
+ svg = svgRadar(series, 100, label);
4936
+ break;
4937
+ default:
4938
+ svg = svgDonut(series, label);
4661
4939
  }
4662
- 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);
4664
- return subFigure(metric.title, svg, dataTable(series, "Value", metric.title));
4940
+ return subFigure(spec.title, svg, dataTable(series, "Value", spec.title));
4665
4941
  }
4666
4942
  function groupOverviewPanel(group, metrics) {
4667
4943
  const description = GROUP_DESCRIPTION[group];
4668
4944
  const label = `Group ${group} overview`;
4669
- const subs = GROUP_CHARTS[group].map((spec) => renderSubChart(group, spec, metrics)).filter((html) => html !== void 0);
4945
+ const byId = new Map(metrics.map((m) => [m.id, m]));
4946
+ const subs = CHART_PLAN[group].map((spec) => renderChartSpec(group, spec, byId)).filter((html) => html !== void 0);
4670
4947
  if (subs.length === 0) {
4671
4948
  return `<figure class="chart-panel" aria-label="${escapeHtml(label)}">
4672
4949
  <figcaption>${escapeHtml(description)}</figcaption>
@@ -4923,19 +5200,20 @@ function metricStat(metric) {
4923
5200
  return `${fmtStat(range.value)}${range.max === 100 ? "%" : ""}`;
4924
5201
  }
4925
5202
  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
- }
5203
+ return objectStat(value);
4936
5204
  }
4937
5205
  return "";
4938
5206
  }
5207
+ function objectStat(value) {
5208
+ const nums = Object.entries(value).filter((e) => typeof e[1] === "number" && Number.isFinite(e[1]));
5209
+ for (const key of ["total", "count", "score", "busFactor", "value"]) {
5210
+ const hit = nums.find(([k]) => k === key);
5211
+ if (hit !== void 0) {
5212
+ return fmtStat(hit[1]);
5213
+ }
5214
+ }
5215
+ return nums.length === 1 ? fmtStat(nums[0][1]) : "";
5216
+ }
4939
5217
  function fmtStat(n) {
4940
5218
  return Number.isInteger(n) ? String(n) : String(Math.round(n * 100) / 100);
4941
5219
  }
@@ -4972,7 +5250,7 @@ function formatCount2(n) {
4972
5250
  function isoDate2(iso) {
4973
5251
  return iso.slice(0, 10);
4974
5252
  }
4975
- var STYLE = `
5253
+ var STYLE = String.raw`
4976
5254
  :root {
4977
5255
  color-scheme: dark light;
4978
5256
  --bg: #0a0e14; --surface: #11161f; --surface-2: #161c28;
@@ -5031,7 +5309,7 @@ a:focus-visible, :focus-visible { outline: 2px solid var(--accent); outline-offs
5031
5309
  }
5032
5310
  .masthead h1 { margin: 0; font-size: 1.95rem; position: relative; display: flex; align-items: center; gap: 0.75rem; }
5033
5311
  .masthead h1::before {
5034
- content: "\\25D1"; display: inline-grid; place-items: center;
5312
+ content: "\25D1"; display: inline-grid; place-items: center;
5035
5313
  width: 2.7rem; height: 2.7rem; font-size: 1.45rem; color: #fff;
5036
5314
  background: linear-gradient(135deg, var(--accent), var(--accent-2));
5037
5315
  border-radius: 0.72rem; box-shadow: 0 10px 26px -8px rgba(124,92,255,0.75);
@@ -5075,7 +5353,7 @@ a:focus-visible, :focus-visible { outline: 2px solid var(--accent); outline-offs
5075
5353
  .lead, .explanation p, .coaching-intro, .coaching-closing { color: var(--fg-soft); }
5076
5354
  .key-findings { list-style: none; padding: 0; margin: 1.2rem 0 0; display: grid; gap: 0.6rem; }
5077
5355
  .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; }
5356
+ .key-findings li::before { content: "\203A"; position: absolute; left: 1rem; top: 0.7rem; color: var(--accent); font-weight: 800; font-size: 1.1rem; }
5079
5357
  .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
5358
  .chapter h3 { margin: 0 0 0.6rem; }
5081
5359
  .chapter ol { margin: 0; padding-left: 1.2rem; display: grid; gap: 0.5rem; color: var(--fg-soft); }
@@ -5246,25 +5524,6 @@ function textBars(series) {
5246
5524
  });
5247
5525
  return ["```", ...rows, "```"].join("\n");
5248
5526
  }
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
5527
  function representativeSeries(metrics) {
5269
5528
  for (const metric of metrics) {
5270
5529
  if (metric.status !== "computed") {
@@ -5276,20 +5535,17 @@ function representativeSeries(metrics) {
5276
5535
  }
5277
5536
  const series = extractSeries(metric.value);
5278
5537
  if (series.length > 0) {
5279
- return { series, shape };
5538
+ return series;
5280
5539
  }
5281
5540
  }
5282
5541
  return void 0;
5283
5542
  }
5284
- function groupOverview(group, metrics) {
5285
- const rep = representativeSeries(metrics);
5286
- if (rep === void 0) {
5543
+ function groupOverview(_group, metrics) {
5544
+ const series = representativeSeries(metrics);
5545
+ if (series === void 0) {
5287
5546
  return GROUP_OVERVIEW_NONE;
5288
5547
  }
5289
- if (rep.shape === "timeseries") {
5290
- return mermaidXychart(rep.series, `Group ${group} overview`);
5291
- }
5292
- return textBars(rep.series);
5548
+ return textBars(series);
5293
5549
  }
5294
5550
  function scalarNumber(value) {
5295
5551
  if (typeof value === "number") {
@@ -5517,14 +5773,48 @@ function metricBullets(metric, explanations) {
5517
5773
  if (explanation === void 0) {
5518
5774
  return value;
5519
5775
  }
5520
- return [value, ...facetBullets2(explanation)].join("\n");
5776
+ const separator = value.includes("\n") ? "\n\n" : "\n";
5777
+ return [value, facetBullets2(explanation).join("\n")].join(separator);
5521
5778
  }
5522
5779
  function valueBullet2(metric) {
5523
5780
  if (metric.status !== "computed") {
5524
5781
  const reason = metric.reason === void 0 ? "" : ` \u2014 ${escapeCell(metric.reason)}`;
5525
5782
  return `- **Value** \u2014 _not available${reason}_`;
5526
5783
  }
5527
- return `- **Value** \u2014 ${escapeCell(formatValue3(metric.value))}`;
5784
+ const value = metric.value;
5785
+ if (value === null || typeof value !== "object") {
5786
+ return `- **Value** \u2014 ${escapeCell(formatValue3(value))}`;
5787
+ }
5788
+ const series = extractSeries(value);
5789
+ if (series.length === 0) {
5790
+ const tree = buildValueTree(value);
5791
+ if (tree.kind === "scalar") {
5792
+ return `- **Value** \u2014 ${escapeCell(tree.text)}`;
5793
+ }
5794
+ return [`- **Value**`, ...treeBullets(tree.entries, " ")].join("\n");
5795
+ }
5796
+ if (series.length === 1) {
5797
+ return `- **Value** \u2014 ${escapeCell(formatNumber3(series[0].value))}`;
5798
+ }
5799
+ return valueTable(value);
5800
+ }
5801
+ function treeBullets(entries, indent) {
5802
+ return entries.flatMap((entry) => {
5803
+ if (entry.child.kind === "scalar") {
5804
+ const text = entry.child.text === "" ? escapeCell(entry.label) : `${escapeCell(entry.label)}: ${escapeCell(entry.child.text)}`;
5805
+ return [`${indent}- ${text}`];
5806
+ }
5807
+ return [`${indent}- ${escapeCell(entry.label)}`, ...treeBullets(entry.child.entries, `${indent} `)];
5808
+ });
5809
+ }
5810
+ function formatNumber3(n) {
5811
+ return Number.isFinite(n) ? String(Math.round(n * 100) / 100) : "0";
5812
+ }
5813
+ function valueTable(value) {
5814
+ const shape = detectShape(value);
5815
+ const labelHeader = shape === "timeseries" ? "Period" : shape === "distribution" ? "Item" : "Field";
5816
+ const rows = extractSeries(value).map((point) => `| ${escapeCell(point.label)} | ${escapeCell(formatNumber3(point.value))} |`);
5817
+ return [`- **Value**`, "", `| ${labelHeader} | Value |`, "| --- | --- |", ...rows].join("\n");
5528
5818
  }
5529
5819
  function facetBullets2(explanation) {
5530
5820
  return [
@@ -5931,7 +6221,7 @@ function createRetrieve(deps = {}) {
5931
6221
  }
5932
6222
 
5933
6223
  // src/cli/provenance.ts
5934
- import { basename } from "path";
6224
+ import { basename, resolve } from "path";
5935
6225
  function buildProvenance(input) {
5936
6226
  const source = isRemoteTarget(input.target) ? "remote" : "local";
5937
6227
  const repo = {
@@ -6004,13 +6294,17 @@ function localName(target) {
6004
6294
  const trimmed = stripTrailingSlashes(target.trim());
6005
6295
  const base = basename(trimmed);
6006
6296
  if (base === "" || base === "." || base === "..") {
6297
+ const resolved = basename(resolve(trimmed === "" ? "." : trimmed));
6298
+ if (resolved !== "" && resolved !== "." && resolved !== "..") {
6299
+ return resolved;
6300
+ }
6007
6301
  return trimmed === "" ? target.trim() : trimmed;
6008
6302
  }
6009
6303
  return base;
6010
6304
  }
6011
6305
 
6012
6306
  // src/cli/version.ts
6013
- var VERSION = "1.1.0";
6307
+ var VERSION = "1.1.2";
6014
6308
 
6015
6309
  // src/cli/write-file.ts
6016
6310
  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.2",
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",