commit-whisper 1.1.1 → 1.1.3

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 +386 -53
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -942,11 +942,14 @@ function randomSuffix2() {
942
942
  }
943
943
 
944
944
  // src/retrieve/git.ts
945
- import { execFile } from "child_process";
945
+ import { execFile, spawn } from "child_process";
946
946
  import { promisify } from "util";
947
947
  var execFileAsync = promisify(execFile);
948
948
  var MAX_BUFFER = 256 * 1024 * 1024;
949
949
  var execFileGitRunner = async (args, options) => {
950
+ if (options.onChunk !== void 0) {
951
+ return spawnGitRunner(args, options.cwd, options.extraEnv, options.onChunk);
952
+ }
950
953
  const { stdout } = await execFileAsync("git", [...args], {
951
954
  cwd: options.cwd,
952
955
  // execFile already inherits `process.env` by default; we make that explicit
@@ -961,6 +964,38 @@ var execFileGitRunner = async (args, options) => {
961
964
  });
962
965
  return stdout;
963
966
  };
967
+ function spawnGitRunner(args, cwd, extraEnv, onChunk) {
968
+ return new Promise((resolve2, reject) => {
969
+ const child = spawn("git", [...args], {
970
+ cwd,
971
+ // eslint-disable-next-line no-restricted-properties -- propagate OS env to the git child + add auth vars (not config reading)
972
+ env: extraEnv === void 0 ? void 0 : { ...process.env, ...extraEnv },
973
+ windowsHide: true
974
+ });
975
+ let stdout = "";
976
+ let stderr = "";
977
+ child.stdout.setEncoding("utf8");
978
+ child.stderr.setEncoding("utf8");
979
+ child.stdout.on("data", (chunk) => {
980
+ stdout += chunk;
981
+ onChunk(chunk);
982
+ });
983
+ child.stderr.on("data", (chunk) => {
984
+ stderr += chunk;
985
+ });
986
+ child.on("error", reject);
987
+ child.on("close", (code) => {
988
+ if (code === 0) {
989
+ resolve2(stdout);
990
+ return;
991
+ }
992
+ const err = new Error(`git exited with code ${code ?? "unknown"}`);
993
+ err.code = code;
994
+ err.stderr = stderr;
995
+ reject(err);
996
+ });
997
+ });
998
+ }
964
999
 
965
1000
  // src/cli/open-browser.ts
966
1001
  import { execFile as execFile2 } from "child_process";
@@ -1046,6 +1081,14 @@ var ui = createUi();
1046
1081
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1047
1082
  var SPINNER_INTERVAL_MS = 80;
1048
1083
  var CLEAR_LINE = "\r\x1B[2K";
1084
+ function progressBar(completed, total, width = 12) {
1085
+ if (total <= 0 || width <= 0) {
1086
+ return "";
1087
+ }
1088
+ const ratio2 = Math.min(1, Math.max(0, completed / total));
1089
+ const filled = Math.min(width, Math.round(ratio2 * width));
1090
+ return "\u25B0".repeat(filled) + "\u25B1".repeat(width - filled);
1091
+ }
1049
1092
  var noopProgress = {
1050
1093
  start() {
1051
1094
  },
@@ -1171,7 +1214,7 @@ function messageForError(err) {
1171
1214
  import pc2 from "picocolors";
1172
1215
  import { isCancel, multiselect as clackMultiselect, select as clackSelect, text as clackText } from "@clack/prompts";
1173
1216
  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:";
1217
+ var QUIT_MESSAGE = "Case closed. Until your next commit \u2014 here's the cheatsheet for the road \u{1F6E3}\uFE0F";
1175
1218
  var FLAGS_CHEATSHEET = [
1176
1219
  "Common commands:",
1177
1220
  " commit-whisper . analyze the current repository",
@@ -1472,12 +1515,12 @@ async function waitForKey(output) {
1472
1515
  const wasRaw = stdin.isRaw === true;
1473
1516
  stdin.setRawMode?.(true);
1474
1517
  stdin.resume();
1475
- return await new Promise((resolve) => {
1518
+ return await new Promise((resolve2) => {
1476
1519
  stdin.once("data", (data) => {
1477
1520
  stdin.setRawMode?.(wasRaw);
1478
1521
  stdin.pause();
1479
1522
  const key = data.toString("utf8");
1480
- resolve(key === "" || key === "\x1B" ? "quit" : "continue");
1523
+ resolve2(key === "" || key === "\x1B" ? "quit" : "continue");
1481
1524
  });
1482
1525
  });
1483
1526
  }
@@ -3689,7 +3732,9 @@ async function generateExplanations(model, analysis, deps = {}) {
3689
3732
  metrics: analysis.metrics.filter((metric) => metric.group === group)
3690
3733
  })).filter((batch) => batch.metrics.length > 0);
3691
3734
  const settled = await Promise.allSettled(
3692
- batches.map((batch) => generateGroupExplanations(model, { metrics: batch.metrics }, deps))
3735
+ batches.map(
3736
+ (batch) => generateGroupExplanations(model, { metrics: batch.metrics }, deps).finally(() => deps.onGroup?.(batch.group))
3737
+ )
3693
3738
  );
3694
3739
  const merged = {};
3695
3740
  for (const result of settled) {
@@ -3927,18 +3972,32 @@ function assertNeverProvider(provider) {
3927
3972
 
3928
3973
  // src/narrate/narrate.ts
3929
3974
  function createNarrate(deps = {}) {
3930
- const resolve = deps.resolveModel ?? resolveModel;
3975
+ const resolve2 = deps.resolveModel ?? resolveModel;
3931
3976
  const generate = deps.generate ?? generateNarrative;
3932
3977
  const generateExpl = deps.generateExplanations ?? generateExplanations;
3933
- return async (analysis, config) => {
3978
+ return async (analysis, config, onProgress) => {
3934
3979
  if (config.aiMode === "off") {
3935
3980
  return { kind: "skipped" };
3936
3981
  }
3937
3982
  try {
3938
- const model = resolve(config);
3983
+ const presentGroups2 = METRIC_GROUPS.filter((group) => analysis.metrics.some((metric) => metric.group === group));
3984
+ const total = 1 + presentGroups2.length + 1;
3985
+ let completed = 0;
3986
+ const report = (label) => onProgress?.({ completed, total, label });
3987
+ report(`Connecting to ${config.provider ?? "the model"}\u2026`);
3988
+ const model = resolve2(config);
3939
3989
  const [parts, explanations] = await Promise.all([
3940
- generate(model, analysis),
3941
- generateExpl(model, analysis)
3990
+ generate(model, analysis).then((value) => {
3991
+ completed += 1;
3992
+ report("Wrote the summary, explanation & coaching");
3993
+ return value;
3994
+ }),
3995
+ generateExpl(model, analysis, {
3996
+ onGroup: (group) => {
3997
+ completed += 1;
3998
+ report(`Explained Group ${group} metrics`);
3999
+ }
4000
+ })
3942
4001
  ]);
3943
4002
  const grounded = groundNarrative({ ...parts, explanations }, analysis);
3944
4003
  const confidence = assessConfidence({
@@ -3948,6 +4007,8 @@ function createNarrate(deps = {}) {
3948
4007
  provider: config.provider,
3949
4008
  llmModel: config.llmModel
3950
4009
  });
4010
+ completed = total;
4011
+ report("Grounded the numbers & assessed confidence");
3951
4012
  return { kind: "narrated", narrative: { ...grounded.narrative, confidence } };
3952
4013
  } catch (err) {
3953
4014
  const reason = narrationReason(err, config.aiKey?.reveal());
@@ -4067,6 +4128,61 @@ function isDateKeyedNumbers(value) {
4067
4128
  function numericEntries(value) {
4068
4129
  return Object.entries(value).filter(([, v]) => typeof v === "number" && Number.isFinite(v)).map(([label, v]) => ({ label, value: v }));
4069
4130
  }
4131
+ var BUCKET_FIELDS = ["churn", "value", "count", "total", "score"];
4132
+ function bucketNumber(value) {
4133
+ if (typeof value === "number") {
4134
+ return Number.isFinite(value) ? value : void 0;
4135
+ }
4136
+ if (!isObject(value)) {
4137
+ return void 0;
4138
+ }
4139
+ const nums = numericEntries(value);
4140
+ for (const field2 of BUCKET_FIELDS) {
4141
+ const hit = nums.find((p) => p.label === field2);
4142
+ if (hit !== void 0) {
4143
+ return hit.value;
4144
+ }
4145
+ }
4146
+ return nums[0]?.value;
4147
+ }
4148
+ function bucketEntries(value) {
4149
+ const out = [];
4150
+ for (const [label, v] of Object.entries(value)) {
4151
+ const num = bucketNumber(v);
4152
+ if (num !== void 0) {
4153
+ out.push({ label, value: num });
4154
+ }
4155
+ }
4156
+ return out;
4157
+ }
4158
+ function nestedChartable(value) {
4159
+ for (const sub of Object.values(value)) {
4160
+ if (isObject(sub)) {
4161
+ if (isDateKeyedNumbers(sub)) {
4162
+ return { series: numericEntries(sub), timeseries: true };
4163
+ }
4164
+ const nums = numericEntries(sub);
4165
+ if (nums.length >= 2) {
4166
+ return { series: nums, timeseries: false };
4167
+ }
4168
+ } else if (Array.isArray(sub)) {
4169
+ const series = extractSeries(sub);
4170
+ if (series.length >= 2) {
4171
+ return { series, timeseries: false };
4172
+ }
4173
+ }
4174
+ }
4175
+ return void 0;
4176
+ }
4177
+ function collectionCounts(value) {
4178
+ const out = [];
4179
+ for (const [label, v] of Object.entries(value)) {
4180
+ if (Array.isArray(v)) {
4181
+ out.push({ label, value: v.length });
4182
+ }
4183
+ }
4184
+ return out;
4185
+ }
4070
4186
  function rangeField(value) {
4071
4187
  if (!isObject(value)) {
4072
4188
  return void 0;
@@ -4098,6 +4214,13 @@ function detectShape(value) {
4098
4214
  if (nums.length >= 2) {
4099
4215
  return "distribution";
4100
4216
  }
4217
+ const nested = nestedChartable(value);
4218
+ if (nested !== void 0) {
4219
+ return nested.timeseries ? "timeseries" : "distribution";
4220
+ }
4221
+ if (collectionCounts(value).length >= 2) {
4222
+ return "distribution";
4223
+ }
4101
4224
  if (nums.length === 1) {
4102
4225
  return "scalar";
4103
4226
  }
@@ -4141,6 +4264,34 @@ function extractSeries(value) {
4141
4264
  }
4142
4265
  return numericEntries(value);
4143
4266
  }
4267
+ function chartSeries(value) {
4268
+ if (Array.isArray(value)) {
4269
+ return extractSeries(value);
4270
+ }
4271
+ if (!isObject(value)) {
4272
+ return [];
4273
+ }
4274
+ const bucket = timeBucket(value);
4275
+ if (bucket !== void 0) {
4276
+ return bucketEntries(bucket);
4277
+ }
4278
+ if (isDateKeyedNumbers(value)) {
4279
+ return numericEntries(value);
4280
+ }
4281
+ const direct = numericEntries(value);
4282
+ if (direct.length >= 2) {
4283
+ return direct;
4284
+ }
4285
+ const nested = nestedChartable(value);
4286
+ if (nested !== void 0) {
4287
+ return nested.series;
4288
+ }
4289
+ const collections = collectionCounts(value);
4290
+ if (collections.length >= 2) {
4291
+ return collections;
4292
+ }
4293
+ return direct;
4294
+ }
4144
4295
 
4145
4296
  // src/render/value-tree.ts
4146
4297
  var LABEL_FIELDS2 = ["path", "file", "directory", "area", "name", "id", "label", "key"];
@@ -4463,6 +4614,8 @@ function escapeHtml(text) {
4463
4614
  }
4464
4615
 
4465
4616
  // src/render/html/svg.ts
4617
+ var GAUGE_W = 100;
4618
+ var GAUGE_H = 40;
4466
4619
  function safe(n) {
4467
4620
  return Number.isFinite(n) ? n : 0;
4468
4621
  }
@@ -4661,6 +4814,14 @@ function svgHBars(series, label) {
4661
4814
  const viewBox = `0 0 ${W} ${H}`;
4662
4815
  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>`;
4663
4816
  }
4817
+ function svgGauge(value, max, label) {
4818
+ const id = `cw-gauge-${hashId(label)}`;
4819
+ const denom = max <= 0 ? 1 : max;
4820
+ const t = Math.min(1, Math.max(0, safe(value) / denom));
4821
+ const y = r(GAUGE_H / 2 - 4);
4822
+ const viewBox = `0 0 ${GAUGE_W} ${GAUGE_H}`;
4823
+ return `${open(label, "chart-gauge", viewBox, "none")}${fillGradient(id, false)}<rect class="gauge-track" x="0" y="${y}" width="${GAUGE_W}" height="8" rx="4"/><rect class="gauge-fill" x="0" y="${y}" width="${r(t * GAUGE_W)}" height="8" rx="4" fill="url(#${id})"/></svg>`;
4824
+ }
4664
4825
  function svgRadar(points, max, label) {
4665
4826
  if (points.length < 3) {
4666
4827
  return svgBars(points, label).replace("chart-bars", "chart-radar");
@@ -4725,6 +4886,13 @@ function svgDonut(series, label) {
4725
4886
  const a0 = (-90 + cum * 360) * Math.PI / 180;
4726
4887
  const a1 = (-90 + (cum + frac) * 360) * Math.PI / 180;
4727
4888
  cum += frac;
4889
+ if (frac <= 0) {
4890
+ return "";
4891
+ }
4892
+ if (frac >= 0.999) {
4893
+ 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`;
4894
+ return `<path class="donut-seg slice-${i % 6}" d="${ring}"/>`;
4895
+ }
4728
4896
  const large = frac > 0.5 ? 1 : 0;
4729
4897
  const x0o = r(cx + rOuter * Math.cos(a0));
4730
4898
  const y0o = r(cy + rOuter * Math.sin(a0));
@@ -4773,20 +4941,121 @@ function formatNumber2(value) {
4773
4941
  }
4774
4942
  return Number.isInteger(value) ? String(value) : String(Math.round(value * 100) / 100);
4775
4943
  }
4776
- var GROUP_CHARTS = {
4777
- A: [{ kind: "line", pick: "timeseries", index: 0 }, { kind: "bars", pick: "timeseries", index: 1 }],
4778
- B: [{ kind: "donut", pick: "distribution", index: 0 }, { kind: "gauge", pick: "range", index: 0 }],
4779
- C: [{ kind: "bars", pick: "distribution", index: 0 }, { kind: "gauge", pick: "range", index: 0 }],
4780
- D: [{ kind: "line", pick: "timeseries", index: 0 }, { kind: "gauge", pick: "range", index: 0 }],
4781
- E: [{ kind: "hbars", pick: "distribution", index: 0 }, { kind: "line", pick: "timeseries", index: 0 }],
4782
- F: [{ kind: "radar", pick: "distribution", index: 0 }, { kind: "gauge", pick: "range", index: 0 }]
4783
- };
4784
- function metricsOfShape(metrics, shape) {
4785
- return metrics.filter((m) => m.status === "computed" && detectShape(m.value) === shape && extractSeries(m.value).length > 0);
4944
+ function asRecord(value) {
4945
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
4946
+ }
4947
+ function objectSeries(value) {
4948
+ return Object.entries(asRecord(value)).filter(([, v]) => typeof v === "number" && Number.isFinite(v)).map(([label, v]) => ({ label, value: v }));
4949
+ }
4950
+ function pickFields(value, fields) {
4951
+ const obj = asRecord(value);
4952
+ const out = [];
4953
+ for (const pair of fields) {
4954
+ const key = pair[0];
4955
+ if (typeof key !== "string") {
4956
+ continue;
4957
+ }
4958
+ const v = obj[key];
4959
+ if (typeof v === "number" && Number.isFinite(v)) {
4960
+ out.push({ label: pair[1] ?? key, value: v });
4961
+ }
4962
+ }
4963
+ return out;
4964
+ }
4965
+ function pctField(value, key) {
4966
+ const v = asRecord(value)[key];
4967
+ return typeof v === "number" && Number.isFinite(v) ? { value: v, max: 100 } : void 0;
4968
+ }
4969
+ function baseName(path) {
4970
+ const parts = path.split("/").filter((p) => p !== "");
4971
+ return parts.at(-1) ?? path;
4972
+ }
4973
+ function rowsSeries(value, key, field2, limit) {
4974
+ const rows = asRecord(value)[key];
4975
+ if (!Array.isArray(rows)) {
4976
+ return [];
4977
+ }
4978
+ const out = [];
4979
+ for (const row of rows.slice(0, limit)) {
4980
+ const obj = asRecord(row);
4981
+ const v = obj[field2];
4982
+ if (typeof v === "number" && Number.isFinite(v) && typeof obj.path === "string") {
4983
+ out.push({ label: baseName(obj.path), value: v });
4984
+ }
4985
+ }
4986
+ return out;
4987
+ }
4988
+ function contributorSplit(value) {
4989
+ const obj = asRecord(value);
4990
+ const active = typeof obj.active === "number" ? obj.active : 0;
4991
+ const total = typeof obj.total === "number" ? obj.total : active;
4992
+ return [
4993
+ { label: "active", value: active },
4994
+ { label: "inactive", value: Math.max(0, total - active) }
4995
+ ];
4786
4996
  }
4787
- function rangeMetrics(metrics) {
4788
- return metrics.filter((m) => m.status === "computed" && rangeField(m.value) !== void 0);
4997
+ function hygieneDimensions(value) {
4998
+ const obj = asRecord(value);
4999
+ const out = [];
5000
+ for (const key of ["strengths", "weaknesses"]) {
5001
+ const arr = obj[key];
5002
+ if (!Array.isArray(arr)) {
5003
+ continue;
5004
+ }
5005
+ for (const entry of arr) {
5006
+ const dim = asRecord(entry);
5007
+ if (typeof dim.name === "string" && typeof dim.subScore === "number" && Number.isFinite(dim.subScore)) {
5008
+ out.push({ label: dim.name, value: dim.subScore });
5009
+ }
5010
+ }
5011
+ }
5012
+ return out;
4789
5013
  }
5014
+ function churnByMonth(value) {
5015
+ const out = [];
5016
+ for (const [label, bucket] of Object.entries(asRecord(asRecord(value).perMonth))) {
5017
+ const churn = asRecord(bucket).churn;
5018
+ if (typeof churn === "number" && Number.isFinite(churn)) {
5019
+ out.push({ label, value: churn });
5020
+ }
5021
+ }
5022
+ return out;
5023
+ }
5024
+ function subjectLengthSeries(value) {
5025
+ return pickFields(asRecord(value).subjectLength, [
5026
+ ["min", "Min"],
5027
+ ["median", "Median"],
5028
+ ["mean", "Mean"],
5029
+ ["p90", "p90"],
5030
+ ["max", "Max"]
5031
+ ]);
5032
+ }
5033
+ var CHART_PLAN = {
5034
+ A: [
5035
+ { title: "Commit volume over time", sourceId: "a-commit-volume", kind: "line", series: (v) => objectSeries(asRecord(v).perMonth) },
5036
+ { title: "Commit frequency / cadence", sourceId: "a-commit-volume", kind: "bars", series: (v) => objectSeries(asRecord(v).perWeek) }
5037
+ ],
5038
+ B: [
5039
+ { title: "Contributor count", sourceId: "b-contributor-count", kind: "donut", series: contributorSplit },
5040
+ { title: "Contribution distribution", sourceId: "b-contribution-distribution", kind: "gauge", gauge: (v) => pctField(v, "topCommitSharePct") }
5041
+ ],
5042
+ C: [
5043
+ { title: "Message length distribution", sourceId: "c-message-length-distribution", kind: "bars", series: subjectLengthSeries },
5044
+ { title: "Conventional Commits adherence", sourceId: "c-conventional-commits", kind: "gauge", gauge: (v) => pctField(v, "adherenceSharePct") }
5045
+ ],
5046
+ D: [
5047
+ { title: "Branch/merge topology summary", sourceId: "d-topology-summary", kind: "bars", series: (v) => pickFields(v, [["regularCommitCount", "Regular"], ["mergeCommitCount", "Merges"], ["rootCommitCount", "Root"]]) },
5048
+ { title: "Direct-to-default-branch rate", sourceId: "d-direct-to-default", kind: "gauge", gauge: (v) => pctField(v, "directToDefaultSharePct") }
5049
+ ],
5050
+ E: [
5051
+ { title: "Most-changed files / directories", sourceId: "e-most-changed", kind: "hbars", series: (v) => rowsSeries(v, "topFiles", "touchCount", 8) },
5052
+ { title: "Churn rate over time", sourceId: "e-churn-over-time", kind: "line", series: churnByMonth }
5053
+ ],
5054
+ F: [
5055
+ { title: "Hygiene strengths & weaknesses", sourceId: "f-strengths-weaknesses", kind: "radar", series: hygieneDimensions },
5056
+ { title: "Overall hygiene score", sourceId: "f-hygiene-score", kind: "gauge", gauge: (v) => pctField(v, "score") }
5057
+ ]
5058
+ };
4790
5059
  function subFigure(title2, svg, table) {
4791
5060
  return `<div class="chart-sub">
4792
5061
  <h4>${escapeHtml(title2)}</h4>
@@ -4794,22 +5063,24 @@ ${svg}
4794
5063
  ${table}
4795
5064
  </div>`;
4796
5065
  }
4797
- function renderSubChart(group, spec, metrics) {
4798
- const pool = spec.pick === "range" ? rangeMetrics(metrics) : metricsOfShape(metrics, spec.pick);
4799
- const metric = pool[spec.index];
4800
- if (metric === void 0) {
5066
+ function renderChartSpec(group, spec, byId) {
5067
+ const metric = byId.get(spec.sourceId);
5068
+ if (metric?.status !== "computed") {
4801
5069
  return void 0;
4802
5070
  }
4803
- const label = `Group ${group} \u2014 ${metric.title}`;
5071
+ const label = `Group ${group} \u2014 ${spec.title}`;
4804
5072
  if (spec.kind === "gauge") {
4805
- const range = rangeField(metric.value);
5073
+ const range = spec.gauge?.(metric.value);
4806
5074
  if (range === void 0) {
4807
5075
  return void 0;
4808
5076
  }
4809
- const table = dataTable([{ label: metric.title, value: range.value }], "Value", metric.title);
4810
- return subFigure(metric.title, svgRadialGauge(range.value, range.max, label), table);
5077
+ const table = dataTable([{ label: spec.title, value: range.value }], "Value", spec.title);
5078
+ return subFigure(spec.title, svgRadialGauge(range.value, range.max, label), table);
5079
+ }
5080
+ const series = spec.series?.(metric.value) ?? [];
5081
+ if (series.length === 0) {
5082
+ return void 0;
4811
5083
  }
4812
- const series = extractSeries(metric.value);
4813
5084
  let svg;
4814
5085
  switch (spec.kind) {
4815
5086
  case "line":
@@ -4827,12 +5098,13 @@ function renderSubChart(group, spec, metrics) {
4827
5098
  default:
4828
5099
  svg = svgDonut(series, label);
4829
5100
  }
4830
- return subFigure(metric.title, svg, dataTable(series, "Value", metric.title));
5101
+ return subFigure(spec.title, svg, dataTable(series, "Value", spec.title));
4831
5102
  }
4832
5103
  function groupOverviewPanel(group, metrics) {
4833
5104
  const description = GROUP_DESCRIPTION[group];
4834
5105
  const label = `Group ${group} overview`;
4835
- const subs = GROUP_CHARTS[group].map((spec) => renderSubChart(group, spec, metrics)).filter((html) => html !== void 0);
5106
+ const byId = new Map(metrics.map((m) => [m.id, m]));
5107
+ const subs = CHART_PLAN[group].map((spec) => renderChartSpec(group, spec, byId)).filter((html) => html !== void 0);
4836
5108
  if (subs.length === 0) {
4837
5109
  return `<figure class="chart-panel" aria-label="${escapeHtml(label)}">
4838
5110
  <figcaption>${escapeHtml(description)}</figcaption>
@@ -4847,6 +5119,38 @@ ${subs.join("\n")}
4847
5119
  </div>
4848
5120
  </figure>`;
4849
5121
  }
5122
+ function metricVisual(metric) {
5123
+ if (metric.status === "not_available") {
5124
+ return "";
5125
+ }
5126
+ const label = `${metric.title} visual`;
5127
+ const shape = detectShape(metric.value);
5128
+ switch (shape) {
5129
+ case "timeseries": {
5130
+ const series = chartSeries(metric.value);
5131
+ return `<div class="metric-visual">${svgLine(series, label)}
5132
+ ${dataTable(series, "Value", metric.title)}</div>`;
5133
+ }
5134
+ case "distribution": {
5135
+ const series = chartSeries(metric.value);
5136
+ return `<div class="metric-visual">${svgBars(series, label)}
5137
+ ${dataTable(series, "Value", metric.title)}</div>`;
5138
+ }
5139
+ case "scalar-range": {
5140
+ const range = rangeField(metric.value);
5141
+ const series = chartSeries(metric.value);
5142
+ const bars = series.length > 1 ? svgBars(series, label) : "";
5143
+ const gauge = bars === "" && range !== void 0 ? svgGauge(range.value, range.max, label) : "";
5144
+ const number = bars === "" && range !== void 0 ? `<span class="metric-number">${escapeHtml(formatNumber2(range.value))}</span>` : "";
5145
+ return `<div class="metric-visual metric-visual-range">${gauge}${number}${bars}
5146
+ ${dataTable(series, "Value", metric.title)}</div>`;
5147
+ }
5148
+ case "scalar":
5149
+ case "none":
5150
+ default:
5151
+ return "";
5152
+ }
5153
+ }
4850
5154
 
4851
5155
  // src/render/html/inter-font.ts
4852
5156
  var INTER_FONT_CSS = `@font-face{font-family:'Inter';font-style:normal;font-weight:400;font-display:swap;src:url(data:font/woff2;base64,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) format('woff2');}
@@ -4864,7 +5168,7 @@ var GROUPS2 = [
4864
5168
  { id: "E", title: "Churn & Hotspots" },
4865
5169
  { id: "F", title: "Repository Health Signals" }
4866
5170
  ];
4867
- var HTML_DEGRADED_BANNER = "\u26A0 Narrative unavailable \u2014 showing raw analysis";
5171
+ var HTML_DEGRADED_BANNER = "\u26A0 AI Narrative unavailable \u2014 showing raw analysis";
4868
5172
  var HTML_METRICS_ONLY_NOTE = "Metrics-only run \u2014 no AI narrative requested";
4869
5173
  function renderHtml(report) {
4870
5174
  const route = classifyReport(report);
@@ -5045,10 +5349,12 @@ function metricCard2(metric, explanations) {
5045
5349
  const explanation = explanations?.[metric.id];
5046
5350
  const facets = explanation === void 0 ? "" : fourFacets(explanation);
5047
5351
  const reason = metric.status === "computed" ? "" : `<p class="why">${escapeHtml(metric.reason ?? "Not available.")}</p>`;
5352
+ const visual = metricVisual(metric);
5048
5353
  return `<details class="metric-card" data-status="${escapeHtml(metric.status)}" data-health="${band}" open>
5049
5354
  <summary><h3 class="metric-title">${escapeHtml(metric.title)}</h3> ${bandHtml}${statHtml}</summary>
5050
5355
  <div class="metric-body">
5051
5356
  ${reason}
5357
+ ${visual}
5052
5358
  ${facets}
5053
5359
  </div>
5054
5360
  </details>`;
@@ -5222,7 +5528,7 @@ a:focus-visible, :focus-visible { outline: 2px solid var(--accent); outline-offs
5222
5528
  .confidence-medium .confidence-label, .confidence-medium strong { color: var(--watch); }
5223
5529
  .confidence-low .confidence-label, .confidence-low strong { color: var(--risk); }
5224
5530
  .confidence-escalation { color: var(--risk); font-weight: 600; }
5225
- .banner { border-radius: 12px; padding: 0.85rem 1.1rem; margin: 1.25rem 0; }
5531
+ .banner { max-width: 66rem; margin: 1.25rem auto; border-radius: 12px; padding: 0.85rem 1.1rem; }
5226
5532
  .banner-degraded { border: 1px solid rgba(255,107,107,0.4); color: var(--risk); font-weight: 600; background: linear-gradient(90deg, rgba(255,107,107,0.12), transparent); }
5227
5533
  .banner-metrics-only { border: 1px solid var(--border); color: var(--muted); background: var(--surface); }
5228
5534
  .toc {
@@ -5248,7 +5554,8 @@ a:focus-visible, :focus-visible { outline: 2px solid var(--accent); outline-offs
5248
5554
  .chapter ol { margin: 0; padding-left: 1.2rem; display: grid; gap: 0.5rem; color: var(--fg-soft); }
5249
5555
  .coaching-closing { margin-top: 1rem; padding: 1rem 1.2rem; border: 1px solid rgba(124,92,255,0.35); border-radius: 12px; background: linear-gradient(90deg, rgba(124,92,255,0.10), transparent); }
5250
5556
  .metric-group .chart-panel + .metric-card { margin-top: 1.2rem; }
5251
- .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; margin-top: 1.2rem; align-items: stretch; }
5557
+ .cards { display: grid; grid-template-columns: 1fr; gap: 1rem; margin-top: 1.2rem; align-items: stretch; }
5558
+ @media (min-width: 720px) { .cards { grid-template-columns: repeat(2, 1fr); } }
5252
5559
  .cards > .metric-card { margin: 0; }
5253
5560
  .metric-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; padding: 0.7rem 1.1rem; margin: 0.85rem 0; transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; }
5254
5561
  .metric-card:hover { transform: translateY(-2px); border-color: #2f3a4d; box-shadow: var(--shadow); }
@@ -5422,7 +5729,7 @@ function representativeSeries(metrics) {
5422
5729
  if (shape !== "timeseries" && shape !== "distribution") {
5423
5730
  continue;
5424
5731
  }
5425
- const series = extractSeries(metric.value);
5732
+ const series = chartSeries(metric.value);
5426
5733
  if (series.length > 0) {
5427
5734
  return series;
5428
5735
  }
@@ -5451,11 +5758,11 @@ function metricVisualMarkdown(metric) {
5451
5758
  const shape = detectShape(metric.value);
5452
5759
  switch (shape) {
5453
5760
  case "timeseries": {
5454
- const spark = sparkline(extractSeries(metric.value));
5761
+ const spark = sparkline(chartSeries(metric.value));
5455
5762
  return spark === "" ? none : { headingSuffix: `\`${spark}\``, body: "" };
5456
5763
  }
5457
5764
  case "distribution":
5458
- return { headingSuffix: "", body: textBars(extractSeries(metric.value)) };
5765
+ return { headingSuffix: "", body: textBars(chartSeries(metric.value)) };
5459
5766
  case "scalar-range": {
5460
5767
  const range = rangeField(metric.value);
5461
5768
  return range === void 0 ? none : { headingSuffix: `**${round2(range.value)}/${round2(range.max)}**`, body: "" };
@@ -5830,6 +6137,7 @@ function isRemoteTarget(target) {
5830
6137
  // src/retrieve/git-log.ts
5831
6138
  var RS = "";
5832
6139
  var US = "";
6140
+ var RECORD_SEPARATOR = RS;
5833
6141
  var GIT_LOG_FORMAT = `${RS}%H${US}%an${US}%ae${US}%aI${US}%cn${US}%ce${US}%cI${US}%P${US}%B${US}`;
5834
6142
  function gitLogArgs() {
5835
6143
  return [
@@ -5903,19 +6211,33 @@ function parseGitLog(stdout) {
5903
6211
  }
5904
6212
 
5905
6213
  // src/retrieve/read-history.ts
5906
- async function readGitHistory(runner, workdir, repoTargetLabel) {
6214
+ async function readGitHistory(runner, workdir, repoTargetLabel, onProgress) {
5907
6215
  await assertGitRepo(runner, workdir, repoTargetLabel);
5908
6216
  if (!await hasCommits(runner, workdir, repoTargetLabel)) {
5909
6217
  return { repoTarget: repoTargetLabel, commits: [] };
5910
6218
  }
5911
6219
  let stdout;
5912
6220
  try {
5913
- stdout = await runner(gitLogArgs(), { cwd: workdir });
6221
+ stdout = await runner(gitLogArgs(), { cwd: workdir, onChunk: countingChunkHandler(onProgress) });
5914
6222
  } catch (cause) {
5915
6223
  throw new RetrieveError(`Failed to read git history from "${repoTargetLabel}".`, { cause });
5916
6224
  }
5917
6225
  return { repoTarget: repoTargetLabel, commits: parseGitLog(stdout) };
5918
6226
  }
6227
+ function countingChunkHandler(onProgress) {
6228
+ if (onProgress === void 0) {
6229
+ return void 0;
6230
+ }
6231
+ let count = 0;
6232
+ return (chunk) => {
6233
+ let index = chunk.indexOf(RECORD_SEPARATOR);
6234
+ while (index !== -1) {
6235
+ count += 1;
6236
+ index = chunk.indexOf(RECORD_SEPARATOR, index + 1);
6237
+ }
6238
+ onProgress(count);
6239
+ };
6240
+ }
5919
6241
  async function assertGitRepo(runner, workdir, label) {
5920
6242
  let out;
5921
6243
  try {
@@ -5952,7 +6274,7 @@ function isUnbornHead(cause) {
5952
6274
 
5953
6275
  // src/retrieve/local.ts
5954
6276
  function createLocalRetrieve(runner = execFileGitRunner) {
5955
- return async (config) => readGitHistory(runner, config.repoTarget, config.repoTarget);
6277
+ return async (config, onProgress) => readGitHistory(runner, config.repoTarget, config.repoTarget, onProgress);
5956
6278
  }
5957
6279
 
5958
6280
  // src/retrieve/remote.ts
@@ -6087,7 +6409,7 @@ function cloneEnv(gitToken) {
6087
6409
  return env;
6088
6410
  }
6089
6411
  function createRemoteRetrieve(runner = execFileGitRunner, workspaceDeps = {}, gitToken) {
6090
- return async (config) => {
6412
+ return async (config, onProgress) => {
6091
6413
  const url = config.repoTarget;
6092
6414
  return withTempWorkspace(async (dir) => {
6093
6415
  const dest = join4(dir, "repo");
@@ -6096,7 +6418,7 @@ function createRemoteRetrieve(runner = execFileGitRunner, workspaceDeps = {}, gi
6096
6418
  } catch (cause) {
6097
6419
  throw cloneFailureError(url, gitToken !== void 0, cause);
6098
6420
  }
6099
- return readGitHistory(runner, dest, url);
6421
+ return readGitHistory(runner, dest, url, onProgress);
6100
6422
  }, workspaceDeps);
6101
6423
  };
6102
6424
  }
@@ -6106,11 +6428,11 @@ function createRetrieve(deps = {}) {
6106
6428
  const runner = deps.runner ?? execFileGitRunner;
6107
6429
  const local = createLocalRetrieve(runner);
6108
6430
  const remote = createRemoteRetrieve(runner, deps.workspace, deps.gitToken);
6109
- return async (config) => (isRemoteTarget(config.repoTarget) ? remote : local)(config);
6431
+ return async (config, onProgress) => (isRemoteTarget(config.repoTarget) ? remote : local)(config, onProgress);
6110
6432
  }
6111
6433
 
6112
6434
  // src/cli/provenance.ts
6113
- import { basename } from "path";
6435
+ import { basename, resolve } from "path";
6114
6436
  function buildProvenance(input) {
6115
6437
  const source = isRemoteTarget(input.target) ? "remote" : "local";
6116
6438
  const repo = {
@@ -6183,13 +6505,17 @@ function localName(target) {
6183
6505
  const trimmed = stripTrailingSlashes(target.trim());
6184
6506
  const base = basename(trimmed);
6185
6507
  if (base === "" || base === "." || base === "..") {
6508
+ const resolved = basename(resolve(trimmed === "" ? "." : trimmed));
6509
+ if (resolved !== "" && resolved !== "." && resolved !== "..") {
6510
+ return resolved;
6511
+ }
6186
6512
  return trimmed === "" ? target.trim() : trimmed;
6187
6513
  }
6188
6514
  return base;
6189
6515
  }
6190
6516
 
6191
6517
  // src/cli/version.ts
6192
- var VERSION = "1.1.1";
6518
+ var VERSION = "1.1.3";
6193
6519
 
6194
6520
  // src/cli/write-file.ts
6195
6521
  import { writeFile as fsWriteFile } from "fs/promises";
@@ -6221,7 +6547,11 @@ async function runPipeline(config, deps = {}) {
6221
6547
  if (config.aiMode === "off" && config.provenance.aiMode === "default") {
6222
6548
  ui2.info("Running metrics-only \u2014 for the AI narrative, run interactively or set a provider key.");
6223
6549
  }
6224
- const history = await stage(progress, "Retrieving commit history\u2026", () => retrieve(config));
6550
+ const history = await stage(
6551
+ progress,
6552
+ "Retrieving commit history\u2026",
6553
+ () => retrieve(config, (count) => progress.update(`Retrieving commit history\u2026 ${count} commit(s)`))
6554
+ );
6225
6555
  progress.done(`Retrieved ${history.commits.length} commit(s) from ${history.repoTarget}`);
6226
6556
  ui2.debug?.(`Retrieved ${history.commits.length} commit(s) from ${history.repoTarget}.`);
6227
6557
  const selection = selectCommitsWithNotice(history, projectSelection(config));
@@ -6241,7 +6571,7 @@ async function runPipeline(config, deps = {}) {
6241
6571
  const outcome = await narrateStage(
6242
6572
  progress,
6243
6573
  config,
6244
- () => narrateOutcome(config, narrateConfig, analysis, narrate, preflightReason)
6574
+ (onProgress) => narrateOutcome(config, narrateConfig, analysis, narrate, preflightReason, onProgress)
6245
6575
  );
6246
6576
  const provenance = buildProvenance({
6247
6577
  target: config.repoTarget,
@@ -6277,11 +6607,14 @@ async function stage(progress, label, fn) {
6277
6607
  }
6278
6608
  async function narrateStage(progress, config, run) {
6279
6609
  if (config.aiMode === "off") {
6280
- return run();
6610
+ return run(() => {
6611
+ });
6281
6612
  }
6282
6613
  progress.start("Generating AI narrative\u2026");
6283
6614
  try {
6284
- const outcome = await run();
6615
+ const outcome = await run(({ completed, total, label }) => {
6616
+ progress.update(`${progressBar(completed, total)} ${completed}/${total} \xB7 ${label}`);
6617
+ });
6285
6618
  progress.done(outcome.kind === "narrated" ? "AI narrative ready" : "Narrative unavailable \u2014 metrics-only");
6286
6619
  return outcome;
6287
6620
  } catch (err) {
@@ -6348,14 +6681,14 @@ async function runPreflight(config, narrateConfig, preflight, fetchImpl, ui2) {
6348
6681
  ui2.warn(`\u26A0 Narrative unavailable: ${result.reason}`);
6349
6682
  return result.reason;
6350
6683
  }
6351
- async function narrateOutcome(config, narrateConfig, analysis, narrate, preflightReason) {
6684
+ async function narrateOutcome(config, narrateConfig, analysis, narrate, preflightReason, onProgress) {
6352
6685
  if (config.aiMode === "off") {
6353
6686
  return { kind: "skipped" };
6354
6687
  }
6355
6688
  if (preflightReason !== void 0) {
6356
6689
  return { kind: "degraded", reason: preflightReason };
6357
6690
  }
6358
- return narrate(analysis, narrateConfig);
6691
+ return narrate(analysis, narrateConfig, onProgress);
6359
6692
  }
6360
6693
  function countContributors(commits, mailmap) {
6361
6694
  const keys = /* @__PURE__ */ new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-whisper",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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",