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