commit-whisper 1.1.1 → 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 +145 -30
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1171,7 +1171,7 @@ function messageForError(err) {
1171
1171
  import pc2 from "picocolors";
1172
1172
  import { isCancel, multiselect as clackMultiselect, select as clackSelect, text as clackText } from "@clack/prompts";
1173
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:";
1174
+ var QUIT_MESSAGE = "Case closed. Until your next commit \u2014 here's the cheatsheet for the road \u{1F6E3}\uFE0F";
1175
1175
  var FLAGS_CHEATSHEET = [
1176
1176
  "Common commands:",
1177
1177
  " commit-whisper . analyze the current repository",
@@ -1472,12 +1472,12 @@ async function waitForKey(output) {
1472
1472
  const wasRaw = stdin.isRaw === true;
1473
1473
  stdin.setRawMode?.(true);
1474
1474
  stdin.resume();
1475
- return await new Promise((resolve) => {
1475
+ return await new Promise((resolve2) => {
1476
1476
  stdin.once("data", (data) => {
1477
1477
  stdin.setRawMode?.(wasRaw);
1478
1478
  stdin.pause();
1479
1479
  const key = data.toString("utf8");
1480
- resolve(key === "" || key === "\x1B" ? "quit" : "continue");
1480
+ resolve2(key === "" || key === "\x1B" ? "quit" : "continue");
1481
1481
  });
1482
1482
  });
1483
1483
  }
@@ -3927,7 +3927,7 @@ function assertNeverProvider(provider) {
3927
3927
 
3928
3928
  // src/narrate/narrate.ts
3929
3929
  function createNarrate(deps = {}) {
3930
- const resolve = deps.resolveModel ?? resolveModel;
3930
+ const resolve2 = deps.resolveModel ?? resolveModel;
3931
3931
  const generate = deps.generate ?? generateNarrative;
3932
3932
  const generateExpl = deps.generateExplanations ?? generateExplanations;
3933
3933
  return async (analysis, config) => {
@@ -3935,7 +3935,7 @@ function createNarrate(deps = {}) {
3935
3935
  return { kind: "skipped" };
3936
3936
  }
3937
3937
  try {
3938
- const model = resolve(config);
3938
+ const model = resolve2(config);
3939
3939
  const [parts, explanations] = await Promise.all([
3940
3940
  generate(model, analysis),
3941
3941
  generateExpl(model, analysis)
@@ -4725,6 +4725,13 @@ function svgDonut(series, label) {
4725
4725
  const a0 = (-90 + cum * 360) * Math.PI / 180;
4726
4726
  const a1 = (-90 + (cum + frac) * 360) * Math.PI / 180;
4727
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
+ }
4728
4735
  const large = frac > 0.5 ? 1 : 0;
4729
4736
  const x0o = r(cx + rOuter * Math.cos(a0));
4730
4737
  const y0o = r(cy + rOuter * Math.sin(a0));
@@ -4773,20 +4780,121 @@ function formatNumber2(value) {
4773
4780
  }
4774
4781
  return Number.isInteger(value) ? String(value) : String(Math.round(value * 100) / 100);
4775
4782
  }
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);
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;
4803
+ }
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;
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;
4786
4826
  }
4787
- function rangeMetrics(metrics) {
4788
- return metrics.filter((m) => m.status === "computed" && rangeField(m.value) !== void 0);
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;
4789
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
+ };
4790
4898
  function subFigure(title2, svg, table) {
4791
4899
  return `<div class="chart-sub">
4792
4900
  <h4>${escapeHtml(title2)}</h4>
@@ -4794,22 +4902,24 @@ ${svg}
4794
4902
  ${table}
4795
4903
  </div>`;
4796
4904
  }
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) {
4905
+ function renderChartSpec(group, spec, byId) {
4906
+ const metric = byId.get(spec.sourceId);
4907
+ if (metric?.status !== "computed") {
4801
4908
  return void 0;
4802
4909
  }
4803
- const label = `Group ${group} \u2014 ${metric.title}`;
4910
+ const label = `Group ${group} \u2014 ${spec.title}`;
4804
4911
  if (spec.kind === "gauge") {
4805
- const range = rangeField(metric.value);
4912
+ const range = spec.gauge?.(metric.value);
4806
4913
  if (range === void 0) {
4807
4914
  return void 0;
4808
4915
  }
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);
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;
4811
4922
  }
4812
- const series = extractSeries(metric.value);
4813
4923
  let svg;
4814
4924
  switch (spec.kind) {
4815
4925
  case "line":
@@ -4827,12 +4937,13 @@ function renderSubChart(group, spec, metrics) {
4827
4937
  default:
4828
4938
  svg = svgDonut(series, label);
4829
4939
  }
4830
- return subFigure(metric.title, svg, dataTable(series, "Value", metric.title));
4940
+ return subFigure(spec.title, svg, dataTable(series, "Value", spec.title));
4831
4941
  }
4832
4942
  function groupOverviewPanel(group, metrics) {
4833
4943
  const description = GROUP_DESCRIPTION[group];
4834
4944
  const label = `Group ${group} overview`;
4835
- 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);
4836
4947
  if (subs.length === 0) {
4837
4948
  return `<figure class="chart-panel" aria-label="${escapeHtml(label)}">
4838
4949
  <figcaption>${escapeHtml(description)}</figcaption>
@@ -6110,7 +6221,7 @@ function createRetrieve(deps = {}) {
6110
6221
  }
6111
6222
 
6112
6223
  // src/cli/provenance.ts
6113
- import { basename } from "path";
6224
+ import { basename, resolve } from "path";
6114
6225
  function buildProvenance(input) {
6115
6226
  const source = isRemoteTarget(input.target) ? "remote" : "local";
6116
6227
  const repo = {
@@ -6183,13 +6294,17 @@ function localName(target) {
6183
6294
  const trimmed = stripTrailingSlashes(target.trim());
6184
6295
  const base = basename(trimmed);
6185
6296
  if (base === "" || base === "." || base === "..") {
6297
+ const resolved = basename(resolve(trimmed === "" ? "." : trimmed));
6298
+ if (resolved !== "" && resolved !== "." && resolved !== "..") {
6299
+ return resolved;
6300
+ }
6186
6301
  return trimmed === "" ? target.trim() : trimmed;
6187
6302
  }
6188
6303
  return base;
6189
6304
  }
6190
6305
 
6191
6306
  // src/cli/version.ts
6192
- var VERSION = "1.1.1";
6307
+ var VERSION = "1.1.2";
6193
6308
 
6194
6309
  // src/cli/write-file.ts
6195
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.1",
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",