commit-whisper 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +339 -160
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1168,8 +1168,10 @@ function messageForError(err) {
|
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
1170
|
// src/cli/interactive.ts
|
|
1171
|
+
import pc2 from "picocolors";
|
|
1171
1172
|
import { isCancel, multiselect as clackMultiselect, select as clackSelect, text as clackText } from "@clack/prompts";
|
|
1172
1173
|
var LAUNCHPAD_TAGLINE = "commit-whisper \xB7 \u{1F575}\uFE0F I know what you did last commit";
|
|
1174
|
+
var QUIT_MESSAGE = "Case closed. \u{1F575}\uFE0F Until your next commit \u2014 here's the cheatsheet for the road:";
|
|
1173
1175
|
var FLAGS_CHEATSHEET = [
|
|
1174
1176
|
"Common commands:",
|
|
1175
1177
|
" commit-whisper . analyze the current repository",
|
|
@@ -1194,6 +1196,11 @@ var TIER_LABEL = {
|
|
|
1194
1196
|
"single-device": "Single-device",
|
|
1195
1197
|
unlimited: "Unlimited"
|
|
1196
1198
|
};
|
|
1199
|
+
function tierSegment(tier, color) {
|
|
1200
|
+
const c = pc2.createColors(color);
|
|
1201
|
+
const label = TIER_LABEL[tier];
|
|
1202
|
+
return tier === "free" ? c.red(label) : c.green(label);
|
|
1203
|
+
}
|
|
1197
1204
|
var OUTPUT_FORMAT_OPTIONS = [
|
|
1198
1205
|
{ value: "terminal", label: "terminal" },
|
|
1199
1206
|
{ value: "html", label: "html" },
|
|
@@ -1233,8 +1240,8 @@ function cwdSegment(state) {
|
|
|
1233
1240
|
}
|
|
1234
1241
|
return `${state.cwdLabel} (${state.branch ?? "detached"})`;
|
|
1235
1242
|
}
|
|
1236
|
-
function formatReadinessLine(state) {
|
|
1237
|
-
return `${
|
|
1243
|
+
function formatReadinessLine(state, color = false) {
|
|
1244
|
+
return `${tierSegment(state.tier, color)} \xB7 AI: ${aiSegment(state)} \xB7 cwd: ${cwdSegment(state)}`;
|
|
1238
1245
|
}
|
|
1239
1246
|
function buildLaunchpadOptions(state) {
|
|
1240
1247
|
const options = [
|
|
@@ -1373,7 +1380,11 @@ function formatStatusReport(state, envVars, reachability, config) {
|
|
|
1373
1380
|
if (reachability.kind === "not-configured") {
|
|
1374
1381
|
lines.push("", NO_AI_FIX);
|
|
1375
1382
|
}
|
|
1376
|
-
return lines.join("\n");
|
|
1383
|
+
return paintStatusMarks(lines.join("\n"), config?.color ?? false);
|
|
1384
|
+
}
|
|
1385
|
+
function paintStatusMarks(text, color) {
|
|
1386
|
+
const c = pc2.createColors(color);
|
|
1387
|
+
return text.replaceAll("\u2713", c.green("\u2713")).replaceAll("\u2717", c.red("\u2717"));
|
|
1377
1388
|
}
|
|
1378
1389
|
var GUIDED_DATE = /^\d{4}-\d{2}-\d{2}/;
|
|
1379
1390
|
function formatEquivalentCommand(target, flags) {
|
|
@@ -1792,6 +1803,8 @@ var DEFAULT_COFFEE_URL = "https://buymeacoffee.com/georgiosnikitas";
|
|
|
1792
1803
|
async function dispatchAction(deps, action, output) {
|
|
1793
1804
|
switch (action) {
|
|
1794
1805
|
case "quit":
|
|
1806
|
+
writeLine(output, QUIT_MESSAGE);
|
|
1807
|
+
writeLine(output, "");
|
|
1795
1808
|
writeLine(output, FLAGS_CHEATSHEET);
|
|
1796
1809
|
return "quit";
|
|
1797
1810
|
case "help":
|
|
@@ -1836,7 +1849,7 @@ async function runLaunchpad(deps) {
|
|
|
1836
1849
|
const pause = deps.waitForKey ?? waitForKey;
|
|
1837
1850
|
const writeHeader = () => {
|
|
1838
1851
|
writeLine(output, LAUNCHPAD_TAGLINE);
|
|
1839
|
-
writeLine(output, formatReadinessLine(deps.state));
|
|
1852
|
+
writeLine(output, formatReadinessLine(deps.state, deps.doctorConfig?.color));
|
|
1840
1853
|
};
|
|
1841
1854
|
if (!repaint) {
|
|
1842
1855
|
writeHeader();
|
|
@@ -3954,7 +3967,7 @@ function narrationReason(err, secret) {
|
|
|
3954
3967
|
}
|
|
3955
3968
|
|
|
3956
3969
|
// src/render/terminal/terminal-renderer.ts
|
|
3957
|
-
import
|
|
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 ?
|
|
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
|
-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4699
|
+
const viewBox = `0 0 ${W} ${H}`;
|
|
4700
|
+
return `${open(label, "chart-radar", viewBox)}${fillGradient(id, true)}${rings}${axes}<polygon class="radar-area" points="${dataPts}" fill="url(#${id})"/>${dots}${labels}</svg>`;
|
|
4551
4701
|
}
|
|
4552
4702
|
function svgRadialGauge(value, max, label) {
|
|
4553
4703
|
const id = `cw-rgauge-${hashId(label)}`;
|
|
@@ -4605,7 +4755,7 @@ var GROUP_DESCRIPTION = {
|
|
|
4605
4755
|
F: "Overall repository health signals."
|
|
4606
4756
|
};
|
|
4607
4757
|
function dataTable(series, valueHeader, caption) {
|
|
4608
|
-
const rows = series.map((p) => `<tr><th scope="row">${escapeHtml(p.label)}</th><td>${escapeHtml(
|
|
4758
|
+
const rows = series.map((p) => `<tr><th scope="row">${escapeHtml(p.label)}</th><td>${escapeHtml(formatNumber2(p.value))}</td></tr>`).join("\n");
|
|
4609
4759
|
return `<details class="data-table" open>
|
|
4610
4760
|
<summary>Show data table</summary>
|
|
4611
4761
|
<table>
|
|
@@ -4617,7 +4767,7 @@ ${rows}
|
|
|
4617
4767
|
</table>
|
|
4618
4768
|
</details>`;
|
|
4619
4769
|
}
|
|
4620
|
-
function
|
|
4770
|
+
function formatNumber2(value) {
|
|
4621
4771
|
if (!Number.isFinite(value)) {
|
|
4622
4772
|
return "\u2014";
|
|
4623
4773
|
}
|
|
@@ -4660,7 +4810,23 @@ function renderSubChart(group, spec, metrics) {
|
|
|
4660
4810
|
return subFigure(metric.title, svgRadialGauge(range.value, range.max, label), table);
|
|
4661
4811
|
}
|
|
4662
4812
|
const series = extractSeries(metric.value);
|
|
4663
|
-
|
|
4813
|
+
let svg;
|
|
4814
|
+
switch (spec.kind) {
|
|
4815
|
+
case "line":
|
|
4816
|
+
svg = svgLine(series, label);
|
|
4817
|
+
break;
|
|
4818
|
+
case "bars":
|
|
4819
|
+
svg = svgBars(series, label);
|
|
4820
|
+
break;
|
|
4821
|
+
case "hbars":
|
|
4822
|
+
svg = svgHBars(series, label);
|
|
4823
|
+
break;
|
|
4824
|
+
case "radar":
|
|
4825
|
+
svg = svgRadar(series, 100, label);
|
|
4826
|
+
break;
|
|
4827
|
+
default:
|
|
4828
|
+
svg = svgDonut(series, label);
|
|
4829
|
+
}
|
|
4664
4830
|
return subFigure(metric.title, svg, dataTable(series, "Value", metric.title));
|
|
4665
4831
|
}
|
|
4666
4832
|
function groupOverviewPanel(group, metrics) {
|
|
@@ -4923,19 +5089,20 @@ function metricStat(metric) {
|
|
|
4923
5089
|
return `${fmtStat(range.value)}${range.max === 100 ? "%" : ""}`;
|
|
4924
5090
|
}
|
|
4925
5091
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
4926
|
-
|
|
4927
|
-
for (const key of ["total", "count", "score", "busFactor", "value"]) {
|
|
4928
|
-
const hit = nums.find(([k]) => k === key);
|
|
4929
|
-
if (hit !== void 0) {
|
|
4930
|
-
return fmtStat(hit[1]);
|
|
4931
|
-
}
|
|
4932
|
-
}
|
|
4933
|
-
if (nums.length === 1) {
|
|
4934
|
-
return fmtStat(nums[0][1]);
|
|
4935
|
-
}
|
|
5092
|
+
return objectStat(value);
|
|
4936
5093
|
}
|
|
4937
5094
|
return "";
|
|
4938
5095
|
}
|
|
5096
|
+
function objectStat(value) {
|
|
5097
|
+
const nums = Object.entries(value).filter((e) => typeof e[1] === "number" && Number.isFinite(e[1]));
|
|
5098
|
+
for (const key of ["total", "count", "score", "busFactor", "value"]) {
|
|
5099
|
+
const hit = nums.find(([k]) => k === key);
|
|
5100
|
+
if (hit !== void 0) {
|
|
5101
|
+
return fmtStat(hit[1]);
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
return nums.length === 1 ? fmtStat(nums[0][1]) : "";
|
|
5105
|
+
}
|
|
4939
5106
|
function fmtStat(n) {
|
|
4940
5107
|
return Number.isInteger(n) ? String(n) : String(Math.round(n * 100) / 100);
|
|
4941
5108
|
}
|
|
@@ -4972,7 +5139,7 @@ function formatCount2(n) {
|
|
|
4972
5139
|
function isoDate2(iso) {
|
|
4973
5140
|
return iso.slice(0, 10);
|
|
4974
5141
|
}
|
|
4975
|
-
var STYLE = `
|
|
5142
|
+
var STYLE = String.raw`
|
|
4976
5143
|
:root {
|
|
4977
5144
|
color-scheme: dark light;
|
|
4978
5145
|
--bg: #0a0e14; --surface: #11161f; --surface-2: #161c28;
|
|
@@ -5031,7 +5198,7 @@ a:focus-visible, :focus-visible { outline: 2px solid var(--accent); outline-offs
|
|
|
5031
5198
|
}
|
|
5032
5199
|
.masthead h1 { margin: 0; font-size: 1.95rem; position: relative; display: flex; align-items: center; gap: 0.75rem; }
|
|
5033
5200
|
.masthead h1::before {
|
|
5034
|
-
content: "
|
|
5201
|
+
content: "\25D1"; display: inline-grid; place-items: center;
|
|
5035
5202
|
width: 2.7rem; height: 2.7rem; font-size: 1.45rem; color: #fff;
|
|
5036
5203
|
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
|
5037
5204
|
border-radius: 0.72rem; box-shadow: 0 10px 26px -8px rgba(124,92,255,0.75);
|
|
@@ -5075,7 +5242,7 @@ a:focus-visible, :focus-visible { outline: 2px solid var(--accent); outline-offs
|
|
|
5075
5242
|
.lead, .explanation p, .coaching-intro, .coaching-closing { color: var(--fg-soft); }
|
|
5076
5243
|
.key-findings { list-style: none; padding: 0; margin: 1.2rem 0 0; display: grid; gap: 0.6rem; }
|
|
5077
5244
|
.key-findings li { position: relative; padding: 0.8rem 1rem 0.8rem 2.5rem; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; }
|
|
5078
|
-
.key-findings li::before { content: "
|
|
5245
|
+
.key-findings li::before { content: "\203A"; position: absolute; left: 1rem; top: 0.7rem; color: var(--accent); font-weight: 800; font-size: 1.1rem; }
|
|
5079
5246
|
.chapter { background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--accent-2); border-radius: 14px; padding: 1.1rem 1.35rem; margin: 1rem 0; }
|
|
5080
5247
|
.chapter h3 { margin: 0 0 0.6rem; }
|
|
5081
5248
|
.chapter ol { margin: 0; padding-left: 1.2rem; display: grid; gap: 0.5rem; color: var(--fg-soft); }
|
|
@@ -5246,25 +5413,6 @@ function textBars(series) {
|
|
|
5246
5413
|
});
|
|
5247
5414
|
return ["```", ...rows, "```"].join("\n");
|
|
5248
5415
|
}
|
|
5249
|
-
function mermaidLabel(text) {
|
|
5250
|
-
const cleaned = text.replaceAll(/["[\],\r\n\t]+/g, " ").replaceAll(/\s+/g, " ").trim();
|
|
5251
|
-
return cleaned === "" ? "-" : cleaned;
|
|
5252
|
-
}
|
|
5253
|
-
function mermaidXychart(series, title2) {
|
|
5254
|
-
if (series.length === 0) {
|
|
5255
|
-
return "";
|
|
5256
|
-
}
|
|
5257
|
-
const axis = series.map((p) => `"${mermaidLabel(p.label)}"`).join(", ");
|
|
5258
|
-
const values = series.map((p) => round2(p.value)).join(", ");
|
|
5259
|
-
return [
|
|
5260
|
-
"```mermaid",
|
|
5261
|
-
"xychart-beta",
|
|
5262
|
-
` title "${mermaidLabel(title2)}"`,
|
|
5263
|
-
` x-axis [${axis}]`,
|
|
5264
|
-
` bar [${values}]`,
|
|
5265
|
-
"```"
|
|
5266
|
-
].join("\n");
|
|
5267
|
-
}
|
|
5268
5416
|
function representativeSeries(metrics) {
|
|
5269
5417
|
for (const metric of metrics) {
|
|
5270
5418
|
if (metric.status !== "computed") {
|
|
@@ -5276,20 +5424,17 @@ function representativeSeries(metrics) {
|
|
|
5276
5424
|
}
|
|
5277
5425
|
const series = extractSeries(metric.value);
|
|
5278
5426
|
if (series.length > 0) {
|
|
5279
|
-
return
|
|
5427
|
+
return series;
|
|
5280
5428
|
}
|
|
5281
5429
|
}
|
|
5282
5430
|
return void 0;
|
|
5283
5431
|
}
|
|
5284
|
-
function groupOverview(
|
|
5285
|
-
const
|
|
5286
|
-
if (
|
|
5432
|
+
function groupOverview(_group, metrics) {
|
|
5433
|
+
const series = representativeSeries(metrics);
|
|
5434
|
+
if (series === void 0) {
|
|
5287
5435
|
return GROUP_OVERVIEW_NONE;
|
|
5288
5436
|
}
|
|
5289
|
-
|
|
5290
|
-
return mermaidXychart(rep.series, `Group ${group} overview`);
|
|
5291
|
-
}
|
|
5292
|
-
return textBars(rep.series);
|
|
5437
|
+
return textBars(series);
|
|
5293
5438
|
}
|
|
5294
5439
|
function scalarNumber(value) {
|
|
5295
5440
|
if (typeof value === "number") {
|
|
@@ -5517,14 +5662,48 @@ function metricBullets(metric, explanations) {
|
|
|
5517
5662
|
if (explanation === void 0) {
|
|
5518
5663
|
return value;
|
|
5519
5664
|
}
|
|
5520
|
-
|
|
5665
|
+
const separator = value.includes("\n") ? "\n\n" : "\n";
|
|
5666
|
+
return [value, facetBullets2(explanation).join("\n")].join(separator);
|
|
5521
5667
|
}
|
|
5522
5668
|
function valueBullet2(metric) {
|
|
5523
5669
|
if (metric.status !== "computed") {
|
|
5524
5670
|
const reason = metric.reason === void 0 ? "" : ` \u2014 ${escapeCell(metric.reason)}`;
|
|
5525
5671
|
return `- **Value** \u2014 _not available${reason}_`;
|
|
5526
5672
|
}
|
|
5527
|
-
|
|
5673
|
+
const value = metric.value;
|
|
5674
|
+
if (value === null || typeof value !== "object") {
|
|
5675
|
+
return `- **Value** \u2014 ${escapeCell(formatValue3(value))}`;
|
|
5676
|
+
}
|
|
5677
|
+
const series = extractSeries(value);
|
|
5678
|
+
if (series.length === 0) {
|
|
5679
|
+
const tree = buildValueTree(value);
|
|
5680
|
+
if (tree.kind === "scalar") {
|
|
5681
|
+
return `- **Value** \u2014 ${escapeCell(tree.text)}`;
|
|
5682
|
+
}
|
|
5683
|
+
return [`- **Value**`, ...treeBullets(tree.entries, " ")].join("\n");
|
|
5684
|
+
}
|
|
5685
|
+
if (series.length === 1) {
|
|
5686
|
+
return `- **Value** \u2014 ${escapeCell(formatNumber3(series[0].value))}`;
|
|
5687
|
+
}
|
|
5688
|
+
return valueTable(value);
|
|
5689
|
+
}
|
|
5690
|
+
function treeBullets(entries, indent) {
|
|
5691
|
+
return entries.flatMap((entry) => {
|
|
5692
|
+
if (entry.child.kind === "scalar") {
|
|
5693
|
+
const text = entry.child.text === "" ? escapeCell(entry.label) : `${escapeCell(entry.label)}: ${escapeCell(entry.child.text)}`;
|
|
5694
|
+
return [`${indent}- ${text}`];
|
|
5695
|
+
}
|
|
5696
|
+
return [`${indent}- ${escapeCell(entry.label)}`, ...treeBullets(entry.child.entries, `${indent} `)];
|
|
5697
|
+
});
|
|
5698
|
+
}
|
|
5699
|
+
function formatNumber3(n) {
|
|
5700
|
+
return Number.isFinite(n) ? String(Math.round(n * 100) / 100) : "0";
|
|
5701
|
+
}
|
|
5702
|
+
function valueTable(value) {
|
|
5703
|
+
const shape = detectShape(value);
|
|
5704
|
+
const labelHeader = shape === "timeseries" ? "Period" : shape === "distribution" ? "Item" : "Field";
|
|
5705
|
+
const rows = extractSeries(value).map((point) => `| ${escapeCell(point.label)} | ${escapeCell(formatNumber3(point.value))} |`);
|
|
5706
|
+
return [`- **Value**`, "", `| ${labelHeader} | Value |`, "| --- | --- |", ...rows].join("\n");
|
|
5528
5707
|
}
|
|
5529
5708
|
function facetBullets2(explanation) {
|
|
5530
5709
|
return [
|
|
@@ -6010,7 +6189,7 @@ function localName(target) {
|
|
|
6010
6189
|
}
|
|
6011
6190
|
|
|
6012
6191
|
// src/cli/version.ts
|
|
6013
|
-
var VERSION = "1.1.
|
|
6192
|
+
var VERSION = "1.1.1";
|
|
6014
6193
|
|
|
6015
6194
|
// src/cli/write-file.ts
|
|
6016
6195
|
import { writeFile as fsWriteFile } from "fs/promises";
|