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.
- package/dist/index.js +482 -188
- 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 `${
|
|
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((
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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)}`;
|
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
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
|
|
4638
|
-
|
|
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
|
|
4648
|
-
const
|
|
4649
|
-
|
|
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 ${
|
|
4910
|
+
const label = `Group ${group} \u2014 ${spec.title}`;
|
|
4654
4911
|
if (spec.kind === "gauge") {
|
|
4655
|
-
const range =
|
|
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:
|
|
4660
|
-
return subFigure(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
|
5538
|
+
return series;
|
|
5280
5539
|
}
|
|
5281
5540
|
}
|
|
5282
5541
|
return void 0;
|
|
5283
5542
|
}
|
|
5284
|
-
function groupOverview(
|
|
5285
|
-
const
|
|
5286
|
-
if (
|
|
5543
|
+
function groupOverview(_group, metrics) {
|
|
5544
|
+
const series = representativeSeries(metrics);
|
|
5545
|
+
if (series === void 0) {
|
|
5287
5546
|
return GROUP_OVERVIEW_NONE;
|
|
5288
5547
|
}
|
|
5289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
6307
|
+
var VERSION = "1.1.2";
|
|
6014
6308
|
|
|
6015
6309
|
// src/cli/write-file.ts
|
|
6016
6310
|
import { writeFile as fsWriteFile } from "fs/promises";
|