@getcodesentinel/codesentinel 1.16.0 → 1.17.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/README.md +23 -31
- package/dist/index.js +1103 -1095
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
package/dist/index.js
CHANGED
|
@@ -7,6 +7,12 @@ import { Command, Option } from "commander";
|
|
|
7
7
|
import { existsSync, readFileSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { setTimeout as sleep } from "timers/promises";
|
|
10
|
+
import { join as join4 } from "path";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join as join2 } from "path";
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
import { mkdir, readdir, readFile, rename, stat, unlink, writeFile } from "fs/promises";
|
|
15
|
+
import { join as join3 } from "path";
|
|
10
16
|
var round4 = (value) => Number(value.toFixed(4));
|
|
11
17
|
var normalizeNodes = (nodes) => {
|
|
12
18
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -614,6 +620,23 @@ var parseLockfileExtraction = (lockfileKind, lockfileRaw, directSpecs) => {
|
|
|
614
620
|
throw new Error("unsupported_lockfile_format");
|
|
615
621
|
}
|
|
616
622
|
};
|
|
623
|
+
var cachedFetch = async (options) => {
|
|
624
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
625
|
+
const cachedEntry = options.cacheStore === null ? null : await options.cacheStore.get(options.key);
|
|
626
|
+
if (cachedEntry !== null && nowMs - cachedEntry.fetchedAtMs <= options.ttlMs) {
|
|
627
|
+
return cachedEntry.value;
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
const fresh = await options.fetchFresh();
|
|
631
|
+
if (fresh !== null) {
|
|
632
|
+
void options.cacheStore?.set(options.key, { value: fresh, fetchedAtMs: nowMs }).catch(() => {
|
|
633
|
+
});
|
|
634
|
+
return fresh;
|
|
635
|
+
}
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
return cachedEntry?.value ?? null;
|
|
639
|
+
};
|
|
617
640
|
var parseRetryAfterMs = (value) => {
|
|
618
641
|
if (value === null) {
|
|
619
642
|
return null;
|
|
@@ -640,8 +663,202 @@ var fetchJsonWithRetry = async (url, options) => {
|
|
|
640
663
|
}
|
|
641
664
|
return null;
|
|
642
665
|
};
|
|
666
|
+
var resolveCodesentinelCacheDir = (env = process.env) => {
|
|
667
|
+
const explicit = env["CODESENTINEL_CACHE_DIR"]?.trim();
|
|
668
|
+
if (explicit !== void 0 && explicit.length > 0) {
|
|
669
|
+
return explicit;
|
|
670
|
+
}
|
|
671
|
+
if (process.platform === "win32") {
|
|
672
|
+
const localAppData = env["LOCALAPPDATA"]?.trim();
|
|
673
|
+
if (localAppData !== void 0 && localAppData.length > 0) {
|
|
674
|
+
return join2(localAppData, "codesentinel", "Cache");
|
|
675
|
+
}
|
|
676
|
+
return join2(homedir(), "AppData", "Local", "codesentinel", "Cache");
|
|
677
|
+
}
|
|
678
|
+
const xdgCacheHome = env["XDG_CACHE_HOME"]?.trim();
|
|
679
|
+
if (xdgCacheHome !== void 0 && xdgCacheHome.length > 0) {
|
|
680
|
+
return join2(xdgCacheHome, "codesentinel");
|
|
681
|
+
}
|
|
682
|
+
return join2(homedir(), ".cache", "codesentinel");
|
|
683
|
+
};
|
|
684
|
+
var parseCacheEntryPayload = (value) => {
|
|
685
|
+
if (typeof value !== "object" || value === null) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const payload = value;
|
|
689
|
+
if (typeof payload.key !== "string" || payload.key.length === 0) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
if (typeof payload.fetchedAtMs !== "number" || !Number.isFinite(payload.fetchedAtMs)) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
key: payload.key,
|
|
697
|
+
fetchedAtMs: payload.fetchedAtMs,
|
|
698
|
+
value: payload.value
|
|
699
|
+
};
|
|
700
|
+
};
|
|
701
|
+
var DEFAULT_OPTIONS = {
|
|
702
|
+
maxBytes: 100 * 1024 * 1024,
|
|
703
|
+
maxEntryBytes: 4 * 1024 * 1024,
|
|
704
|
+
sweepIntervalWrites: 25
|
|
705
|
+
};
|
|
706
|
+
var FileCacheStore = class {
|
|
707
|
+
constructor(directoryPath, options = DEFAULT_OPTIONS) {
|
|
708
|
+
this.directoryPath = directoryPath;
|
|
709
|
+
this.options = options;
|
|
710
|
+
}
|
|
711
|
+
byKey = /* @__PURE__ */ new Map();
|
|
712
|
+
inFlightWrites = /* @__PURE__ */ new Map();
|
|
713
|
+
writesSinceSweep = 0;
|
|
714
|
+
sweepPromise = Promise.resolve();
|
|
715
|
+
toEntryPath(key) {
|
|
716
|
+
const digest = createHash("sha256").update(key).digest("hex");
|
|
717
|
+
return join3(this.directoryPath, `${digest}.json`);
|
|
718
|
+
}
|
|
719
|
+
async writeEntry(key, entry) {
|
|
720
|
+
const filePath = this.toEntryPath(key);
|
|
721
|
+
await mkdir(this.directoryPath, { recursive: true });
|
|
722
|
+
const tempPath = `${filePath}.tmp`;
|
|
723
|
+
const payload = {
|
|
724
|
+
key,
|
|
725
|
+
fetchedAtMs: entry.fetchedAtMs,
|
|
726
|
+
value: entry.value
|
|
727
|
+
};
|
|
728
|
+
const raw = JSON.stringify(payload);
|
|
729
|
+
if (Buffer.byteLength(raw, "utf8") > this.options.maxEntryBytes) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
await writeFile(tempPath, raw, "utf8");
|
|
733
|
+
await rename(tempPath, filePath);
|
|
734
|
+
}
|
|
735
|
+
async sweepIfNeeded() {
|
|
736
|
+
this.writesSinceSweep += 1;
|
|
737
|
+
if (this.writesSinceSweep < this.options.sweepIntervalWrites) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
this.writesSinceSweep = 0;
|
|
741
|
+
this.sweepPromise = this.sweepPromise.catch(() => {
|
|
742
|
+
}).then(async () => {
|
|
743
|
+
await this.evictToSizeLimit();
|
|
744
|
+
});
|
|
745
|
+
await this.sweepPromise;
|
|
746
|
+
}
|
|
747
|
+
async evictToSizeLimit() {
|
|
748
|
+
let entries;
|
|
749
|
+
try {
|
|
750
|
+
const dirEntries = await readdir(this.directoryPath, { withFileTypes: true });
|
|
751
|
+
entries = (await Promise.all(
|
|
752
|
+
dirEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map(async (entry) => {
|
|
753
|
+
const path = join3(this.directoryPath, entry.name);
|
|
754
|
+
const info = await stat(path);
|
|
755
|
+
return { path, size: info.size, mtimeMs: info.mtimeMs };
|
|
756
|
+
})
|
|
757
|
+
)).filter((entry) => Number.isFinite(entry.size) && entry.size > 0);
|
|
758
|
+
} catch {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
let totalBytes = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
762
|
+
if (totalBytes <= this.options.maxBytes) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
entries.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
766
|
+
for (const entry of entries) {
|
|
767
|
+
if (totalBytes <= this.options.maxBytes) {
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
await unlink(entry.path);
|
|
772
|
+
totalBytes -= entry.size;
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async get(key) {
|
|
778
|
+
if (this.byKey.has(key)) {
|
|
779
|
+
return this.byKey.get(key) ?? null;
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
const raw = await readFile(this.toEntryPath(key), "utf8");
|
|
783
|
+
const parsed = parseCacheEntryPayload(JSON.parse(raw));
|
|
784
|
+
if (parsed === null || parsed.key !== key) {
|
|
785
|
+
this.byKey.set(key, null);
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
const value = { fetchedAtMs: parsed.fetchedAtMs, value: parsed.value };
|
|
789
|
+
this.byKey.set(key, value);
|
|
790
|
+
return value;
|
|
791
|
+
} catch {
|
|
792
|
+
this.byKey.set(key, null);
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async set(key, entry) {
|
|
797
|
+
const normalized = entry;
|
|
798
|
+
this.byKey.set(key, normalized);
|
|
799
|
+
const previous = this.inFlightWrites.get(key) ?? Promise.resolve();
|
|
800
|
+
const next = previous.catch(() => {
|
|
801
|
+
}).then(async () => {
|
|
802
|
+
await this.writeEntry(key, normalized);
|
|
803
|
+
await this.sweepIfNeeded();
|
|
804
|
+
});
|
|
805
|
+
this.inFlightWrites.set(key, next);
|
|
806
|
+
await next;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
var SIX_HOURS_MS = 6 * 60 * 60 * 1e3;
|
|
810
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
811
|
+
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
812
|
+
var DEFAULT_MAX_ENTRY_BYTES = 4 * 1024 * 1024;
|
|
813
|
+
var DEFAULT_SWEEP_INTERVAL_WRITES = 25;
|
|
814
|
+
var cacheStoreSingleton;
|
|
815
|
+
var parsePositiveIntegerFromEnv = (value, fallback) => {
|
|
816
|
+
if (value === void 0) {
|
|
817
|
+
return fallback;
|
|
818
|
+
}
|
|
819
|
+
const parsed = Number.parseInt(value, 10);
|
|
820
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
821
|
+
return fallback;
|
|
822
|
+
}
|
|
823
|
+
return parsed;
|
|
824
|
+
};
|
|
825
|
+
var cacheDisabled = (env) => {
|
|
826
|
+
const mode = env["CODESENTINEL_CACHE_MODE"]?.trim().toLowerCase();
|
|
827
|
+
return mode === "none";
|
|
828
|
+
};
|
|
829
|
+
var getNpmMetadataCacheStore = () => {
|
|
830
|
+
if (cacheStoreSingleton !== void 0) {
|
|
831
|
+
return cacheStoreSingleton;
|
|
832
|
+
}
|
|
833
|
+
if (cacheDisabled(process.env)) {
|
|
834
|
+
cacheStoreSingleton = null;
|
|
835
|
+
return cacheStoreSingleton;
|
|
836
|
+
}
|
|
837
|
+
const path = join4(resolveCodesentinelCacheDir(process.env), "npm-metadata-v2");
|
|
838
|
+
cacheStoreSingleton = new FileCacheStore(path, {
|
|
839
|
+
maxBytes: parsePositiveIntegerFromEnv(
|
|
840
|
+
process.env["CODESENTINEL_CACHE_MAX_BYTES"],
|
|
841
|
+
DEFAULT_MAX_BYTES
|
|
842
|
+
),
|
|
843
|
+
maxEntryBytes: parsePositiveIntegerFromEnv(
|
|
844
|
+
process.env["CODESENTINEL_CACHE_MAX_ENTRY_BYTES"],
|
|
845
|
+
DEFAULT_MAX_ENTRY_BYTES
|
|
846
|
+
),
|
|
847
|
+
sweepIntervalWrites: parsePositiveIntegerFromEnv(
|
|
848
|
+
process.env["CODESENTINEL_CACHE_SWEEP_INTERVAL_WRITES"],
|
|
849
|
+
DEFAULT_SWEEP_INTERVAL_WRITES
|
|
850
|
+
)
|
|
851
|
+
});
|
|
852
|
+
return cacheStoreSingleton;
|
|
853
|
+
};
|
|
854
|
+
var getPackumentCacheTtlMs = () => parsePositiveIntegerFromEnv(process.env["CODESENTINEL_CACHE_TTL_PACKUMENT_MS"], SIX_HOURS_MS);
|
|
855
|
+
var getWeeklyDownloadsCacheTtlMs = () => parsePositiveIntegerFromEnv(process.env["CODESENTINEL_CACHE_TTL_DOWNLOADS_MS"], ONE_DAY_MS);
|
|
856
|
+
var toMetadataPackumentCacheKey = (name) => `npm:packument:metadata:${name}`;
|
|
857
|
+
var toGraphPackumentCacheKey = (name) => `npm:packument:graph:${name}`;
|
|
858
|
+
var toWeeklyDownloadsCacheKey = (name) => `npm:downloads:last-week:${name}`;
|
|
643
859
|
var MAX_RETRIES = 3;
|
|
644
860
|
var RETRY_BASE_DELAY_MS = 500;
|
|
861
|
+
var PACKUMENT_CACHE_STORE = getNpmMetadataCacheStore();
|
|
645
862
|
var parsePrerelease = (value) => {
|
|
646
863
|
if (value === void 0 || value.length === 0) {
|
|
647
864
|
return [];
|
|
@@ -892,23 +1109,62 @@ var resolveRangeVersion = (versions, requested) => {
|
|
|
892
1109
|
}
|
|
893
1110
|
return null;
|
|
894
1111
|
};
|
|
1112
|
+
var slimPackumentForGraph = (payload) => {
|
|
1113
|
+
const versions = payload.versions ?? {};
|
|
1114
|
+
const dependenciesByVersion = {};
|
|
1115
|
+
const versionNames = Object.keys(versions);
|
|
1116
|
+
for (const [version2, manifest] of Object.entries(versions)) {
|
|
1117
|
+
const dependenciesRaw = manifest?.dependencies ?? {};
|
|
1118
|
+
const dependencies = {};
|
|
1119
|
+
for (const [dependencyName, dependencyRange] of Object.entries(dependenciesRaw)) {
|
|
1120
|
+
if (dependencyName.length > 0 && dependencyRange.length > 0) {
|
|
1121
|
+
dependencies[dependencyName] = dependencyRange;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (Object.keys(dependencies).length > 0) {
|
|
1125
|
+
dependenciesByVersion[version2] = dependencies;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const slim = {
|
|
1129
|
+
versionNames,
|
|
1130
|
+
dependenciesByVersion
|
|
1131
|
+
};
|
|
1132
|
+
if (payload["dist-tags"] !== void 0) {
|
|
1133
|
+
slim["dist-tags"] = payload["dist-tags"];
|
|
1134
|
+
}
|
|
1135
|
+
return slim;
|
|
1136
|
+
};
|
|
895
1137
|
var fetchPackument = async (name) => {
|
|
896
1138
|
const encodedName = encodeURIComponent(name);
|
|
897
1139
|
try {
|
|
898
|
-
return await
|
|
899
|
-
|
|
900
|
-
|
|
1140
|
+
return await cachedFetch({
|
|
1141
|
+
key: toGraphPackumentCacheKey(name),
|
|
1142
|
+
ttlMs: getPackumentCacheTtlMs(),
|
|
1143
|
+
cacheStore: PACKUMENT_CACHE_STORE,
|
|
1144
|
+
fetchFresh: async () => {
|
|
1145
|
+
const payload = await fetchJsonWithRetry(
|
|
1146
|
+
`https://registry.npmjs.org/${encodedName}`,
|
|
1147
|
+
{
|
|
1148
|
+
retries: MAX_RETRIES,
|
|
1149
|
+
baseDelayMs: RETRY_BASE_DELAY_MS
|
|
1150
|
+
}
|
|
1151
|
+
);
|
|
1152
|
+
if (payload === null) {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
return slimPackumentForGraph(payload);
|
|
1156
|
+
}
|
|
901
1157
|
});
|
|
902
1158
|
} catch {
|
|
903
1159
|
return null;
|
|
904
1160
|
}
|
|
905
1161
|
};
|
|
906
1162
|
var resolveRequestedVersion = (packument, requested) => {
|
|
907
|
-
const
|
|
908
|
-
const
|
|
1163
|
+
const versionKeys = [...packument.versionNames];
|
|
1164
|
+
const versionSet = new Set(versionKeys);
|
|
909
1165
|
const tags = packument["dist-tags"] ?? {};
|
|
910
1166
|
const latest = tags["latest"];
|
|
911
|
-
if (requested !== null &&
|
|
1167
|
+
if (requested !== null && versionSet.has(requested)) {
|
|
912
1168
|
return {
|
|
913
1169
|
version: requested,
|
|
914
1170
|
resolution: "exact",
|
|
@@ -917,7 +1173,7 @@ var resolveRequestedVersion = (packument, requested) => {
|
|
|
917
1173
|
}
|
|
918
1174
|
if (requested !== null) {
|
|
919
1175
|
const tagged = tags[requested];
|
|
920
|
-
if (tagged !== void 0 &&
|
|
1176
|
+
if (tagged !== void 0 && versionSet.has(tagged)) {
|
|
921
1177
|
return {
|
|
922
1178
|
version: tagged,
|
|
923
1179
|
resolution: "tag",
|
|
@@ -927,7 +1183,7 @@ var resolveRequestedVersion = (packument, requested) => {
|
|
|
927
1183
|
}
|
|
928
1184
|
if (requested !== null) {
|
|
929
1185
|
const matched = resolveRangeVersion(versionKeys, requested);
|
|
930
|
-
if (matched !== null &&
|
|
1186
|
+
if (matched !== null && versionSet.has(matched)) {
|
|
931
1187
|
return {
|
|
932
1188
|
version: matched,
|
|
933
1189
|
resolution: "range",
|
|
@@ -935,7 +1191,7 @@ var resolveRequestedVersion = (packument, requested) => {
|
|
|
935
1191
|
};
|
|
936
1192
|
}
|
|
937
1193
|
}
|
|
938
|
-
if (latest !== void 0 &&
|
|
1194
|
+
if (latest !== void 0 && versionSet.has(latest)) {
|
|
939
1195
|
return {
|
|
940
1196
|
version: latest,
|
|
941
1197
|
resolution: "latest",
|
|
@@ -1016,8 +1272,8 @@ var resolveRegistryGraphFromDirectSpecs = async (directSpecs, options) => {
|
|
|
1016
1272
|
if (nodesByKey.has(nodeKey)) {
|
|
1017
1273
|
continue;
|
|
1018
1274
|
}
|
|
1019
|
-
const
|
|
1020
|
-
const dependencies = Object.entries(
|
|
1275
|
+
const manifestDependencies = packument.dependenciesByVersion[resolved.version] ?? {};
|
|
1276
|
+
const dependencies = Object.entries(manifestDependencies).filter(
|
|
1021
1277
|
([dependencyName, dependencyRange]) => dependencyName.length > 0 && dependencyRange.length > 0
|
|
1022
1278
|
).sort((a, b) => a[0].localeCompare(b[0]));
|
|
1023
1279
|
nodesByKey.set(nodeKey, {
|
|
@@ -1301,7 +1557,7 @@ var analyzeDependencyCandidate = async (input, metadataProvider) => {
|
|
|
1301
1557
|
external
|
|
1302
1558
|
};
|
|
1303
1559
|
};
|
|
1304
|
-
var
|
|
1560
|
+
var ONE_DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
1305
1561
|
var MAX_RETRIES2 = 3;
|
|
1306
1562
|
var RETRY_BASE_DELAY_MS2 = 500;
|
|
1307
1563
|
var round42 = (value) => Number(value.toFixed(4));
|
|
@@ -1314,12 +1570,18 @@ var parseDate = (iso) => {
|
|
|
1314
1570
|
};
|
|
1315
1571
|
var NpmRegistryMetadataProvider = class {
|
|
1316
1572
|
cache = /* @__PURE__ */ new Map();
|
|
1573
|
+
cacheStore = getNpmMetadataCacheStore();
|
|
1317
1574
|
async fetchWeeklyDownloads(name) {
|
|
1318
1575
|
const encodedName = encodeURIComponent(name);
|
|
1319
|
-
const payload = await
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1576
|
+
const payload = await cachedFetch({
|
|
1577
|
+
key: toWeeklyDownloadsCacheKey(name),
|
|
1578
|
+
ttlMs: getWeeklyDownloadsCacheTtlMs(),
|
|
1579
|
+
cacheStore: this.cacheStore,
|
|
1580
|
+
fetchFresh: async () => await fetchJsonWithRetry(
|
|
1581
|
+
`https://api.npmjs.org/downloads/point/last-week/${encodedName}`,
|
|
1582
|
+
{ retries: MAX_RETRIES2, baseDelayMs: RETRY_BASE_DELAY_MS2 }
|
|
1583
|
+
)
|
|
1584
|
+
});
|
|
1323
1585
|
if (payload === null) {
|
|
1324
1586
|
return null;
|
|
1325
1587
|
}
|
|
@@ -1336,10 +1598,31 @@ var NpmRegistryMetadataProvider = class {
|
|
|
1336
1598
|
}
|
|
1337
1599
|
try {
|
|
1338
1600
|
const encodedName = encodeURIComponent(name);
|
|
1339
|
-
const payload = await
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1601
|
+
const payload = await cachedFetch({
|
|
1602
|
+
key: toMetadataPackumentCacheKey(name),
|
|
1603
|
+
ttlMs: getPackumentCacheTtlMs(),
|
|
1604
|
+
cacheStore: this.cacheStore,
|
|
1605
|
+
fetchFresh: async () => {
|
|
1606
|
+
const fresh = await fetchJsonWithRetry(
|
|
1607
|
+
`https://registry.npmjs.org/${encodedName}`,
|
|
1608
|
+
{
|
|
1609
|
+
retries: MAX_RETRIES2,
|
|
1610
|
+
baseDelayMs: RETRY_BASE_DELAY_MS2
|
|
1611
|
+
}
|
|
1612
|
+
);
|
|
1613
|
+
if (fresh === null) {
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
const slim = {};
|
|
1617
|
+
if (fresh.time !== void 0) {
|
|
1618
|
+
slim.time = fresh.time;
|
|
1619
|
+
}
|
|
1620
|
+
if (fresh.maintainers !== void 0) {
|
|
1621
|
+
slim.maintainers = fresh.maintainers;
|
|
1622
|
+
}
|
|
1623
|
+
return slim;
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1343
1626
|
if (payload === null) {
|
|
1344
1627
|
this.cache.set(key, null);
|
|
1345
1628
|
return null;
|
|
@@ -1348,7 +1631,7 @@ var NpmRegistryMetadataProvider = class {
|
|
|
1348
1631
|
const publishDates = Object.entries(timeEntries).filter(([tag]) => tag !== "created" && tag !== "modified").map(([, date]) => parseDate(date)).filter((value) => value !== null).sort((a, b) => a - b);
|
|
1349
1632
|
const modifiedAt = parseDate(timeEntries["modified"]);
|
|
1350
1633
|
const now = Date.now();
|
|
1351
|
-
const daysSinceLastRelease = modifiedAt === null ? null : Math.max(0, round42((now - modifiedAt) /
|
|
1634
|
+
const daysSinceLastRelease = modifiedAt === null ? null : Math.max(0, round42((now - modifiedAt) / ONE_DAY_MS2));
|
|
1352
1635
|
let releaseFrequencyDays = null;
|
|
1353
1636
|
if (publishDates.length >= 2) {
|
|
1354
1637
|
const totalIntervals = publishDates.length - 1;
|
|
@@ -1360,7 +1643,7 @@ var NpmRegistryMetadataProvider = class {
|
|
|
1360
1643
|
sum += current - previous;
|
|
1361
1644
|
}
|
|
1362
1645
|
}
|
|
1363
|
-
releaseFrequencyDays = round42(sum / totalIntervals /
|
|
1646
|
+
releaseFrequencyDays = round42(sum / totalIntervals / ONE_DAY_MS2);
|
|
1364
1647
|
}
|
|
1365
1648
|
const maintainers = payload.maintainers ?? [];
|
|
1366
1649
|
const maintainerCount = maintainers.length > 0 ? maintainers.length : null;
|
|
@@ -1656,7 +1939,7 @@ var createReport = (snapshot, diff) => {
|
|
|
1656
1939
|
confidence: repositoryConfidence(snapshot),
|
|
1657
1940
|
dimensionScores: repositoryDimensionScores(snapshot)
|
|
1658
1941
|
},
|
|
1659
|
-
|
|
1942
|
+
health: snapshot.analysis.health,
|
|
1660
1943
|
hotspots: hotspotItems(snapshot),
|
|
1661
1944
|
structural: {
|
|
1662
1945
|
cycleCount: snapshot.analysis.structural.metrics.cycleCount,
|
|
@@ -1728,23 +2011,21 @@ var renderTextReport = (report) => {
|
|
|
1728
2011
|
lines.push(` external: ${report.repository.dimensionScores.external ?? "n/a"}`);
|
|
1729
2012
|
lines.push(` interactions: ${report.repository.dimensionScores.interactions ?? "n/a"}`);
|
|
1730
2013
|
lines.push("");
|
|
1731
|
-
lines.push("
|
|
1732
|
-
lines.push(`
|
|
1733
|
-
lines.push(` normalizedScore: ${report.
|
|
1734
|
-
lines.push(` modularity: ${report.
|
|
1735
|
-
lines.push(` changeHygiene: ${report.
|
|
1736
|
-
lines.push(`
|
|
1737
|
-
lines.push(`
|
|
1738
|
-
lines.push(` duplication: ${report.quality.dimensions.duplication}`);
|
|
1739
|
-
lines.push(` testHealth: ${report.quality.dimensions.testHealth}`);
|
|
2014
|
+
lines.push("Health Summary");
|
|
2015
|
+
lines.push(` healthScore: ${report.health.healthScore}`);
|
|
2016
|
+
lines.push(` normalizedScore: ${report.health.normalizedScore}`);
|
|
2017
|
+
lines.push(` modularity: ${report.health.dimensions.modularity}`);
|
|
2018
|
+
lines.push(` changeHygiene: ${report.health.dimensions.changeHygiene}`);
|
|
2019
|
+
lines.push(` testHealth: ${report.health.dimensions.testHealth}`);
|
|
2020
|
+
lines.push(` ownershipDistribution: ${report.health.dimensions.ownershipDistribution}`);
|
|
1740
2021
|
lines.push(" topIssues:");
|
|
1741
|
-
for (const issue of report.
|
|
2022
|
+
for (const issue of report.health.topIssues.slice(0, 5)) {
|
|
1742
2023
|
const ruleSuffix = issue.ruleId === void 0 ? "" : ` [rule=${issue.ruleId}]`;
|
|
1743
2024
|
lines.push(
|
|
1744
2025
|
` - [${issue.severity}] (${issue.dimension}) ${issue.id}${ruleSuffix} @ ${issue.target}: ${issue.message}`
|
|
1745
2026
|
);
|
|
1746
2027
|
}
|
|
1747
|
-
if (report.
|
|
2028
|
+
if (report.health.topIssues.length === 0) {
|
|
1748
2029
|
lines.push(" - none");
|
|
1749
2030
|
}
|
|
1750
2031
|
lines.push("");
|
|
@@ -1823,20 +2104,18 @@ var renderMarkdownReport = (report) => {
|
|
|
1823
2104
|
lines.push(`- external: \`${report.repository.dimensionScores.external ?? "n/a"}\``);
|
|
1824
2105
|
lines.push(`- interactions: \`${report.repository.dimensionScores.interactions ?? "n/a"}\``);
|
|
1825
2106
|
lines.push("");
|
|
1826
|
-
lines.push("##
|
|
1827
|
-
lines.push(`-
|
|
1828
|
-
lines.push(`- normalizedScore: \`${report.
|
|
1829
|
-
lines.push(`- modularity: \`${report.
|
|
1830
|
-
lines.push(`- changeHygiene: \`${report.
|
|
1831
|
-
lines.push(`-
|
|
1832
|
-
lines.push(`-
|
|
1833
|
-
|
|
1834
|
-
lines.push(`- testHealth: \`${report.quality.dimensions.testHealth}\``);
|
|
1835
|
-
if (report.quality.topIssues.length === 0) {
|
|
2107
|
+
lines.push("## Health Summary");
|
|
2108
|
+
lines.push(`- healthScore: \`${report.health.healthScore}\``);
|
|
2109
|
+
lines.push(`- normalizedScore: \`${report.health.normalizedScore}\``);
|
|
2110
|
+
lines.push(`- modularity: \`${report.health.dimensions.modularity}\``);
|
|
2111
|
+
lines.push(`- changeHygiene: \`${report.health.dimensions.changeHygiene}\``);
|
|
2112
|
+
lines.push(`- testHealth: \`${report.health.dimensions.testHealth}\``);
|
|
2113
|
+
lines.push(`- ownershipDistribution: \`${report.health.dimensions.ownershipDistribution}\``);
|
|
2114
|
+
if (report.health.topIssues.length === 0) {
|
|
1836
2115
|
lines.push("- top issues: none");
|
|
1837
2116
|
} else {
|
|
1838
2117
|
lines.push("- top issues:");
|
|
1839
|
-
for (const issue of report.
|
|
2118
|
+
for (const issue of report.health.topIssues.slice(0, 5)) {
|
|
1840
2119
|
const ruleSuffix = issue.ruleId === void 0 ? "" : ` [rule=${issue.ruleId}]`;
|
|
1841
2120
|
lines.push(
|
|
1842
2121
|
` - [${issue.severity}] \`${issue.id}\`${ruleSuffix} (\`${issue.dimension}\`) @ \`${issue.target}\`: ${issue.message}`
|
|
@@ -1929,7 +2208,7 @@ var formatReport = (report, format) => {
|
|
|
1929
2208
|
|
|
1930
2209
|
// ../governance/dist/index.js
|
|
1931
2210
|
import { mkdirSync, rmSync } from "fs";
|
|
1932
|
-
import { basename, join as
|
|
2211
|
+
import { basename, join as join5, resolve } from "path";
|
|
1933
2212
|
import { execFile } from "child_process";
|
|
1934
2213
|
import { promisify } from "util";
|
|
1935
2214
|
var EXIT_CODES = {
|
|
@@ -1992,8 +2271,8 @@ var validateGateConfig = (input) => {
|
|
|
1992
2271
|
if (config.maxRiskDelta !== void 0 && (!Number.isFinite(config.maxRiskDelta) || config.maxRiskDelta < 0)) {
|
|
1993
2272
|
throw new GovernanceConfigurationError("max-risk-delta must be a finite number >= 0");
|
|
1994
2273
|
}
|
|
1995
|
-
if (config.
|
|
1996
|
-
throw new GovernanceConfigurationError("max-
|
|
2274
|
+
if (config.maxHealthDelta !== void 0 && (!Number.isFinite(config.maxHealthDelta) || config.maxHealthDelta < 0)) {
|
|
2275
|
+
throw new GovernanceConfigurationError("max-health-delta must be a finite number >= 0");
|
|
1997
2276
|
}
|
|
1998
2277
|
if (config.maxNewHotspots !== void 0 && (!Number.isInteger(config.maxNewHotspots) || config.maxNewHotspots < 0)) {
|
|
1999
2278
|
throw new GovernanceConfigurationError("max-new-hotspots must be an integer >= 0");
|
|
@@ -2001,8 +2280,8 @@ var validateGateConfig = (input) => {
|
|
|
2001
2280
|
if (config.maxRiskScore !== void 0 && (!Number.isFinite(config.maxRiskScore) || config.maxRiskScore < 0 || config.maxRiskScore > 100)) {
|
|
2002
2281
|
throw new GovernanceConfigurationError("max-risk-score must be a number in [0, 100]");
|
|
2003
2282
|
}
|
|
2004
|
-
if (config.
|
|
2005
|
-
throw new GovernanceConfigurationError("min-
|
|
2283
|
+
if (config.minHealthScore !== void 0 && (!Number.isFinite(config.minHealthScore) || config.minHealthScore < 0 || config.minHealthScore > 100)) {
|
|
2284
|
+
throw new GovernanceConfigurationError("min-health-score must be a number in [0, 100]");
|
|
2006
2285
|
}
|
|
2007
2286
|
if (config.newHotspotScoreThreshold !== void 0 && (!Number.isFinite(config.newHotspotScoreThreshold) || config.newHotspotScoreThreshold < 0 || config.newHotspotScoreThreshold > 100)) {
|
|
2008
2287
|
throw new GovernanceConfigurationError(
|
|
@@ -2030,17 +2309,17 @@ var evaluateGates = (input) => {
|
|
|
2030
2309
|
);
|
|
2031
2310
|
}
|
|
2032
2311
|
}
|
|
2033
|
-
if (config.
|
|
2034
|
-
evaluatedGates.push("min-
|
|
2035
|
-
const current = input.current.analysis.
|
|
2036
|
-
if (current < config.
|
|
2312
|
+
if (config.minHealthScore !== void 0) {
|
|
2313
|
+
evaluatedGates.push("min-health-score");
|
|
2314
|
+
const current = input.current.analysis.health.healthScore;
|
|
2315
|
+
if (current < config.minHealthScore) {
|
|
2037
2316
|
violations.push(
|
|
2038
2317
|
makeViolation(
|
|
2039
|
-
"min-
|
|
2318
|
+
"min-health-score",
|
|
2040
2319
|
"error",
|
|
2041
|
-
`
|
|
2320
|
+
`Health score ${current} is below configured minimum ${config.minHealthScore}.`,
|
|
2042
2321
|
[input.current.analysis.structural.targetPath],
|
|
2043
|
-
[{ kind: "repository_metric", metric: "
|
|
2322
|
+
[{ kind: "repository_metric", metric: "healthScore" }]
|
|
2044
2323
|
)
|
|
2045
2324
|
);
|
|
2046
2325
|
}
|
|
@@ -2065,22 +2344,22 @@ var evaluateGates = (input) => {
|
|
|
2065
2344
|
);
|
|
2066
2345
|
}
|
|
2067
2346
|
}
|
|
2068
|
-
if (config.
|
|
2069
|
-
evaluatedGates.push("max-
|
|
2070
|
-
requireDiff(input, "max-
|
|
2347
|
+
if (config.maxHealthDelta !== void 0) {
|
|
2348
|
+
evaluatedGates.push("max-health-delta");
|
|
2349
|
+
requireDiff(input, "max-health-delta");
|
|
2071
2350
|
const baseline = input.baseline;
|
|
2072
2351
|
if (baseline === void 0) {
|
|
2073
|
-
throw new GovernanceConfigurationError("max-
|
|
2352
|
+
throw new GovernanceConfigurationError("max-health-delta requires baseline snapshot");
|
|
2074
2353
|
}
|
|
2075
|
-
const delta = input.current.analysis.
|
|
2076
|
-
if (delta < -config.
|
|
2354
|
+
const delta = input.current.analysis.health.normalizedScore - baseline.analysis.health.normalizedScore;
|
|
2355
|
+
if (delta < -config.maxHealthDelta) {
|
|
2077
2356
|
violations.push(
|
|
2078
2357
|
makeViolation(
|
|
2079
|
-
"max-
|
|
2358
|
+
"max-health-delta",
|
|
2080
2359
|
"error",
|
|
2081
|
-
`
|
|
2360
|
+
`Health normalized score delta ${delta.toFixed(4)} is below allowed minimum ${(-config.maxHealthDelta).toFixed(4)}.`,
|
|
2082
2361
|
[input.current.analysis.structural.targetPath],
|
|
2083
|
-
[{ kind: "repository_metric", metric: "
|
|
2362
|
+
[{ kind: "repository_metric", metric: "healthNormalizedScore" }]
|
|
2084
2363
|
)
|
|
2085
2364
|
);
|
|
2086
2365
|
}
|
|
@@ -2450,7 +2729,7 @@ var tryRunGit = async (repositoryPath, args) => {
|
|
|
2450
2729
|
}
|
|
2451
2730
|
};
|
|
2452
2731
|
var buildWorktreePath = (repoRoot, sha) => {
|
|
2453
|
-
const tmpRoot =
|
|
2732
|
+
const tmpRoot = join5(repoRoot, SENTINEL_TMP_DIR, WORKTREE_DIR);
|
|
2454
2733
|
mkdirSync(tmpRoot, { recursive: true });
|
|
2455
2734
|
const baseName = `baseline-${sha.slice(0, 12)}-${process.pid}`;
|
|
2456
2735
|
const candidate = resolve(tmpRoot, baseName);
|
|
@@ -2542,8 +2821,8 @@ var resolveAutoBaselineRef = async (input) => {
|
|
|
2542
2821
|
|
|
2543
2822
|
// src/index.ts
|
|
2544
2823
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2545
|
-
import { readFile as readFile6, writeFile as
|
|
2546
|
-
import { dirname as dirname2, resolve as
|
|
2824
|
+
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
2825
|
+
import { dirname as dirname2, resolve as resolve5 } from "path";
|
|
2547
2826
|
import { fileURLToPath } from "url";
|
|
2548
2827
|
|
|
2549
2828
|
// src/application/format-analyze-output.ts
|
|
@@ -2584,11 +2863,11 @@ var createSummaryShape = (summary) => ({
|
|
|
2584
2863
|
fragileClusterCount: summary.risk.fragileClusters.length,
|
|
2585
2864
|
dependencyAmplificationZoneCount: summary.risk.dependencyAmplificationZones.length
|
|
2586
2865
|
},
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
normalizedScore: summary.
|
|
2590
|
-
dimensions: summary.
|
|
2591
|
-
topIssues: summary.
|
|
2866
|
+
health: {
|
|
2867
|
+
healthScore: summary.health.healthScore,
|
|
2868
|
+
normalizedScore: summary.health.normalizedScore,
|
|
2869
|
+
dimensions: summary.health.dimensions,
|
|
2870
|
+
topIssues: summary.health.topIssues.slice(0, 5)
|
|
2592
2871
|
}
|
|
2593
2872
|
});
|
|
2594
2873
|
var formatAnalyzeOutput = (summary, mode) => mode === "json" ? JSON.stringify(summary, null, 2) : JSON.stringify(createSummaryShape(summary), null, 2);
|
|
@@ -2935,13 +3214,13 @@ var parseLogLevel = (value) => {
|
|
|
2935
3214
|
|
|
2936
3215
|
// src/application/check-for-updates.ts
|
|
2937
3216
|
import { spawn } from "child_process";
|
|
2938
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2939
|
-
import { homedir } from "os";
|
|
2940
|
-
import { dirname, join as
|
|
3217
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
3218
|
+
import { homedir as homedir2 } from "os";
|
|
3219
|
+
import { dirname, join as join6 } from "path";
|
|
2941
3220
|
import { stderr, stdin } from "process";
|
|
2942
3221
|
import { clearScreenDown, cursorTo, emitKeypressEvents } from "readline";
|
|
2943
3222
|
var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2944
|
-
var UPDATE_CACHE_PATH =
|
|
3223
|
+
var UPDATE_CACHE_PATH = join6(homedir2(), ".cache", "codesentinel", "update-check.json");
|
|
2945
3224
|
var SEMVER_PATTERN = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
2946
3225
|
var ANSI = {
|
|
2947
3226
|
reset: "\x1B[0m",
|
|
@@ -3065,7 +3344,7 @@ var parseNpmViewVersionOutput = (output) => {
|
|
|
3065
3344
|
};
|
|
3066
3345
|
var readCache = async () => {
|
|
3067
3346
|
try {
|
|
3068
|
-
const raw = await
|
|
3347
|
+
const raw = await readFile2(UPDATE_CACHE_PATH, "utf8");
|
|
3069
3348
|
const parsed = JSON.parse(raw);
|
|
3070
3349
|
if (typeof parsed === "object" && parsed !== null && typeof parsed.lastCheckedAt === "string") {
|
|
3071
3350
|
return { lastCheckedAt: parsed.lastCheckedAt };
|
|
@@ -3076,8 +3355,8 @@ var readCache = async () => {
|
|
|
3076
3355
|
return null;
|
|
3077
3356
|
};
|
|
3078
3357
|
var writeCache = async (cache) => {
|
|
3079
|
-
await
|
|
3080
|
-
await
|
|
3358
|
+
await mkdir2(dirname(UPDATE_CACHE_PATH), { recursive: true });
|
|
3359
|
+
await writeFile2(UPDATE_CACHE_PATH, JSON.stringify(cache), "utf8");
|
|
3081
3360
|
};
|
|
3082
3361
|
var shouldRunUpdateCheck = (input) => {
|
|
3083
3362
|
if (!input.isInteractive) {
|
|
@@ -3161,7 +3440,7 @@ var promptInstall = async (packageName, latestVersion, currentVersion) => {
|
|
|
3161
3440
|
);
|
|
3162
3441
|
return "skip";
|
|
3163
3442
|
}
|
|
3164
|
-
return await new Promise((
|
|
3443
|
+
return await new Promise((resolve6) => {
|
|
3165
3444
|
emitKeypressEvents(stdin);
|
|
3166
3445
|
let selectedIndex = 0;
|
|
3167
3446
|
const previousRawMode = stdin.isRaw;
|
|
@@ -3186,7 +3465,7 @@ var promptInstall = async (packageName, latestVersion, currentVersion) => {
|
|
|
3186
3465
|
} else {
|
|
3187
3466
|
stderr.write("\n");
|
|
3188
3467
|
}
|
|
3189
|
-
|
|
3468
|
+
resolve6(choice);
|
|
3190
3469
|
};
|
|
3191
3470
|
const onKeypress = (_str, key) => {
|
|
3192
3471
|
if (key.ctrl === true && key.name === "c") {
|
|
@@ -3265,7 +3544,7 @@ var checkForCliUpdates = async (input) => {
|
|
|
3265
3544
|
};
|
|
3266
3545
|
|
|
3267
3546
|
// src/application/run-analyze-command.ts
|
|
3268
|
-
import { resolve as
|
|
3547
|
+
import { resolve as resolve3 } from "path";
|
|
3269
3548
|
|
|
3270
3549
|
// ../code-graph/dist/index.js
|
|
3271
3550
|
import { extname, isAbsolute, relative, resolve as resolve2 } from "path";
|
|
@@ -4313,618 +4592,121 @@ var analyzeRepositoryEvolutionFromGit = (input, onProgress) => {
|
|
|
4313
4592
|
return analyzeRepositoryEvolution(input, historyProvider, onProgress);
|
|
4314
4593
|
};
|
|
4315
4594
|
|
|
4316
|
-
// ../
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
import { ESLint } from "eslint";
|
|
4321
|
-
import * as ts2 from "typescript";
|
|
4322
|
-
import * as ts3 from "typescript";
|
|
4323
|
-
var markerRegex = /\b(?:TODO|FIXME)\b/gi;
|
|
4324
|
-
var countMarkers = (text) => text.match(markerRegex)?.length ?? 0;
|
|
4325
|
-
var countTodoFixmeInComments = (content) => {
|
|
4326
|
-
const scanner = ts3.createScanner(
|
|
4327
|
-
ts3.ScriptTarget.Latest,
|
|
4328
|
-
false,
|
|
4329
|
-
ts3.LanguageVariant.Standard,
|
|
4330
|
-
content
|
|
4331
|
-
);
|
|
4332
|
-
let total = 0;
|
|
4333
|
-
let token = scanner.scan();
|
|
4334
|
-
while (token !== ts3.SyntaxKind.EndOfFileToken) {
|
|
4335
|
-
if (token === ts3.SyntaxKind.SingleLineCommentTrivia || token === ts3.SyntaxKind.MultiLineCommentTrivia) {
|
|
4336
|
-
total += countMarkers(scanner.getTokenText());
|
|
4337
|
-
}
|
|
4338
|
-
token = scanner.scan();
|
|
4339
|
-
}
|
|
4340
|
-
return total;
|
|
4341
|
-
};
|
|
4342
|
-
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
4343
|
-
var normalizePath2 = (value) => value.replaceAll("\\", "/");
|
|
4344
|
-
var isTestPath = (path) => {
|
|
4345
|
-
const normalized = normalizePath2(path);
|
|
4346
|
-
return normalized.includes("/__tests__/") || normalized.includes("\\__tests__\\") || normalized.includes(".test.") || normalized.includes(".spec.");
|
|
4347
|
-
};
|
|
4348
|
-
var collectTodoFixmeCommentCount = async (targetPath, structural) => {
|
|
4349
|
-
const filePaths2 = [...structural.files].map((file) => file.relativePath).sort((a, b) => a.localeCompare(b));
|
|
4350
|
-
let total = 0;
|
|
4351
|
-
for (const relativePath of filePaths2) {
|
|
4352
|
-
try {
|
|
4353
|
-
const content = await readFile2(join4(targetPath, relativePath), "utf8");
|
|
4354
|
-
total += countTodoFixmeInComments(content);
|
|
4355
|
-
} catch {
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
return total;
|
|
4359
|
-
};
|
|
4360
|
-
var collectEslintSignals = async (targetPath, structural, logger) => {
|
|
4361
|
-
const absoluteFiles = structural.files.map((file) => join4(targetPath, file.relativePath));
|
|
4362
|
-
if (absoluteFiles.length === 0) {
|
|
4363
|
-
return {
|
|
4364
|
-
errorCount: 0,
|
|
4365
|
-
warningCount: 0,
|
|
4366
|
-
filesWithIssues: 0,
|
|
4367
|
-
ruleCounts: []
|
|
4368
|
-
};
|
|
4369
|
-
}
|
|
4370
|
-
try {
|
|
4371
|
-
const eslint = new ESLint({ cwd: targetPath, errorOnUnmatchedPattern: false });
|
|
4372
|
-
const results = await eslint.lintFiles(absoluteFiles);
|
|
4373
|
-
let errorCount = 0;
|
|
4374
|
-
let warningCount = 0;
|
|
4375
|
-
let filesWithIssues = 0;
|
|
4376
|
-
const ruleCounts = /* @__PURE__ */ new Map();
|
|
4377
|
-
for (const result of results) {
|
|
4378
|
-
if (result.errorCount + result.warningCount > 0) {
|
|
4379
|
-
filesWithIssues += 1;
|
|
4380
|
-
}
|
|
4381
|
-
errorCount += result.errorCount;
|
|
4382
|
-
warningCount += result.warningCount;
|
|
4383
|
-
for (const message of result.messages) {
|
|
4384
|
-
if (message.ruleId === null) {
|
|
4385
|
-
continue;
|
|
4386
|
-
}
|
|
4387
|
-
const severity = message.severity >= 2 ? "error" : "warn";
|
|
4388
|
-
const current = ruleCounts.get(message.ruleId);
|
|
4389
|
-
if (current === void 0) {
|
|
4390
|
-
ruleCounts.set(message.ruleId, {
|
|
4391
|
-
ruleId: message.ruleId,
|
|
4392
|
-
severity,
|
|
4393
|
-
count: 1
|
|
4394
|
-
});
|
|
4395
|
-
} else {
|
|
4396
|
-
ruleCounts.set(message.ruleId, {
|
|
4397
|
-
ruleId: current.ruleId,
|
|
4398
|
-
severity: current.severity === "error" || severity === "error" ? "error" : "warn",
|
|
4399
|
-
count: current.count + 1
|
|
4400
|
-
});
|
|
4401
|
-
}
|
|
4402
|
-
}
|
|
4403
|
-
}
|
|
4404
|
-
return {
|
|
4405
|
-
errorCount,
|
|
4406
|
-
warningCount,
|
|
4407
|
-
filesWithIssues,
|
|
4408
|
-
ruleCounts: [...ruleCounts.values()].sort(
|
|
4409
|
-
(a, b) => b.count - a.count || a.ruleId.localeCompare(b.ruleId)
|
|
4410
|
-
)
|
|
4411
|
-
};
|
|
4412
|
-
} catch (error) {
|
|
4413
|
-
logger.warn(
|
|
4414
|
-
`quality signals: eslint collection unavailable (${error instanceof Error ? error.message : "unknown error"})`
|
|
4415
|
-
);
|
|
4416
|
-
return void 0;
|
|
4417
|
-
}
|
|
4418
|
-
};
|
|
4419
|
-
var collectTypeScriptSignals = (targetPath, logger) => {
|
|
4420
|
-
const tsconfigPath = ts2.findConfigFile(targetPath, ts2.sys.fileExists, "tsconfig.json");
|
|
4421
|
-
if (tsconfigPath === void 0) {
|
|
4422
|
-
return void 0;
|
|
4423
|
-
}
|
|
4424
|
-
try {
|
|
4425
|
-
const parsed = ts2.getParsedCommandLineOfConfigFile(
|
|
4426
|
-
tsconfigPath,
|
|
4427
|
-
{},
|
|
4428
|
-
{
|
|
4429
|
-
...ts2.sys,
|
|
4430
|
-
onUnRecoverableConfigFileDiagnostic: () => {
|
|
4431
|
-
throw new Error(`failed to parse ${tsconfigPath}`);
|
|
4432
|
-
}
|
|
4433
|
-
}
|
|
4434
|
-
);
|
|
4435
|
-
if (parsed === void 0) {
|
|
4436
|
-
return void 0;
|
|
4437
|
-
}
|
|
4438
|
-
const program2 = ts2.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
|
|
4439
|
-
const diagnostics = [
|
|
4440
|
-
...program2.getOptionsDiagnostics(),
|
|
4441
|
-
...program2.getGlobalDiagnostics(),
|
|
4442
|
-
...program2.getSyntacticDiagnostics(),
|
|
4443
|
-
...program2.getSemanticDiagnostics()
|
|
4444
|
-
];
|
|
4445
|
-
let errorCount = 0;
|
|
4446
|
-
let warningCount = 0;
|
|
4447
|
-
const fileSet = /* @__PURE__ */ new Set();
|
|
4448
|
-
for (const diagnostic of diagnostics) {
|
|
4449
|
-
if (diagnostic.category === ts2.DiagnosticCategory.Error) {
|
|
4450
|
-
errorCount += 1;
|
|
4451
|
-
} else if (diagnostic.category === ts2.DiagnosticCategory.Warning) {
|
|
4452
|
-
warningCount += 1;
|
|
4453
|
-
}
|
|
4454
|
-
if (diagnostic.file !== void 0) {
|
|
4455
|
-
const path = normalizePath2(relative2(targetPath, diagnostic.file.fileName));
|
|
4456
|
-
fileSet.add(path);
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
return {
|
|
4460
|
-
errorCount,
|
|
4461
|
-
warningCount,
|
|
4462
|
-
filesWithDiagnostics: fileSet.size
|
|
4463
|
-
};
|
|
4464
|
-
} catch (error) {
|
|
4465
|
-
logger.warn(
|
|
4466
|
-
`quality signals: typescript diagnostic collection unavailable (${error instanceof Error ? error.message : "unknown error"})`
|
|
4467
|
-
);
|
|
4468
|
-
return void 0;
|
|
4595
|
+
// ../health-engine/dist/index.js
|
|
4596
|
+
var clamp01 = (value) => {
|
|
4597
|
+
if (!Number.isFinite(value)) {
|
|
4598
|
+
return 0;
|
|
4469
4599
|
}
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
if (ts2.isIfStatement(node) || ts2.isForStatement(node) || ts2.isForInStatement(node) || ts2.isForOfStatement(node) || ts2.isWhileStatement(node) || ts2.isDoStatement(node) || ts2.isCatchClause(node) || ts2.isConditionalExpression(node)) {
|
|
4473
|
-
return 1;
|
|
4600
|
+
if (value <= 0) {
|
|
4601
|
+
return 0;
|
|
4474
4602
|
}
|
|
4475
|
-
if (
|
|
4603
|
+
if (value >= 1) {
|
|
4476
4604
|
return 1;
|
|
4477
4605
|
}
|
|
4478
|
-
|
|
4479
|
-
if (node.operatorToken.kind === ts2.SyntaxKind.AmpersandAmpersandToken || node.operatorToken.kind === ts2.SyntaxKind.BarBarToken || node.operatorToken.kind === ts2.SyntaxKind.QuestionQuestionToken) {
|
|
4480
|
-
return 1;
|
|
4481
|
-
}
|
|
4482
|
-
}
|
|
4483
|
-
return 0;
|
|
4484
|
-
};
|
|
4485
|
-
var computeCyclomaticComplexity = (node) => {
|
|
4486
|
-
let complexity = 1;
|
|
4487
|
-
const visit = (current) => {
|
|
4488
|
-
complexity += cyclomaticIncrement(current);
|
|
4489
|
-
if (current !== node && (ts2.isFunctionLike(current) || ts2.isArrowFunction(current) || ts2.isMethodDeclaration(current) || ts2.isConstructorDeclaration(current))) {
|
|
4490
|
-
return;
|
|
4491
|
-
}
|
|
4492
|
-
ts2.forEachChild(current, visit);
|
|
4493
|
-
};
|
|
4494
|
-
visit(node);
|
|
4495
|
-
return complexity;
|
|
4606
|
+
return value;
|
|
4496
4607
|
};
|
|
4497
|
-
var
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
if (ts2.isFunctionDeclaration(node) || ts2.isMethodDeclaration(node) || ts2.isFunctionExpression(node) || ts2.isArrowFunction(node) || ts2.isConstructorDeclaration(node) || ts2.isGetAccessorDeclaration(node) || ts2.isSetAccessorDeclaration(node)) {
|
|
4502
|
-
complexities.push(computeCyclomaticComplexity(node));
|
|
4503
|
-
}
|
|
4504
|
-
ts2.forEachChild(node, visit);
|
|
4505
|
-
};
|
|
4506
|
-
visit(sourceFile);
|
|
4507
|
-
if (complexities.length === 0) {
|
|
4508
|
-
return [computeCyclomaticComplexity(sourceFile)];
|
|
4608
|
+
var round45 = (value) => Number(value.toFixed(4));
|
|
4609
|
+
var average = (values) => {
|
|
4610
|
+
if (values.length === 0) {
|
|
4611
|
+
return 0;
|
|
4509
4612
|
}
|
|
4510
|
-
|
|
4613
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
4614
|
+
return total / values.length;
|
|
4511
4615
|
};
|
|
4512
|
-
var
|
|
4513
|
-
const
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
continue;
|
|
4518
|
-
}
|
|
4519
|
-
try {
|
|
4520
|
-
const content = await readFile2(join4(targetPath, file.relativePath), "utf8");
|
|
4521
|
-
complexities.push(...collectFunctionComplexities(content, file.relativePath));
|
|
4522
|
-
} catch {
|
|
4523
|
-
}
|
|
4616
|
+
var concentration = (rawValues) => {
|
|
4617
|
+
const values = rawValues.filter((value) => value > 0);
|
|
4618
|
+
const count = values.length;
|
|
4619
|
+
if (count <= 1) {
|
|
4620
|
+
return 0;
|
|
4524
4621
|
}
|
|
4525
|
-
|
|
4526
|
-
|
|
4622
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
4623
|
+
if (total <= 0) {
|
|
4624
|
+
return 0;
|
|
4527
4625
|
}
|
|
4528
|
-
const
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
analyzedFileCount: complexities.length
|
|
4536
|
-
};
|
|
4626
|
+
const hhi = values.reduce((sum, value) => {
|
|
4627
|
+
const share = value / total;
|
|
4628
|
+
return sum + share * share;
|
|
4629
|
+
}, 0);
|
|
4630
|
+
const minHhi = 1 / count;
|
|
4631
|
+
const normalized = (hhi - minHhi) / (1 - minHhi);
|
|
4632
|
+
return clamp01(normalized);
|
|
4537
4633
|
};
|
|
4538
|
-
var
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
var
|
|
4545
|
-
var
|
|
4546
|
-
var
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
let value = 1;
|
|
4556
|
-
for (let index = 1; index < kgramSize; index += 1) {
|
|
4557
|
-
value = Math.imul(value, HASH_BASE) >>> 0;
|
|
4634
|
+
var DIMENSION_WEIGHTS = {
|
|
4635
|
+
modularity: 0.35,
|
|
4636
|
+
changeHygiene: 0.3,
|
|
4637
|
+
testHealth: 0.2,
|
|
4638
|
+
ownershipDistribution: 0.15
|
|
4639
|
+
};
|
|
4640
|
+
var HEALTH_TRACE_VERSION = "1";
|
|
4641
|
+
var toPercentage = (normalizedHealth) => round45(clamp01(normalizedHealth) * 100);
|
|
4642
|
+
var dampenForSmallSamples = (penalty, sampleSize, warmupSize, minimumWeight = 0.35) => {
|
|
4643
|
+
const reliability = clamp01(sampleSize / Math.max(1, warmupSize));
|
|
4644
|
+
const dampeningWeight = minimumWeight + (1 - minimumWeight) * reliability;
|
|
4645
|
+
return clamp01(penalty) * dampeningWeight;
|
|
4646
|
+
};
|
|
4647
|
+
var topPercentShare = (values, fraction) => {
|
|
4648
|
+
const positive = values.filter((value) => value > 0).sort((a, b) => b - a);
|
|
4649
|
+
if (positive.length === 0) {
|
|
4650
|
+
return 0;
|
|
4558
4651
|
}
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
const tokens = [];
|
|
4565
|
-
let token = scanner.scan();
|
|
4566
|
-
while (token !== ts2.SyntaxKind.EndOfFileToken) {
|
|
4567
|
-
if (token !== ts2.SyntaxKind.WhitespaceTrivia && token !== ts2.SyntaxKind.NewLineTrivia && token !== ts2.SyntaxKind.SingleLineCommentTrivia && token !== ts2.SyntaxKind.MultiLineCommentTrivia) {
|
|
4568
|
-
if (token === ts2.SyntaxKind.Identifier || token === ts2.SyntaxKind.PrivateIdentifier) {
|
|
4569
|
-
tokens.push("id");
|
|
4570
|
-
} else if (token === ts2.SyntaxKind.StringLiteral || token === ts2.SyntaxKind.NoSubstitutionTemplateLiteral || token === ts2.SyntaxKind.TemplateHead || token === ts2.SyntaxKind.TemplateMiddle || token === ts2.SyntaxKind.TemplateTail || token === ts2.SyntaxKind.NumericLiteral || token === ts2.SyntaxKind.BigIntLiteral || token === ts2.SyntaxKind.RegularExpressionLiteral) {
|
|
4571
|
-
tokens.push("lit");
|
|
4572
|
-
} else {
|
|
4573
|
-
const stable = ts2.tokenToString(token) ?? ts2.SyntaxKind[token] ?? `${token}`;
|
|
4574
|
-
tokens.push(stable);
|
|
4575
|
-
}
|
|
4576
|
-
}
|
|
4577
|
-
token = scanner.scan();
|
|
4652
|
+
const topCount = Math.max(1, Math.ceil(positive.length * clamp01(fraction)));
|
|
4653
|
+
const total = positive.reduce((sum, value) => sum + value, 0);
|
|
4654
|
+
const topTotal = positive.slice(0, topCount).reduce((sum, value) => sum + value, 0);
|
|
4655
|
+
if (total <= 0) {
|
|
4656
|
+
return 0;
|
|
4578
4657
|
}
|
|
4579
|
-
return
|
|
4658
|
+
return clamp01(topTotal / total);
|
|
4580
4659
|
};
|
|
4581
|
-
var
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
const fingerprints = [];
|
|
4586
|
-
const removePower = computeRollingBasePower(kgramSize);
|
|
4587
|
-
let hash = 0;
|
|
4588
|
-
for (let index = 0; index < kgramSize; index += 1) {
|
|
4589
|
-
hash = Math.imul(hash, HASH_BASE) + (tokenValues[index] ?? 0) >>> 0;
|
|
4590
|
-
}
|
|
4591
|
-
fingerprints.push({ hash, start: 0 });
|
|
4592
|
-
for (let start = 1; start <= tokenValues.length - kgramSize; start += 1) {
|
|
4593
|
-
const removed = tokenValues[start - 1] ?? 0;
|
|
4594
|
-
const added = tokenValues[start + kgramSize - 1] ?? 0;
|
|
4595
|
-
const removedContribution = Math.imul(removed, removePower) >>> 0;
|
|
4596
|
-
const shifted = Math.imul(hash - removedContribution >>> 0, HASH_BASE) >>> 0;
|
|
4597
|
-
hash = shifted + added >>> 0;
|
|
4598
|
-
fingerprints.push({ hash, start });
|
|
4599
|
-
}
|
|
4600
|
-
return fingerprints;
|
|
4601
|
-
};
|
|
4602
|
-
var winnowFingerprints = (kgrams, windowSize) => {
|
|
4603
|
-
if (kgrams.length === 0) {
|
|
4604
|
-
return [];
|
|
4605
|
-
}
|
|
4606
|
-
if (kgrams.length <= windowSize) {
|
|
4607
|
-
const minimum = [...kgrams].sort(
|
|
4608
|
-
(left, right) => left.hash - right.hash || right.start - left.start
|
|
4609
|
-
)[0];
|
|
4610
|
-
return minimum === void 0 ? [] : [minimum];
|
|
4660
|
+
var normalizedEntropy = (weights) => {
|
|
4661
|
+
const positive = weights.filter((value) => value > 0);
|
|
4662
|
+
if (positive.length <= 1) {
|
|
4663
|
+
return 0;
|
|
4611
4664
|
}
|
|
4612
|
-
const
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
if (best === void 0) {
|
|
4616
|
-
continue;
|
|
4617
|
-
}
|
|
4618
|
-
for (let offset = 1; offset < windowSize; offset += 1) {
|
|
4619
|
-
const candidate = kgrams[start + offset];
|
|
4620
|
-
if (candidate === void 0) {
|
|
4621
|
-
continue;
|
|
4622
|
-
}
|
|
4623
|
-
if (candidate.hash < best.hash || candidate.hash === best.hash && candidate.start > best.start) {
|
|
4624
|
-
best = candidate;
|
|
4625
|
-
}
|
|
4626
|
-
}
|
|
4627
|
-
selected.set(`${best.hash}:${best.start}`, best);
|
|
4665
|
+
const total = positive.reduce((sum, value) => sum + value, 0);
|
|
4666
|
+
if (total <= 0) {
|
|
4667
|
+
return 0;
|
|
4628
4668
|
}
|
|
4629
|
-
|
|
4669
|
+
const entropy = positive.reduce((sum, value) => {
|
|
4670
|
+
const p = value / total;
|
|
4671
|
+
return sum - p * Math.log(p);
|
|
4672
|
+
}, 0);
|
|
4673
|
+
return clamp01(entropy / Math.log(positive.length));
|
|
4630
4674
|
};
|
|
4631
|
-
var
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4675
|
+
var toFactorTrace = (spec) => ({
|
|
4676
|
+
factorId: spec.factorId,
|
|
4677
|
+
contribution: round45(spec.penalty * spec.weight * 100),
|
|
4678
|
+
penalty: round45(spec.penalty),
|
|
4679
|
+
rawMetrics: spec.rawMetrics,
|
|
4680
|
+
normalizedMetrics: spec.normalizedMetrics,
|
|
4681
|
+
weight: round45(spec.weight),
|
|
4682
|
+
evidence: spec.evidence
|
|
4683
|
+
});
|
|
4684
|
+
var createDimensionTrace = (dimension, health, factors) => ({
|
|
4685
|
+
dimension,
|
|
4686
|
+
normalizedScore: round45(clamp01(health)),
|
|
4687
|
+
score: toPercentage(health),
|
|
4688
|
+
factors: factors.map((factor) => toFactorTrace(factor))
|
|
4689
|
+
});
|
|
4690
|
+
var filePaths = (structural) => structural.files.map((file) => file.relativePath);
|
|
4691
|
+
var normalizePath2 = (value) => value.replaceAll("\\", "/").toLowerCase();
|
|
4692
|
+
var isTestPath = (path) => {
|
|
4693
|
+
const normalized = normalizePath2(path);
|
|
4694
|
+
return normalized.includes("/__tests__/") || normalized.includes("/tests/") || normalized.includes("/test/") || normalized.includes(".test.") || normalized.includes(".spec.");
|
|
4644
4695
|
};
|
|
4645
|
-
var
|
|
4646
|
-
if (
|
|
4647
|
-
return
|
|
4696
|
+
var isSourcePath = (path) => {
|
|
4697
|
+
if (path.endsWith(".d.ts")) {
|
|
4698
|
+
return false;
|
|
4648
4699
|
}
|
|
4649
|
-
return
|
|
4700
|
+
return !isTestPath(path);
|
|
4650
4701
|
};
|
|
4651
|
-
var
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
);
|
|
4658
|
-
|
|
4659
|
-
for (const range of sorted) {
|
|
4660
|
-
const previous = merged[merged.length - 1];
|
|
4661
|
-
if (previous === void 0 || range.start > previous.end) {
|
|
4662
|
-
merged.push({ ...range });
|
|
4663
|
-
continue;
|
|
4664
|
-
}
|
|
4665
|
-
previous.end = Math.max(previous.end, range.end);
|
|
4666
|
-
}
|
|
4667
|
-
return merged;
|
|
4668
|
-
};
|
|
4669
|
-
var aggregateDuplicationFromSignatures = (signatures, fileByPath) => {
|
|
4670
|
-
let duplicatedBlockCount = 0;
|
|
4671
|
-
const duplicatedRanges = /* @__PURE__ */ new Map();
|
|
4672
|
-
for (const entries of signatures.values()) {
|
|
4673
|
-
if (entries.length <= 1) {
|
|
4674
|
-
continue;
|
|
4675
|
-
}
|
|
4676
|
-
const uniqueEntries = /* @__PURE__ */ new Map();
|
|
4677
|
-
for (const entry of entries) {
|
|
4678
|
-
uniqueEntries.set(`${entry.file}:${entry.start}`, entry);
|
|
4679
|
-
}
|
|
4680
|
-
if (uniqueEntries.size <= 1) {
|
|
4681
|
-
continue;
|
|
4682
|
-
}
|
|
4683
|
-
duplicatedBlockCount += uniqueEntries.size - 1;
|
|
4684
|
-
for (const entry of uniqueEntries.values()) {
|
|
4685
|
-
const source = fileByPath.get(entry.file);
|
|
4686
|
-
if (source === void 0) {
|
|
4687
|
-
continue;
|
|
4688
|
-
}
|
|
4689
|
-
const signature = tokenBlockSignature(
|
|
4690
|
-
source.tokens,
|
|
4691
|
-
entry.start,
|
|
4692
|
-
DUPLICATION_MIN_BLOCK_TOKENS
|
|
4693
|
-
);
|
|
4694
|
-
if (signature === void 0) {
|
|
4695
|
-
continue;
|
|
4696
|
-
}
|
|
4697
|
-
const ranges = duplicatedRanges.get(entry.file) ?? [];
|
|
4698
|
-
ranges.push({
|
|
4699
|
-
start: entry.start,
|
|
4700
|
-
end: Math.min(source.tokens.length, entry.start + DUPLICATION_MIN_BLOCK_TOKENS)
|
|
4701
|
-
});
|
|
4702
|
-
duplicatedRanges.set(entry.file, ranges);
|
|
4703
|
-
}
|
|
4704
|
-
}
|
|
4705
|
-
let duplicatedTokenCount = 0;
|
|
4706
|
-
for (const ranges of duplicatedRanges.values()) {
|
|
4707
|
-
const mergedRanges = mergeTokenRanges(ranges);
|
|
4708
|
-
duplicatedTokenCount += mergedRanges.reduce((sum, range) => sum + (range.end - range.start), 0);
|
|
4709
|
-
}
|
|
4710
|
-
return {
|
|
4711
|
-
duplicatedBlockCount,
|
|
4712
|
-
duplicatedTokenCount,
|
|
4713
|
-
filesWithDuplication: duplicatedRanges.size
|
|
4714
|
-
};
|
|
4715
|
-
};
|
|
4716
|
-
var collectExactTokenDuplication = (analyzedFiles) => {
|
|
4717
|
-
const signatures = /* @__PURE__ */ new Map();
|
|
4718
|
-
for (const file of analyzedFiles) {
|
|
4719
|
-
const tokenValues = file.tokens.map((token) => hashString32(token));
|
|
4720
|
-
const windows = buildKgramHashes(tokenValues, DUPLICATION_MIN_BLOCK_TOKENS);
|
|
4721
|
-
for (const window of windows) {
|
|
4722
|
-
const signature = tokenBlockSignature(
|
|
4723
|
-
file.tokens,
|
|
4724
|
-
window.start,
|
|
4725
|
-
DUPLICATION_MIN_BLOCK_TOKENS
|
|
4726
|
-
);
|
|
4727
|
-
if (signature === void 0) {
|
|
4728
|
-
continue;
|
|
4729
|
-
}
|
|
4730
|
-
const entries = signatures.get(signature) ?? [];
|
|
4731
|
-
entries.push({ file: file.file, start: window.start });
|
|
4732
|
-
signatures.set(signature, entries);
|
|
4733
|
-
}
|
|
4734
|
-
}
|
|
4735
|
-
const fileByPath = new Map(analyzedFiles.map((file) => [file.file, file]));
|
|
4736
|
-
return aggregateDuplicationFromSignatures(signatures, fileByPath);
|
|
4737
|
-
};
|
|
4738
|
-
var collectWinnowingDuplication = (analyzedFiles) => {
|
|
4739
|
-
const signatures = /* @__PURE__ */ new Map();
|
|
4740
|
-
for (const file of analyzedFiles) {
|
|
4741
|
-
const tokenValues = file.tokens.map((token) => hashString32(token));
|
|
4742
|
-
const kgrams = buildKgramHashes(tokenValues, DUPLICATION_KGRAM_TOKENS);
|
|
4743
|
-
const fingerprints = capFingerprints(
|
|
4744
|
-
winnowFingerprints(kgrams, DUPLICATION_WINDOW_SIZE),
|
|
4745
|
-
DUPLICATION_MAX_FINGERPRINTS_PER_FILE
|
|
4746
|
-
);
|
|
4747
|
-
for (const fingerprint of fingerprints) {
|
|
4748
|
-
const signature = tokenBlockSignature(
|
|
4749
|
-
file.tokens,
|
|
4750
|
-
fingerprint.start,
|
|
4751
|
-
DUPLICATION_MIN_BLOCK_TOKENS
|
|
4752
|
-
);
|
|
4753
|
-
if (signature === void 0) {
|
|
4754
|
-
continue;
|
|
4755
|
-
}
|
|
4756
|
-
const entries = signatures.get(signature) ?? [];
|
|
4757
|
-
entries.push({ file: file.file, start: fingerprint.start });
|
|
4758
|
-
signatures.set(signature, entries);
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
|
-
const fileByPath = new Map(analyzedFiles.map((file) => [file.file, file]));
|
|
4762
|
-
return aggregateDuplicationFromSignatures(signatures, fileByPath);
|
|
4763
|
-
};
|
|
4764
|
-
var collectDuplicationSignals = async (targetPath, structural) => {
|
|
4765
|
-
const files = [...structural.files].map((file) => file.relativePath).sort((left, right) => left.localeCompare(right)).filter((filePath) => SOURCE_EXTENSIONS2.has(filePath.slice(filePath.lastIndexOf(".")))).filter((filePath) => isTestPath(filePath) === false).slice(0, DUPLICATION_MAX_FILES);
|
|
4766
|
-
const analyzedFiles = [];
|
|
4767
|
-
let significantTokenCount = 0;
|
|
4768
|
-
let exactWindowCount = 0;
|
|
4769
|
-
for (const relativePath of files) {
|
|
4770
|
-
try {
|
|
4771
|
-
const content = await readFile2(join4(targetPath, relativePath), "utf8");
|
|
4772
|
-
const tokens = tokenizeForDuplication(content, relativePath).slice(
|
|
4773
|
-
0,
|
|
4774
|
-
DUPLICATION_MAX_TOKENS_PER_FILE
|
|
4775
|
-
);
|
|
4776
|
-
significantTokenCount += tokens.length;
|
|
4777
|
-
if (tokens.length < DUPLICATION_MIN_BLOCK_TOKENS) {
|
|
4778
|
-
continue;
|
|
4779
|
-
}
|
|
4780
|
-
exactWindowCount += tokens.length - DUPLICATION_MIN_BLOCK_TOKENS + 1;
|
|
4781
|
-
analyzedFiles.push({
|
|
4782
|
-
file: relativePath,
|
|
4783
|
-
tokens
|
|
4784
|
-
});
|
|
4785
|
-
} catch {
|
|
4786
|
-
}
|
|
4787
|
-
}
|
|
4788
|
-
if (analyzedFiles.length === 0) {
|
|
4789
|
-
return void 0;
|
|
4790
|
-
}
|
|
4791
|
-
const mode = exactWindowCount <= DUPLICATION_EXACT_MAX_WINDOWS ? "exact-token" : "winnowing";
|
|
4792
|
-
const aggregated = mode === "exact-token" ? collectExactTokenDuplication(analyzedFiles) : collectWinnowingDuplication(analyzedFiles);
|
|
4793
|
-
const duplicatedLineRatio = significantTokenCount === 0 ? 0 : Math.min(1, aggregated.duplicatedTokenCount / significantTokenCount);
|
|
4794
|
-
return {
|
|
4795
|
-
mode,
|
|
4796
|
-
duplicatedLineRatio,
|
|
4797
|
-
duplicatedBlockCount: aggregated.duplicatedBlockCount,
|
|
4798
|
-
filesWithDuplication: aggregated.filesWithDuplication
|
|
4799
|
-
};
|
|
4800
|
-
};
|
|
4801
|
-
var toRatio = (value) => {
|
|
4802
|
-
if (typeof value !== "number" || Number.isFinite(value) === false) {
|
|
4803
|
-
return null;
|
|
4804
|
-
}
|
|
4805
|
-
return Math.min(1, Math.max(0, value / 100));
|
|
4806
|
-
};
|
|
4807
|
-
var collectCoverageSignals = async (targetPath, logger) => {
|
|
4808
|
-
const configuredPath = process.env["CODESENTINEL_QUALITY_COVERAGE_SUMMARY"];
|
|
4809
|
-
const summaryPath = configuredPath === void 0 || configuredPath.trim().length === 0 ? join4(targetPath, "coverage", "coverage-summary.json") : resolve3(targetPath, configuredPath);
|
|
4810
|
-
if (!existsSync2(summaryPath)) {
|
|
4811
|
-
return void 0;
|
|
4812
|
-
}
|
|
4813
|
-
try {
|
|
4814
|
-
const raw = await readFile2(summaryPath, "utf8");
|
|
4815
|
-
const parsed = JSON.parse(raw);
|
|
4816
|
-
return {
|
|
4817
|
-
lineCoverage: toRatio(parsed.total?.lines?.pct),
|
|
4818
|
-
branchCoverage: toRatio(parsed.total?.branches?.pct),
|
|
4819
|
-
functionCoverage: toRatio(parsed.total?.functions?.pct),
|
|
4820
|
-
statementCoverage: toRatio(parsed.total?.statements?.pct)
|
|
4821
|
-
};
|
|
4822
|
-
} catch (error) {
|
|
4823
|
-
logger.warn(
|
|
4824
|
-
`quality signals: coverage summary parse failed at ${summaryPath} (${error instanceof Error ? error.message : "unknown error"})`
|
|
4825
|
-
);
|
|
4826
|
-
return void 0;
|
|
4827
|
-
}
|
|
4828
|
-
};
|
|
4829
|
-
var collectQualitySignals = async (targetPath, structural, logger) => {
|
|
4830
|
-
const [todoFixmeCommentCount, eslint, complexity, duplication, coverage] = await Promise.all([
|
|
4831
|
-
collectTodoFixmeCommentCount(targetPath, structural),
|
|
4832
|
-
collectEslintSignals(targetPath, structural, logger),
|
|
4833
|
-
collectComplexitySignals(targetPath, structural),
|
|
4834
|
-
collectDuplicationSignals(targetPath, structural),
|
|
4835
|
-
collectCoverageSignals(targetPath, logger)
|
|
4836
|
-
]);
|
|
4837
|
-
const typescript = collectTypeScriptSignals(targetPath, logger);
|
|
4838
|
-
return {
|
|
4839
|
-
todoFixmeCommentCount,
|
|
4840
|
-
...eslint === void 0 ? {} : { eslint },
|
|
4841
|
-
...typescript === void 0 ? {} : { typescript },
|
|
4842
|
-
...complexity === void 0 ? {} : { complexity },
|
|
4843
|
-
...duplication === void 0 ? {} : { duplication },
|
|
4844
|
-
...coverage === void 0 ? {} : { coverage }
|
|
4845
|
-
};
|
|
4846
|
-
};
|
|
4847
|
-
|
|
4848
|
-
// ../quality-engine/dist/index.js
|
|
4849
|
-
var clamp01 = (value) => {
|
|
4850
|
-
if (!Number.isFinite(value)) {
|
|
4851
|
-
return 0;
|
|
4852
|
-
}
|
|
4853
|
-
if (value <= 0) {
|
|
4854
|
-
return 0;
|
|
4855
|
-
}
|
|
4856
|
-
if (value >= 1) {
|
|
4857
|
-
return 1;
|
|
4858
|
-
}
|
|
4859
|
-
return value;
|
|
4860
|
-
};
|
|
4861
|
-
var round45 = (value) => Number(value.toFixed(4));
|
|
4862
|
-
var average = (values) => {
|
|
4863
|
-
if (values.length === 0) {
|
|
4864
|
-
return 0;
|
|
4865
|
-
}
|
|
4866
|
-
const total = values.reduce((sum, value) => sum + value, 0);
|
|
4867
|
-
return total / values.length;
|
|
4868
|
-
};
|
|
4869
|
-
var concentration = (rawValues) => {
|
|
4870
|
-
const values = rawValues.filter((value) => value > 0);
|
|
4871
|
-
const count = values.length;
|
|
4872
|
-
if (count <= 1) {
|
|
4873
|
-
return 0;
|
|
4874
|
-
}
|
|
4875
|
-
const total = values.reduce((sum, value) => sum + value, 0);
|
|
4876
|
-
if (total <= 0) {
|
|
4877
|
-
return 0;
|
|
4878
|
-
}
|
|
4879
|
-
const hhi = values.reduce((sum, value) => {
|
|
4880
|
-
const share = value / total;
|
|
4881
|
-
return sum + share * share;
|
|
4882
|
-
}, 0);
|
|
4883
|
-
const minHhi = 1 / count;
|
|
4884
|
-
const normalized = (hhi - minHhi) / (1 - minHhi);
|
|
4885
|
-
return clamp01(normalized);
|
|
4886
|
-
};
|
|
4887
|
-
var DIMENSION_WEIGHTS = {
|
|
4888
|
-
modularity: 0.2,
|
|
4889
|
-
changeHygiene: 0.2,
|
|
4890
|
-
staticAnalysis: 0.2,
|
|
4891
|
-
complexity: 0.15,
|
|
4892
|
-
duplication: 0.1,
|
|
4893
|
-
testHealth: 0.15
|
|
4894
|
-
};
|
|
4895
|
-
var QUALITY_TRACE_VERSION = "1";
|
|
4896
|
-
var toPercentage = (normalizedQuality) => round45(clamp01(normalizedQuality) * 100);
|
|
4897
|
-
var logScaled = (value, scale) => {
|
|
4898
|
-
if (scale <= 0) {
|
|
4899
|
-
return 0;
|
|
4900
|
-
}
|
|
4901
|
-
return clamp01(Math.log1p(Math.max(0, value)) / Math.log1p(scale));
|
|
4902
|
-
};
|
|
4903
|
-
var toFactorTrace = (spec) => ({
|
|
4904
|
-
factorId: spec.factorId,
|
|
4905
|
-
contribution: round45(spec.penalty * spec.weight * 100),
|
|
4906
|
-
penalty: round45(spec.penalty),
|
|
4907
|
-
rawMetrics: spec.rawMetrics,
|
|
4908
|
-
normalizedMetrics: spec.normalizedMetrics,
|
|
4909
|
-
weight: round45(spec.weight),
|
|
4910
|
-
evidence: spec.evidence
|
|
4911
|
-
});
|
|
4912
|
-
var createDimensionTrace = (dimension, quality, factors) => ({
|
|
4913
|
-
dimension,
|
|
4914
|
-
normalizedScore: round45(clamp01(quality)),
|
|
4915
|
-
score: toPercentage(quality),
|
|
4916
|
-
factors: factors.map((factor) => toFactorTrace(factor))
|
|
4917
|
-
});
|
|
4918
|
-
var filePaths = (structural) => structural.files.map((file) => file.relativePath);
|
|
4919
|
-
var isTestPath2 = (path) => {
|
|
4920
|
-
const normalized = path.toLowerCase();
|
|
4921
|
-
return normalized.includes("/__tests__/") || normalized.includes("\\__tests__\\") || normalized.includes(".test.") || normalized.includes(".spec.");
|
|
4922
|
-
};
|
|
4923
|
-
var isSourcePath = (path) => {
|
|
4924
|
-
if (path.endsWith(".d.ts")) {
|
|
4925
|
-
return false;
|
|
4926
|
-
}
|
|
4927
|
-
return !isTestPath2(path);
|
|
4702
|
+
var hasTestDirectory = (paths) => paths.some((path) => {
|
|
4703
|
+
const normalized = normalizePath2(path);
|
|
4704
|
+
return normalized.includes("/__tests__/") || normalized.includes("/tests/") || normalized.includes("/test/");
|
|
4705
|
+
});
|
|
4706
|
+
var moduleNameFromPath = (path) => {
|
|
4707
|
+
const normalized = path.replaceAll("\\", "/");
|
|
4708
|
+
const firstSegment = normalized.split("/")[0] ?? normalized;
|
|
4709
|
+
return firstSegment.length === 0 ? normalized : firstSegment;
|
|
4928
4710
|
};
|
|
4929
4711
|
var pushIssue = (issues, issue) => {
|
|
4930
4712
|
issues.push({
|
|
@@ -4932,484 +4714,704 @@ var pushIssue = (issues, issue) => {
|
|
|
4932
4714
|
severity: issue.severity ?? "warn"
|
|
4933
4715
|
});
|
|
4934
4716
|
};
|
|
4935
|
-
var
|
|
4717
|
+
var weightedPenalty = (factors) => clamp01(factors.reduce((sum, factor) => sum + factor.penalty * factor.weight, 0));
|
|
4718
|
+
var computeRepositoryHealthSummary = (input) => {
|
|
4719
|
+
const ownershipPenaltyMultiplier = clamp01(input.config?.ownershipPenaltyMultiplier ?? 1);
|
|
4936
4720
|
const issues = [];
|
|
4937
4721
|
const sourceFileSet = new Set(input.structural.files.map((file) => file.relativePath));
|
|
4938
|
-
const
|
|
4939
|
-
const
|
|
4940
|
-
const
|
|
4941
|
-
const
|
|
4722
|
+
const sourceFileCount = Math.max(1, input.structural.files.length);
|
|
4723
|
+
const structuralEdges = input.structural.edges;
|
|
4724
|
+
const cycleSets = input.structural.cycles.map((cycle) => new Set(cycle.nodes)).filter((set) => set.size >= 2);
|
|
4725
|
+
const cycleNodeSet = /* @__PURE__ */ new Set();
|
|
4726
|
+
for (const cycleSet of cycleSets) {
|
|
4727
|
+
for (const node of cycleSet) {
|
|
4728
|
+
cycleNodeSet.add(node);
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
const edgesInsideCycles = structuralEdges.filter(
|
|
4732
|
+
(edge) => cycleSets.some((cycleSet) => cycleSet.has(edge.from) && cycleSet.has(edge.to))
|
|
4733
|
+
).length;
|
|
4734
|
+
const cycleEdgeRatio = structuralEdges.length === 0 ? 0 : clamp01(edgesInsideCycles / structuralEdges.length);
|
|
4735
|
+
const cycleNodeRatio = clamp01(cycleNodeSet.size / sourceFileCount);
|
|
4736
|
+
const cycleDensityPenalty = clamp01(
|
|
4737
|
+
clamp01(cycleEdgeRatio / 0.2) * 0.75 + clamp01(cycleNodeRatio / 0.35) * 0.25
|
|
4738
|
+
);
|
|
4942
4739
|
const fanInConcentration = concentration(input.structural.files.map((file) => file.fanIn));
|
|
4943
4740
|
const fanOutConcentration = concentration(input.structural.files.map((file) => file.fanOut));
|
|
4944
|
-
const
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
target: hottest?.path ?? input.structural.targetPath,
|
|
4966
|
-
message: "Fan-in/fan-out pressure is concentrated in a small set of files.",
|
|
4967
|
-
impact: round45(centralityConcentration * 0.45)
|
|
4968
|
-
});
|
|
4741
|
+
const fanConcentration = average([fanInConcentration, fanOutConcentration]);
|
|
4742
|
+
const centralityPressure = input.structural.files.map((file) => file.fanIn + file.fanOut);
|
|
4743
|
+
const centralityConcentration = concentration(centralityPressure);
|
|
4744
|
+
let structuralHotspotOverlap = 0;
|
|
4745
|
+
if (input.evolution.available) {
|
|
4746
|
+
const evolutionSourceFiles = input.evolution.files.filter(
|
|
4747
|
+
(file) => sourceFileSet.has(file.filePath)
|
|
4748
|
+
);
|
|
4749
|
+
const topStructuralCount = Math.max(1, Math.ceil(sourceFileCount * 0.1));
|
|
4750
|
+
const topChangeCount = Math.max(1, Math.ceil(Math.max(1, evolutionSourceFiles.length) * 0.1));
|
|
4751
|
+
const topStructural = new Set(
|
|
4752
|
+
[...input.structural.files].map((file) => ({
|
|
4753
|
+
filePath: file.relativePath,
|
|
4754
|
+
pressure: file.fanIn + file.fanOut
|
|
4755
|
+
})).sort((a, b) => b.pressure - a.pressure || a.filePath.localeCompare(b.filePath)).slice(0, topStructuralCount).map((item) => item.filePath)
|
|
4756
|
+
);
|
|
4757
|
+
const topChange = new Set(
|
|
4758
|
+
[...evolutionSourceFiles].sort((a, b) => b.churnTotal - a.churnTotal || a.filePath.localeCompare(b.filePath)).slice(0, topChangeCount).map((item) => item.filePath)
|
|
4759
|
+
);
|
|
4760
|
+
const overlapCount = [...topStructural].filter((filePath) => topChange.has(filePath)).length;
|
|
4761
|
+
structuralHotspotOverlap = topStructural.size === 0 || topChange.size === 0 ? 0 : clamp01(overlapCount / Math.min(topStructural.size, topChange.size));
|
|
4969
4762
|
}
|
|
4970
4763
|
const modularityFactors = [
|
|
4971
4764
|
{
|
|
4972
|
-
factorId: "
|
|
4973
|
-
penalty:
|
|
4765
|
+
factorId: "health.modularity.cycle_density",
|
|
4766
|
+
penalty: cycleDensityPenalty,
|
|
4974
4767
|
rawMetrics: {
|
|
4975
|
-
cycleCount,
|
|
4976
|
-
|
|
4768
|
+
cycleCount: input.structural.metrics.cycleCount,
|
|
4769
|
+
cycleEdgeRatio: round45(cycleEdgeRatio),
|
|
4770
|
+
cycleNodeRatio: round45(cycleNodeRatio)
|
|
4977
4771
|
},
|
|
4978
4772
|
normalizedMetrics: {
|
|
4979
|
-
|
|
4773
|
+
cycleDensityPenalty: round45(cycleDensityPenalty)
|
|
4980
4774
|
},
|
|
4981
|
-
weight: 0.
|
|
4982
|
-
evidence: [{ kind: "repository_metric", metric: "structural.
|
|
4775
|
+
weight: 0.4,
|
|
4776
|
+
evidence: [{ kind: "repository_metric", metric: "structural.cycleEdgeRatio" }]
|
|
4983
4777
|
},
|
|
4984
4778
|
{
|
|
4985
|
-
factorId: "
|
|
4986
|
-
penalty:
|
|
4779
|
+
factorId: "health.modularity.fan_concentration",
|
|
4780
|
+
penalty: fanConcentration,
|
|
4987
4781
|
rawMetrics: {
|
|
4988
4782
|
fanInConcentration: round45(fanInConcentration),
|
|
4989
4783
|
fanOutConcentration: round45(fanOutConcentration)
|
|
4990
4784
|
},
|
|
4991
4785
|
normalizedMetrics: {
|
|
4992
|
-
|
|
4786
|
+
fanConcentration: round45(fanConcentration)
|
|
4993
4787
|
},
|
|
4994
|
-
weight: 0.
|
|
4788
|
+
weight: 0.25,
|
|
4995
4789
|
evidence: [{ kind: "repository_metric", metric: "structural.files.fanIn/fanOut" }]
|
|
4790
|
+
},
|
|
4791
|
+
{
|
|
4792
|
+
factorId: "health.modularity.centrality_concentration",
|
|
4793
|
+
penalty: centralityConcentration,
|
|
4794
|
+
rawMetrics: {
|
|
4795
|
+
centralityConcentration: round45(centralityConcentration)
|
|
4796
|
+
},
|
|
4797
|
+
normalizedMetrics: {
|
|
4798
|
+
centralityConcentration: round45(centralityConcentration)
|
|
4799
|
+
},
|
|
4800
|
+
weight: 0.2,
|
|
4801
|
+
evidence: [{ kind: "repository_metric", metric: "structural.centralityPressure" }]
|
|
4802
|
+
},
|
|
4803
|
+
{
|
|
4804
|
+
factorId: "health.modularity.hotspot_overlap",
|
|
4805
|
+
penalty: structuralHotspotOverlap,
|
|
4806
|
+
rawMetrics: {
|
|
4807
|
+
structuralHotspotOverlap: round45(structuralHotspotOverlap)
|
|
4808
|
+
},
|
|
4809
|
+
normalizedMetrics: {
|
|
4810
|
+
structuralHotspotOverlap: round45(structuralHotspotOverlap)
|
|
4811
|
+
},
|
|
4812
|
+
weight: 0.15,
|
|
4813
|
+
evidence: [{ kind: "repository_metric", metric: "structural.evolution.hotspotOverlap" }]
|
|
4996
4814
|
}
|
|
4997
4815
|
];
|
|
4998
|
-
const modularityPenalty =
|
|
4999
|
-
modularityFactors
|
|
4816
|
+
const modularityPenalty = dampenForSmallSamples(
|
|
4817
|
+
weightedPenalty(modularityFactors),
|
|
4818
|
+
sourceFileCount,
|
|
4819
|
+
8,
|
|
4820
|
+
0.45
|
|
5000
4821
|
);
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
4822
|
+
if (cycleDensityPenalty >= 0.35) {
|
|
4823
|
+
const firstCycle = input.structural.cycles[0];
|
|
4824
|
+
pushIssue(issues, {
|
|
4825
|
+
id: "health.modularity.cycle_density",
|
|
4826
|
+
ruleId: "graph.cycle_density",
|
|
4827
|
+
signal: "structural.cycleEdgeRatio",
|
|
4828
|
+
dimension: "modularity",
|
|
4829
|
+
target: firstCycle?.nodes.slice().sort((a, b) => a.localeCompare(b)).join(" -> ") ?? input.structural.targetPath,
|
|
4830
|
+
message: "Dependencies inside cycles consume a high share of graph edges, reducing refactor flexibility.",
|
|
4831
|
+
severity: cycleDensityPenalty >= 0.7 ? "error" : "warn",
|
|
4832
|
+
evidenceMetrics: {
|
|
4833
|
+
cycleCount: input.structural.metrics.cycleCount,
|
|
4834
|
+
cycleEdgeRatio: round45(cycleEdgeRatio),
|
|
4835
|
+
cycleNodeRatio: round45(cycleNodeRatio)
|
|
4836
|
+
},
|
|
4837
|
+
impact: round45(modularityPenalty * 0.35)
|
|
4838
|
+
});
|
|
4839
|
+
}
|
|
4840
|
+
if (centralityConcentration >= 0.5) {
|
|
4841
|
+
const hottest = [...input.structural.files].map((file) => ({
|
|
4842
|
+
path: file.relativePath,
|
|
4843
|
+
pressure: file.fanIn + file.fanOut
|
|
4844
|
+
})).sort((a, b) => b.pressure - a.pressure || a.path.localeCompare(b.path))[0];
|
|
4845
|
+
pushIssue(issues, {
|
|
4846
|
+
id: "health.modularity.centrality_concentration",
|
|
4847
|
+
ruleId: "graph.centrality_concentration",
|
|
4848
|
+
signal: "structural.centralityPressure",
|
|
4849
|
+
dimension: "modularity",
|
|
4850
|
+
target: hottest?.path ?? input.structural.targetPath,
|
|
4851
|
+
message: "Dependency flow is concentrated in a narrow set of files, creating architectural bottlenecks.",
|
|
4852
|
+
evidenceMetrics: {
|
|
4853
|
+
fanConcentration: round45(fanConcentration),
|
|
4854
|
+
centralityConcentration: round45(centralityConcentration)
|
|
4855
|
+
},
|
|
4856
|
+
impact: round45(modularityPenalty * 0.25)
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
if (structuralHotspotOverlap >= 0.5) {
|
|
4860
|
+
pushIssue(issues, {
|
|
4861
|
+
id: "health.modularity.hotspot_overlap",
|
|
4862
|
+
ruleId: "graph.hotspot_overlap",
|
|
4863
|
+
signal: "structural.evolution.hotspotOverlap",
|
|
4864
|
+
dimension: "modularity",
|
|
4865
|
+
target: input.structural.targetPath,
|
|
4866
|
+
message: "Structural hubs overlap with top churn hotspots, making change pressure harder to isolate.",
|
|
4867
|
+
evidenceMetrics: {
|
|
4868
|
+
structuralHotspotOverlap: round45(structuralHotspotOverlap)
|
|
4869
|
+
},
|
|
4870
|
+
impact: round45(modularityPenalty * 0.2)
|
|
4871
|
+
});
|
|
4872
|
+
}
|
|
4873
|
+
let churnConcentrationPenalty = 0;
|
|
4874
|
+
let volatilityConcentrationPenalty = 0;
|
|
4875
|
+
let coChangeClusterPenalty = 0;
|
|
4876
|
+
let top10PercentFilesChurnShare = 0;
|
|
4877
|
+
let top10PercentFilesVolatilityShare = 0;
|
|
4878
|
+
let denseCoChangePairRatio = 0;
|
|
5005
4879
|
if (input.evolution.available) {
|
|
5006
4880
|
const evolutionSourceFiles = input.evolution.files.filter(
|
|
5007
4881
|
(file) => sourceFileSet.has(file.filePath)
|
|
5008
4882
|
);
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
evolutionSourceFiles.map((file) => file.
|
|
4883
|
+
const evolutionFileCount = evolutionSourceFiles.length;
|
|
4884
|
+
top10PercentFilesChurnShare = topPercentShare(
|
|
4885
|
+
evolutionSourceFiles.map((file) => file.churnTotal),
|
|
4886
|
+
0.1
|
|
4887
|
+
);
|
|
4888
|
+
top10PercentFilesVolatilityShare = topPercentShare(
|
|
4889
|
+
evolutionSourceFiles.map((file) => file.recentVolatility),
|
|
4890
|
+
0.1
|
|
5012
4891
|
);
|
|
5013
|
-
const fileCount = Math.max(1, evolutionSourceFiles.length);
|
|
5014
|
-
const maxPairs = fileCount * (fileCount - 1) / 2;
|
|
5015
4892
|
const sourcePairs = input.evolution.coupling.pairs.filter(
|
|
5016
4893
|
(pair) => sourceFileSet.has(pair.fileA) && sourceFileSet.has(pair.fileB)
|
|
5017
4894
|
);
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
4895
|
+
const maxPairs = evolutionFileCount * (evolutionFileCount - 1) / 2;
|
|
4896
|
+
const densePairs = sourcePairs.filter((pair) => pair.couplingScore >= 0.55);
|
|
4897
|
+
denseCoChangePairRatio = maxPairs <= 0 ? 0 : clamp01(densePairs.length / maxPairs);
|
|
4898
|
+
const couplingScoreConcentration = concentration(sourcePairs.map((pair) => pair.couplingScore));
|
|
4899
|
+
churnConcentrationPenalty = dampenForSmallSamples(
|
|
4900
|
+
clamp01((top10PercentFilesChurnShare - 0.35) / 0.55),
|
|
4901
|
+
evolutionFileCount,
|
|
4902
|
+
12,
|
|
4903
|
+
0.3
|
|
4904
|
+
);
|
|
4905
|
+
volatilityConcentrationPenalty = dampenForSmallSamples(
|
|
4906
|
+
clamp01((top10PercentFilesVolatilityShare - 0.35) / 0.55),
|
|
4907
|
+
evolutionFileCount,
|
|
4908
|
+
12,
|
|
4909
|
+
0.3
|
|
4910
|
+
);
|
|
4911
|
+
const coChangeRaw = average([
|
|
4912
|
+
clamp01(denseCoChangePairRatio / 0.2),
|
|
4913
|
+
couplingScoreConcentration
|
|
4914
|
+
]);
|
|
4915
|
+
coChangeClusterPenalty = dampenForSmallSamples(coChangeRaw, sourcePairs.length, 20, 0.35);
|
|
4916
|
+
if (churnConcentrationPenalty >= 0.35) {
|
|
5021
4917
|
const mostChurn = [...evolutionSourceFiles].sort(
|
|
5022
4918
|
(a, b) => b.churnTotal - a.churnTotal || a.filePath.localeCompare(b.filePath)
|
|
5023
4919
|
)[0];
|
|
5024
4920
|
pushIssue(issues, {
|
|
5025
|
-
id: "
|
|
5026
|
-
ruleId: "git.
|
|
4921
|
+
id: "health.change_hygiene.churn_concentration",
|
|
4922
|
+
ruleId: "git.churn_distribution",
|
|
4923
|
+
signal: "evolution.top10PercentFilesChurnShare",
|
|
5027
4924
|
dimension: "changeHygiene",
|
|
5028
4925
|
target: mostChurn?.filePath ?? input.structural.targetPath,
|
|
5029
|
-
message: "
|
|
5030
|
-
|
|
4926
|
+
message: "A small slice of files carries most churn, reducing change predictability.",
|
|
4927
|
+
evidenceMetrics: {
|
|
4928
|
+
top10PercentFilesChurnShare: round45(top10PercentFilesChurnShare)
|
|
4929
|
+
},
|
|
4930
|
+
impact: round45(churnConcentrationPenalty * 0.4)
|
|
5031
4931
|
});
|
|
5032
4932
|
}
|
|
5033
|
-
if (
|
|
4933
|
+
if (volatilityConcentrationPenalty >= 0.35) {
|
|
5034
4934
|
const volatileFile = [...evolutionSourceFiles].sort(
|
|
5035
4935
|
(a, b) => b.recentVolatility - a.recentVolatility || a.filePath.localeCompare(b.filePath)
|
|
5036
4936
|
)[0];
|
|
5037
4937
|
pushIssue(issues, {
|
|
5038
|
-
id: "
|
|
5039
|
-
ruleId: "git.
|
|
4938
|
+
id: "health.change_hygiene.volatility_concentration",
|
|
4939
|
+
ruleId: "git.volatility_distribution",
|
|
4940
|
+
signal: "evolution.top10PercentFilesVolatilityShare",
|
|
5040
4941
|
dimension: "changeHygiene",
|
|
5041
4942
|
target: volatileFile?.filePath ?? input.structural.targetPath,
|
|
5042
|
-
message: "Recent volatility is concentrated
|
|
5043
|
-
|
|
4943
|
+
message: "Recent volatility is concentrated, increasing review and release uncertainty.",
|
|
4944
|
+
evidenceMetrics: {
|
|
4945
|
+
top10PercentFilesVolatilityShare: round45(top10PercentFilesVolatilityShare)
|
|
4946
|
+
},
|
|
4947
|
+
impact: round45(volatilityConcentrationPenalty * 0.3)
|
|
5044
4948
|
});
|
|
5045
4949
|
}
|
|
5046
|
-
if (
|
|
4950
|
+
if (coChangeClusterPenalty >= 0.35) {
|
|
5047
4951
|
const strongestPair = [...sourcePairs].sort(
|
|
5048
4952
|
(a, b) => b.couplingScore - a.couplingScore || `${a.fileA}|${a.fileB}`.localeCompare(`${b.fileA}|${b.fileB}`)
|
|
5049
4953
|
)[0];
|
|
5050
4954
|
pushIssue(issues, {
|
|
5051
|
-
id: "
|
|
5052
|
-
ruleId: "git.
|
|
4955
|
+
id: "health.change_hygiene.dense_co_change_clusters",
|
|
4956
|
+
ruleId: "git.co_change_clusters",
|
|
4957
|
+
signal: "evolution.denseCoChangePairRatio",
|
|
5053
4958
|
dimension: "changeHygiene",
|
|
5054
4959
|
target: strongestPair === void 0 ? input.structural.targetPath : `${strongestPair.fileA}<->${strongestPair.fileB}`,
|
|
5055
|
-
message: "
|
|
5056
|
-
|
|
4960
|
+
message: "Dense co-change clusters suggest wider coordination scope per change.",
|
|
4961
|
+
evidenceMetrics: {
|
|
4962
|
+
denseCoChangePairRatio: round45(denseCoChangePairRatio)
|
|
4963
|
+
},
|
|
4964
|
+
impact: round45(coChangeClusterPenalty * 0.3)
|
|
5057
4965
|
});
|
|
5058
4966
|
}
|
|
5059
4967
|
}
|
|
5060
|
-
const todoFixmeCommentCount = Math.max(0, signals?.todoFixmeCommentCount ?? 0);
|
|
5061
|
-
const todoFixmePenalty = logScaled(todoFixmeCommentCount, 80) * 0.08;
|
|
5062
|
-
if (todoFixmeCommentCount > 0) {
|
|
5063
|
-
pushIssue(issues, {
|
|
5064
|
-
id: "quality.change_hygiene.todo_fixme_load",
|
|
5065
|
-
ruleId: "comments.todo_fixme",
|
|
5066
|
-
dimension: "changeHygiene",
|
|
5067
|
-
target: input.structural.targetPath,
|
|
5068
|
-
message: `Found ${todoFixmeCommentCount} TODO/FIXME comment marker(s); cleanup debt is accumulating.`,
|
|
5069
|
-
impact: round45(todoFixmePenalty * 0.4)
|
|
5070
|
-
});
|
|
5071
|
-
}
|
|
5072
4968
|
const changeHygieneFactors = [
|
|
5073
4969
|
{
|
|
5074
|
-
factorId: "
|
|
5075
|
-
penalty:
|
|
4970
|
+
factorId: "health.change_hygiene.churn_concentration",
|
|
4971
|
+
penalty: churnConcentrationPenalty,
|
|
5076
4972
|
rawMetrics: {
|
|
5077
|
-
|
|
4973
|
+
top10PercentFilesChurnShare: round45(top10PercentFilesChurnShare)
|
|
5078
4974
|
},
|
|
5079
4975
|
normalizedMetrics: {
|
|
5080
|
-
|
|
5081
|
-
},
|
|
5082
|
-
weight: 0.35,
|
|
5083
|
-
evidence: [{ kind: "repository_metric", metric: "evolution.churn" }]
|
|
5084
|
-
},
|
|
5085
|
-
{
|
|
5086
|
-
factorId: "quality.change_hygiene.volatility_concentration",
|
|
5087
|
-
penalty: volatilityConcentration,
|
|
5088
|
-
rawMetrics: {
|
|
5089
|
-
volatilityConcentration: round45(volatilityConcentration)
|
|
4976
|
+
churnConcentrationPenalty: round45(churnConcentrationPenalty)
|
|
5090
4977
|
},
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
},
|
|
5094
|
-
weight: 0.25,
|
|
5095
|
-
evidence: [{ kind: "repository_metric", metric: "evolution.recentVolatility" }]
|
|
4978
|
+
weight: 0.4,
|
|
4979
|
+
evidence: [{ kind: "repository_metric", metric: "evolution.top10PercentFilesChurnShare" }]
|
|
5096
4980
|
},
|
|
5097
4981
|
{
|
|
5098
|
-
factorId: "
|
|
5099
|
-
penalty:
|
|
4982
|
+
factorId: "health.change_hygiene.volatility_concentration",
|
|
4983
|
+
penalty: volatilityConcentrationPenalty,
|
|
5100
4984
|
rawMetrics: {
|
|
5101
|
-
|
|
5102
|
-
couplingIntensity: round45(couplingIntensity)
|
|
4985
|
+
top10PercentFilesVolatilityShare: round45(top10PercentFilesVolatilityShare)
|
|
5103
4986
|
},
|
|
5104
4987
|
normalizedMetrics: {
|
|
5105
|
-
|
|
4988
|
+
volatilityConcentrationPenalty: round45(volatilityConcentrationPenalty)
|
|
5106
4989
|
},
|
|
5107
4990
|
weight: 0.3,
|
|
5108
|
-
evidence: [
|
|
4991
|
+
evidence: [
|
|
4992
|
+
{
|
|
4993
|
+
kind: "repository_metric",
|
|
4994
|
+
metric: "evolution.top10PercentFilesVolatilityShare"
|
|
4995
|
+
}
|
|
4996
|
+
]
|
|
5109
4997
|
},
|
|
5110
4998
|
{
|
|
5111
|
-
factorId: "
|
|
5112
|
-
penalty:
|
|
4999
|
+
factorId: "health.change_hygiene.dense_co_change_clusters",
|
|
5000
|
+
penalty: coChangeClusterPenalty,
|
|
5113
5001
|
rawMetrics: {
|
|
5114
|
-
|
|
5002
|
+
denseCoChangePairRatio: round45(denseCoChangePairRatio)
|
|
5115
5003
|
},
|
|
5116
5004
|
normalizedMetrics: {
|
|
5117
|
-
|
|
5005
|
+
coChangeClusterPenalty: round45(coChangeClusterPenalty)
|
|
5118
5006
|
},
|
|
5119
|
-
weight: 0.
|
|
5120
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
5007
|
+
weight: 0.3,
|
|
5008
|
+
evidence: [{ kind: "repository_metric", metric: "evolution.denseCoChangePairRatio" }]
|
|
5121
5009
|
}
|
|
5122
5010
|
];
|
|
5123
|
-
const changeHygienePenalty = input.evolution.available ?
|
|
5124
|
-
const
|
|
5125
|
-
const
|
|
5126
|
-
const
|
|
5127
|
-
const
|
|
5128
|
-
const
|
|
5129
|
-
const
|
|
5130
|
-
const
|
|
5131
|
-
const
|
|
5132
|
-
|
|
5133
|
-
factorId: "quality.static_analysis.eslint_errors",
|
|
5134
|
-
penalty: clamp01(eslintErrorRate / 0.5),
|
|
5135
|
-
rawMetrics: {
|
|
5136
|
-
eslintErrorCount: eslint?.errorCount ?? 0,
|
|
5137
|
-
eslintFilesWithIssues: eslint?.filesWithIssues ?? 0
|
|
5138
|
-
},
|
|
5139
|
-
normalizedMetrics: {
|
|
5140
|
-
eslintErrorRate: round45(eslintErrorRate)
|
|
5141
|
-
},
|
|
5142
|
-
weight: 0.5,
|
|
5143
|
-
evidence: [{ kind: "repository_metric", metric: "eslint.errorCount" }]
|
|
5144
|
-
},
|
|
5011
|
+
const changeHygienePenalty = input.evolution.available ? weightedPenalty(changeHygieneFactors) : 0.12;
|
|
5012
|
+
const paths = filePaths(input.structural);
|
|
5013
|
+
const testFiles = paths.filter((path) => isTestPath(path)).length;
|
|
5014
|
+
const sourceFiles = paths.filter((path) => isSourcePath(path)).length;
|
|
5015
|
+
const testRatio = sourceFiles <= 0 ? 1 : testFiles / sourceFiles;
|
|
5016
|
+
const testingDirectoryPresent = hasTestDirectory(paths);
|
|
5017
|
+
const testPresencePenalty = sourceFiles <= 0 ? 0 : testFiles === 0 ? 1 : 0;
|
|
5018
|
+
const testRatioPenalty = sourceFiles <= 0 ? 0 : 1 - clamp01(testRatio / 0.25);
|
|
5019
|
+
const testingDirectoryPenalty = sourceFiles <= 0 ? 0 : testingDirectoryPresent ? 0 : 0.35;
|
|
5020
|
+
const testHealthFactors = [
|
|
5145
5021
|
{
|
|
5146
|
-
factorId: "
|
|
5147
|
-
penalty:
|
|
5022
|
+
factorId: "health.test_health.test_file_presence",
|
|
5023
|
+
penalty: testPresencePenalty,
|
|
5148
5024
|
rawMetrics: {
|
|
5149
|
-
|
|
5025
|
+
sourceFiles,
|
|
5026
|
+
testFiles
|
|
5150
5027
|
},
|
|
5151
5028
|
normalizedMetrics: {
|
|
5152
|
-
|
|
5029
|
+
testPresencePenalty: round45(testPresencePenalty)
|
|
5153
5030
|
},
|
|
5154
|
-
weight: 0.
|
|
5155
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
5031
|
+
weight: 0.4,
|
|
5032
|
+
evidence: [{ kind: "repository_metric", metric: "tests.filePresence" }]
|
|
5156
5033
|
},
|
|
5157
5034
|
{
|
|
5158
|
-
factorId: "
|
|
5159
|
-
penalty:
|
|
5035
|
+
factorId: "health.test_health.test_to_source_ratio",
|
|
5036
|
+
penalty: testRatioPenalty,
|
|
5160
5037
|
rawMetrics: {
|
|
5161
|
-
|
|
5162
|
-
typeScriptFilesWithDiagnostics: tsc?.filesWithDiagnostics ?? 0
|
|
5038
|
+
testToSourceRatio: round45(testRatio)
|
|
5163
5039
|
},
|
|
5164
5040
|
normalizedMetrics: {
|
|
5165
|
-
|
|
5041
|
+
testRatioPenalty: round45(testRatioPenalty)
|
|
5166
5042
|
},
|
|
5167
|
-
weight: 0.
|
|
5168
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
5043
|
+
weight: 0.45,
|
|
5044
|
+
evidence: [{ kind: "repository_metric", metric: "tests.testToSourceRatio" }]
|
|
5169
5045
|
},
|
|
5170
5046
|
{
|
|
5171
|
-
factorId: "
|
|
5172
|
-
penalty:
|
|
5047
|
+
factorId: "health.test_health.testing_directory_presence",
|
|
5048
|
+
penalty: testingDirectoryPenalty,
|
|
5173
5049
|
rawMetrics: {
|
|
5174
|
-
|
|
5050
|
+
testingDirectoryPresent: testingDirectoryPresent ? 1 : 0
|
|
5175
5051
|
},
|
|
5176
5052
|
normalizedMetrics: {
|
|
5177
|
-
|
|
5053
|
+
testingDirectoryPenalty: round45(testingDirectoryPenalty)
|
|
5178
5054
|
},
|
|
5179
|
-
weight: 0.
|
|
5180
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
5055
|
+
weight: 0.15,
|
|
5056
|
+
evidence: [{ kind: "repository_metric", metric: "tests.directoryPresence" }]
|
|
5181
5057
|
}
|
|
5182
5058
|
];
|
|
5183
|
-
const
|
|
5184
|
-
|
|
5059
|
+
const testHealthPenalty = dampenForSmallSamples(
|
|
5060
|
+
weightedPenalty(testHealthFactors),
|
|
5061
|
+
sourceFiles,
|
|
5062
|
+
10,
|
|
5063
|
+
0.3
|
|
5185
5064
|
);
|
|
5186
|
-
if (
|
|
5187
|
-
const topRule = [...eslint?.ruleCounts ?? []].sort(
|
|
5188
|
-
(a, b) => b.count - a.count || a.ruleId.localeCompare(b.ruleId)
|
|
5189
|
-
)[0];
|
|
5065
|
+
if (sourceFiles > 0 && testFiles === 0) {
|
|
5190
5066
|
pushIssue(issues, {
|
|
5191
|
-
id: "
|
|
5192
|
-
ruleId:
|
|
5193
|
-
|
|
5067
|
+
id: "health.test_health.low_test_presence",
|
|
5068
|
+
ruleId: "tests.file_presence",
|
|
5069
|
+
signal: "tests.filePresence",
|
|
5070
|
+
dimension: "testHealth",
|
|
5194
5071
|
target: input.structural.targetPath,
|
|
5195
|
-
message:
|
|
5196
|
-
severity: "error",
|
|
5197
|
-
|
|
5072
|
+
message: `No test files detected for ${sourceFiles} source file(s).`,
|
|
5073
|
+
severity: sourceFiles >= 12 ? "error" : "warn",
|
|
5074
|
+
evidenceMetrics: {
|
|
5075
|
+
sourceFiles,
|
|
5076
|
+
testFiles,
|
|
5077
|
+
testToSourceRatio: round45(testRatio)
|
|
5078
|
+
},
|
|
5079
|
+
impact: round45(testHealthPenalty * 0.45)
|
|
5198
5080
|
});
|
|
5199
5081
|
}
|
|
5200
|
-
if (
|
|
5082
|
+
if (sourceFiles > 0 && testRatio < 0.12) {
|
|
5201
5083
|
pushIssue(issues, {
|
|
5202
|
-
id: "
|
|
5203
|
-
ruleId: "
|
|
5204
|
-
|
|
5084
|
+
id: "health.test_health.low_test_ratio",
|
|
5085
|
+
ruleId: "tests.ratio",
|
|
5086
|
+
signal: "tests.testToSourceRatio",
|
|
5087
|
+
dimension: "testHealth",
|
|
5205
5088
|
target: input.structural.targetPath,
|
|
5206
|
-
message:
|
|
5207
|
-
|
|
5208
|
-
|
|
5089
|
+
message: "Test-to-source ratio is low; long-term change confidence may degrade.",
|
|
5090
|
+
evidenceMetrics: {
|
|
5091
|
+
sourceFiles,
|
|
5092
|
+
testFiles,
|
|
5093
|
+
testToSourceRatio: round45(testRatio)
|
|
5094
|
+
},
|
|
5095
|
+
impact: round45(testHealthPenalty * 0.35)
|
|
5209
5096
|
});
|
|
5210
5097
|
}
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5098
|
+
if (input.evolution.available) {
|
|
5099
|
+
const evolutionSourceFiles = input.evolution.files.filter(
|
|
5100
|
+
(file) => sourceFileSet.has(file.filePath)
|
|
5101
|
+
);
|
|
5102
|
+
const authorTotals = /* @__PURE__ */ new Map();
|
|
5103
|
+
const moduleTotals = /* @__PURE__ */ new Map();
|
|
5104
|
+
const moduleAuthors = /* @__PURE__ */ new Map();
|
|
5105
|
+
let singleContributorFiles = 0;
|
|
5106
|
+
let trackedFiles = 0;
|
|
5107
|
+
for (const file of evolutionSourceFiles) {
|
|
5108
|
+
if (file.commitCount <= 0 || file.authorDistribution.length === 0) {
|
|
5109
|
+
continue;
|
|
5110
|
+
}
|
|
5111
|
+
trackedFiles += 1;
|
|
5112
|
+
const dominantShare = clamp01(file.authorDistribution[0]?.share ?? 0);
|
|
5113
|
+
if (file.authorDistribution.length === 1 || dominantShare >= 0.9) {
|
|
5114
|
+
singleContributorFiles += 1;
|
|
5115
|
+
}
|
|
5116
|
+
for (const author of file.authorDistribution) {
|
|
5117
|
+
const commits = Math.max(0, author.commits);
|
|
5118
|
+
if (commits <= 0) {
|
|
5119
|
+
continue;
|
|
5120
|
+
}
|
|
5121
|
+
const moduleName = moduleNameFromPath(file.filePath);
|
|
5122
|
+
const moduleAuthorTotals = moduleAuthors.get(moduleName) ?? /* @__PURE__ */ new Map();
|
|
5123
|
+
if (moduleAuthors.has(moduleName) === false) {
|
|
5124
|
+
moduleAuthors.set(moduleName, moduleAuthorTotals);
|
|
5125
|
+
}
|
|
5126
|
+
authorTotals.set(author.authorId, (authorTotals.get(author.authorId) ?? 0) + commits);
|
|
5127
|
+
moduleTotals.set(moduleName, (moduleTotals.get(moduleName) ?? 0) + commits);
|
|
5128
|
+
moduleAuthorTotals.set(
|
|
5129
|
+
author.authorId,
|
|
5130
|
+
(moduleAuthorTotals.get(author.authorId) ?? 0) + commits
|
|
5131
|
+
);
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
const totalAuthorCommits = [...authorTotals.values()].reduce((sum, value) => sum + value, 0);
|
|
5135
|
+
const highestAuthorCommits = [...authorTotals.values()].sort((a, b) => b - a)[0] ?? 0;
|
|
5136
|
+
const topAuthorCommitShare = totalAuthorCommits <= 0 ? 0 : clamp01(highestAuthorCommits / totalAuthorCommits);
|
|
5137
|
+
const filesWithSingleContributorRatio = trackedFiles === 0 ? 0 : clamp01(singleContributorFiles / trackedFiles);
|
|
5138
|
+
const authorEntropy = normalizedEntropy([...authorTotals.values()]);
|
|
5139
|
+
let dominatedModules = 0;
|
|
5140
|
+
let trackedModules = 0;
|
|
5141
|
+
for (const [moduleName, moduleCommitTotal] of moduleTotals.entries()) {
|
|
5142
|
+
if (moduleCommitTotal < 5) {
|
|
5143
|
+
continue;
|
|
5144
|
+
}
|
|
5145
|
+
const perModuleAuthors = moduleAuthors.get(moduleName);
|
|
5146
|
+
if (perModuleAuthors === void 0) {
|
|
5147
|
+
continue;
|
|
5148
|
+
}
|
|
5149
|
+
trackedModules += 1;
|
|
5150
|
+
const topAuthorModuleCommits = [...perModuleAuthors.values()].sort((a, b) => b - a)[0] ?? 0;
|
|
5151
|
+
const moduleTopShare = moduleCommitTotal <= 0 ? 0 : topAuthorModuleCommits / moduleCommitTotal;
|
|
5152
|
+
if (moduleTopShare >= 0.8) {
|
|
5153
|
+
dominatedModules += 1;
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
const modulesDominatedBySingleContributorRatio = trackedModules === 0 ? 0 : clamp01(dominatedModules / trackedModules);
|
|
5157
|
+
const ownershipSampleSize = trackedFiles;
|
|
5158
|
+
const ownershipCommitVolume = totalAuthorCommits;
|
|
5159
|
+
const ownershipReliability = average([
|
|
5160
|
+
clamp01(ownershipSampleSize / 12),
|
|
5161
|
+
clamp01(ownershipCommitVolume / 180)
|
|
5162
|
+
]);
|
|
5163
|
+
const topAuthorPenalty = clamp01((topAuthorCommitShare - 0.55) / 0.4);
|
|
5164
|
+
const singleContributorPenalty = clamp01((filesWithSingleContributorRatio - 0.35) / 0.6);
|
|
5165
|
+
const entropyPenalty = clamp01((0.75 - authorEntropy) / 0.75);
|
|
5166
|
+
const moduleDominancePenalty = clamp01((modulesDominatedBySingleContributorRatio - 0.4) / 0.6);
|
|
5167
|
+
const ownershipBasePenalty = weightedPenalty([
|
|
5168
|
+
{
|
|
5169
|
+
factorId: "health.ownership.top_author_commit_share",
|
|
5170
|
+
penalty: topAuthorPenalty,
|
|
5171
|
+
rawMetrics: {
|
|
5172
|
+
topAuthorCommitShare: round45(topAuthorCommitShare)
|
|
5173
|
+
},
|
|
5174
|
+
normalizedMetrics: {
|
|
5175
|
+
topAuthorPenalty: round45(topAuthorPenalty)
|
|
5176
|
+
},
|
|
5177
|
+
weight: 0.35,
|
|
5178
|
+
evidence: [{ kind: "repository_metric", metric: "ownership.topAuthorCommitShare" }]
|
|
5221
5179
|
},
|
|
5222
|
-
|
|
5223
|
-
|
|
5180
|
+
{
|
|
5181
|
+
factorId: "health.ownership.files_with_single_contributor_ratio",
|
|
5182
|
+
penalty: singleContributorPenalty,
|
|
5183
|
+
rawMetrics: {
|
|
5184
|
+
filesWithSingleContributorRatio: round45(filesWithSingleContributorRatio)
|
|
5185
|
+
},
|
|
5186
|
+
normalizedMetrics: {
|
|
5187
|
+
singleContributorPenalty: round45(singleContributorPenalty)
|
|
5188
|
+
},
|
|
5189
|
+
weight: 0.25,
|
|
5190
|
+
evidence: [
|
|
5191
|
+
{
|
|
5192
|
+
kind: "repository_metric",
|
|
5193
|
+
metric: "ownership.filesWithSingleContributorRatio"
|
|
5194
|
+
}
|
|
5195
|
+
]
|
|
5224
5196
|
},
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5197
|
+
{
|
|
5198
|
+
factorId: "health.ownership.author_entropy",
|
|
5199
|
+
penalty: entropyPenalty,
|
|
5200
|
+
rawMetrics: {
|
|
5201
|
+
authorEntropy: round45(authorEntropy)
|
|
5202
|
+
},
|
|
5203
|
+
normalizedMetrics: {
|
|
5204
|
+
authorEntropyPenalty: round45(entropyPenalty)
|
|
5205
|
+
},
|
|
5206
|
+
weight: 0.25,
|
|
5207
|
+
evidence: [{ kind: "repository_metric", metric: "ownership.authorEntropy" }]
|
|
5233
5208
|
},
|
|
5234
|
-
|
|
5235
|
-
|
|
5209
|
+
{
|
|
5210
|
+
factorId: "health.ownership.module_single_author_dominance",
|
|
5211
|
+
penalty: moduleDominancePenalty,
|
|
5212
|
+
rawMetrics: {
|
|
5213
|
+
modulesDominatedBySingleContributorRatio: round45(
|
|
5214
|
+
modulesDominatedBySingleContributorRatio
|
|
5215
|
+
)
|
|
5216
|
+
},
|
|
5217
|
+
normalizedMetrics: {
|
|
5218
|
+
moduleDominancePenalty: round45(moduleDominancePenalty)
|
|
5219
|
+
},
|
|
5220
|
+
weight: 0.15,
|
|
5221
|
+
evidence: [{ kind: "repository_metric", metric: "ownership.moduleDominance" }]
|
|
5222
|
+
}
|
|
5223
|
+
]);
|
|
5224
|
+
const ownershipDistributionPenalty2 = clamp01(
|
|
5225
|
+
ownershipBasePenalty * (0.3 + 0.7 * ownershipReliability) * ownershipPenaltyMultiplier
|
|
5226
|
+
);
|
|
5227
|
+
const ownershipDistributionFactors2 = [
|
|
5228
|
+
{
|
|
5229
|
+
factorId: "health.ownership.top_author_commit_share",
|
|
5230
|
+
penalty: topAuthorPenalty,
|
|
5231
|
+
rawMetrics: {
|
|
5232
|
+
topAuthorCommitShare: round45(topAuthorCommitShare)
|
|
5233
|
+
},
|
|
5234
|
+
normalizedMetrics: {
|
|
5235
|
+
topAuthorPenalty: round45(topAuthorPenalty),
|
|
5236
|
+
ownershipPenaltyMultiplier: round45(ownershipPenaltyMultiplier),
|
|
5237
|
+
ownershipReliability: round45(ownershipReliability)
|
|
5238
|
+
},
|
|
5239
|
+
weight: 0.35,
|
|
5240
|
+
evidence: [{ kind: "repository_metric", metric: "ownership.topAuthorCommitShare" }]
|
|
5236
5241
|
},
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5242
|
+
{
|
|
5243
|
+
factorId: "health.ownership.files_with_single_contributor_ratio",
|
|
5244
|
+
penalty: singleContributorPenalty,
|
|
5245
|
+
rawMetrics: {
|
|
5246
|
+
filesWithSingleContributorRatio: round45(filesWithSingleContributorRatio)
|
|
5247
|
+
},
|
|
5248
|
+
normalizedMetrics: {
|
|
5249
|
+
singleContributorPenalty: round45(singleContributorPenalty)
|
|
5250
|
+
},
|
|
5251
|
+
weight: 0.25,
|
|
5252
|
+
evidence: [
|
|
5253
|
+
{
|
|
5254
|
+
kind: "repository_metric",
|
|
5255
|
+
metric: "ownership.filesWithSingleContributorRatio"
|
|
5256
|
+
}
|
|
5257
|
+
]
|
|
5246
5258
|
},
|
|
5247
|
-
|
|
5248
|
-
|
|
5259
|
+
{
|
|
5260
|
+
factorId: "health.ownership.author_entropy",
|
|
5261
|
+
penalty: entropyPenalty,
|
|
5262
|
+
rawMetrics: {
|
|
5263
|
+
authorEntropy: round45(authorEntropy)
|
|
5264
|
+
},
|
|
5265
|
+
normalizedMetrics: {
|
|
5266
|
+
authorEntropyPenalty: round45(entropyPenalty)
|
|
5267
|
+
},
|
|
5268
|
+
weight: 0.25,
|
|
5269
|
+
evidence: [{ kind: "repository_metric", metric: "ownership.authorEntropy" }]
|
|
5249
5270
|
},
|
|
5250
|
-
|
|
5251
|
-
|
|
5271
|
+
{
|
|
5272
|
+
factorId: "health.ownership.module_single_author_dominance",
|
|
5273
|
+
penalty: moduleDominancePenalty,
|
|
5274
|
+
rawMetrics: {
|
|
5275
|
+
modulesDominatedBySingleContributorRatio: round45(
|
|
5276
|
+
modulesDominatedBySingleContributorRatio
|
|
5277
|
+
)
|
|
5278
|
+
},
|
|
5279
|
+
normalizedMetrics: {
|
|
5280
|
+
moduleDominancePenalty: round45(moduleDominancePenalty)
|
|
5281
|
+
},
|
|
5282
|
+
weight: 0.15,
|
|
5283
|
+
evidence: [{ kind: "repository_metric", metric: "ownership.moduleDominance" }]
|
|
5284
|
+
}
|
|
5285
|
+
];
|
|
5286
|
+
if (topAuthorPenalty >= 0.35) {
|
|
5287
|
+
pushIssue(issues, {
|
|
5288
|
+
id: "health.ownership.top_author_commit_share",
|
|
5289
|
+
ruleId: "ownership.top_author_share",
|
|
5290
|
+
signal: "ownership.topAuthorCommitShare",
|
|
5291
|
+
dimension: "ownershipDistribution",
|
|
5292
|
+
target: input.structural.targetPath,
|
|
5293
|
+
message: "A single contributor owns most commits, concentrating repository knowledge.",
|
|
5294
|
+
severity: topAuthorPenalty >= 0.75 ? "error" : "warn",
|
|
5295
|
+
evidenceMetrics: {
|
|
5296
|
+
topAuthorCommitShare: round45(topAuthorCommitShare),
|
|
5297
|
+
authorEntropy: round45(authorEntropy)
|
|
5298
|
+
},
|
|
5299
|
+
impact: round45(ownershipDistributionPenalty2 * 0.4)
|
|
5300
|
+
});
|
|
5252
5301
|
}
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
const duplicatedBlockCount = duplication?.duplicatedBlockCount ?? 0;
|
|
5270
|
-
const duplicationFactors = [
|
|
5271
|
-
{
|
|
5272
|
-
factorId: "quality.duplication.line_ratio",
|
|
5273
|
-
penalty: clamp01(duplicatedLineRatio / 0.25),
|
|
5274
|
-
rawMetrics: {
|
|
5275
|
-
duplicatedLineRatio: round45(duplicatedLineRatio)
|
|
5276
|
-
},
|
|
5277
|
-
normalizedMetrics: {
|
|
5278
|
-
duplicatedLineRatioPenalty: round45(clamp01(duplicatedLineRatio / 0.25))
|
|
5279
|
-
},
|
|
5280
|
-
weight: 0.7,
|
|
5281
|
-
evidence: [{ kind: "repository_metric", metric: "duplication.duplicatedLineRatio" }]
|
|
5282
|
-
},
|
|
5283
|
-
{
|
|
5284
|
-
factorId: "quality.duplication.block_count",
|
|
5285
|
-
penalty: logScaled(duplicatedBlockCount, 120),
|
|
5286
|
-
rawMetrics: {
|
|
5287
|
-
duplicatedBlockCount,
|
|
5288
|
-
filesWithDuplication: duplication?.filesWithDuplication ?? 0
|
|
5289
|
-
},
|
|
5290
|
-
normalizedMetrics: {
|
|
5291
|
-
duplicatedBlockPenalty: round45(logScaled(duplicatedBlockCount, 120))
|
|
5292
|
-
},
|
|
5293
|
-
weight: 0.3,
|
|
5294
|
-
evidence: [{ kind: "repository_metric", metric: "duplication.duplicatedBlockCount" }]
|
|
5302
|
+
if (singleContributorPenalty >= 0.35) {
|
|
5303
|
+
pushIssue(issues, {
|
|
5304
|
+
id: "health.ownership.single_author_dominance",
|
|
5305
|
+
ruleId: "ownership.file_dominance",
|
|
5306
|
+
signal: "ownership.filesWithSingleContributorRatio",
|
|
5307
|
+
dimension: "ownershipDistribution",
|
|
5308
|
+
target: input.structural.targetPath,
|
|
5309
|
+
message: "Many files are dominated by a single contributor, reducing change resilience.",
|
|
5310
|
+
evidenceMetrics: {
|
|
5311
|
+
filesWithSingleContributorRatio: round45(filesWithSingleContributorRatio),
|
|
5312
|
+
modulesDominatedBySingleContributorRatio: round45(
|
|
5313
|
+
modulesDominatedBySingleContributorRatio
|
|
5314
|
+
)
|
|
5315
|
+
},
|
|
5316
|
+
impact: round45(ownershipDistributionPenalty2 * 0.35)
|
|
5317
|
+
});
|
|
5295
5318
|
}
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
sourceFiles,
|
|
5330
|
-
testFiles,
|
|
5331
|
-
testRatio: round45(testRatio)
|
|
5332
|
-
},
|
|
5333
|
-
normalizedMetrics: {
|
|
5334
|
-
testPresencePenalty: round45(testPresencePenalty)
|
|
5319
|
+
if (entropyPenalty >= 0.35) {
|
|
5320
|
+
pushIssue(issues, {
|
|
5321
|
+
id: "health.ownership.low_author_entropy",
|
|
5322
|
+
ruleId: "ownership.author_entropy",
|
|
5323
|
+
signal: "ownership.authorEntropy",
|
|
5324
|
+
dimension: "ownershipDistribution",
|
|
5325
|
+
target: input.structural.targetPath,
|
|
5326
|
+
message: "Contributor distribution is narrow across the repository.",
|
|
5327
|
+
evidenceMetrics: {
|
|
5328
|
+
authorEntropy: round45(authorEntropy),
|
|
5329
|
+
topAuthorCommitShare: round45(topAuthorCommitShare)
|
|
5330
|
+
},
|
|
5331
|
+
impact: round45(ownershipDistributionPenalty2 * 0.25)
|
|
5332
|
+
});
|
|
5333
|
+
}
|
|
5334
|
+
const modularityHealth2 = clamp01(1 - modularityPenalty);
|
|
5335
|
+
const changeHygieneHealth2 = clamp01(1 - changeHygienePenalty);
|
|
5336
|
+
const testHealthScore2 = clamp01(1 - testHealthPenalty);
|
|
5337
|
+
const ownershipDistributionHealth2 = clamp01(1 - ownershipDistributionPenalty2);
|
|
5338
|
+
const normalizedScore2 = clamp01(
|
|
5339
|
+
modularityHealth2 * DIMENSION_WEIGHTS.modularity + changeHygieneHealth2 * DIMENSION_WEIGHTS.changeHygiene + testHealthScore2 * DIMENSION_WEIGHTS.testHealth + ownershipDistributionHealth2 * DIMENSION_WEIGHTS.ownershipDistribution
|
|
5340
|
+
);
|
|
5341
|
+
const topIssues2 = [...issues].sort(
|
|
5342
|
+
(a, b) => b.impact - a.impact || a.id.localeCompare(b.id) || a.target.localeCompare(b.target)
|
|
5343
|
+
).slice(0, 12).map(({ impact: _impact, ...issue }) => issue);
|
|
5344
|
+
return {
|
|
5345
|
+
healthScore: toPercentage(normalizedScore2),
|
|
5346
|
+
normalizedScore: round45(normalizedScore2),
|
|
5347
|
+
dimensions: {
|
|
5348
|
+
modularity: toPercentage(modularityHealth2),
|
|
5349
|
+
changeHygiene: toPercentage(changeHygieneHealth2),
|
|
5350
|
+
testHealth: toPercentage(testHealthScore2),
|
|
5351
|
+
ownershipDistribution: toPercentage(ownershipDistributionHealth2)
|
|
5335
5352
|
},
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5353
|
+
topIssues: topIssues2,
|
|
5354
|
+
trace: {
|
|
5355
|
+
schemaVersion: HEALTH_TRACE_VERSION,
|
|
5356
|
+
dimensions: [
|
|
5357
|
+
createDimensionTrace("modularity", modularityHealth2, modularityFactors),
|
|
5358
|
+
createDimensionTrace("changeHygiene", changeHygieneHealth2, changeHygieneFactors),
|
|
5359
|
+
createDimensionTrace("testHealth", testHealthScore2, testHealthFactors),
|
|
5360
|
+
createDimensionTrace(
|
|
5361
|
+
"ownershipDistribution",
|
|
5362
|
+
ownershipDistributionHealth2,
|
|
5363
|
+
ownershipDistributionFactors2
|
|
5364
|
+
)
|
|
5365
|
+
]
|
|
5366
|
+
}
|
|
5367
|
+
};
|
|
5368
|
+
}
|
|
5369
|
+
const ownershipDistributionPenalty = clamp01(0.12 * ownershipPenaltyMultiplier);
|
|
5370
|
+
const ownershipDistributionFactors = [
|
|
5339
5371
|
{
|
|
5340
|
-
factorId: "
|
|
5341
|
-
penalty:
|
|
5372
|
+
factorId: "health.ownership.missing_git_history",
|
|
5373
|
+
penalty: ownershipDistributionPenalty,
|
|
5342
5374
|
rawMetrics: {
|
|
5343
|
-
|
|
5344
|
-
branchCoverage: coverageSignals?.branchCoverage ?? null,
|
|
5345
|
-
functionCoverage: coverageSignals?.functionCoverage ?? null,
|
|
5346
|
-
statementCoverage: coverageSignals?.statementCoverage ?? null
|
|
5375
|
+
gitHistoryAvailable: 0
|
|
5347
5376
|
},
|
|
5348
5377
|
normalizedMetrics: {
|
|
5349
|
-
|
|
5350
|
-
coveragePenalty: round45(coveragePenalty)
|
|
5378
|
+
ownershipDistributionPenalty: round45(ownershipDistributionPenalty)
|
|
5351
5379
|
},
|
|
5352
|
-
weight:
|
|
5353
|
-
evidence: [{ kind: "repository_metric", metric: "
|
|
5380
|
+
weight: 1,
|
|
5381
|
+
evidence: [{ kind: "repository_metric", metric: "evolution.available" }]
|
|
5354
5382
|
}
|
|
5355
5383
|
];
|
|
5356
|
-
const
|
|
5357
|
-
|
|
5358
|
-
);
|
|
5359
|
-
|
|
5360
|
-
pushIssue(issues, {
|
|
5361
|
-
id: "quality.test_health.low_test_presence",
|
|
5362
|
-
ruleId: "tests.file_ratio",
|
|
5363
|
-
dimension: "testHealth",
|
|
5364
|
-
target: input.structural.targetPath,
|
|
5365
|
-
message: `Detected ${testFiles} test file(s) for ${sourceFiles} source file(s).`,
|
|
5366
|
-
severity: testRatio === 0 ? "error" : "warn",
|
|
5367
|
-
impact: round45(testHealthPenalty * 0.4)
|
|
5368
|
-
});
|
|
5369
|
-
}
|
|
5370
|
-
if (coverageRatio !== null && coverageRatio < 0.6) {
|
|
5371
|
-
pushIssue(issues, {
|
|
5372
|
-
id: "quality.test_health.low_coverage",
|
|
5373
|
-
ruleId: "coverage.threshold",
|
|
5374
|
-
dimension: "testHealth",
|
|
5375
|
-
target: input.structural.targetPath,
|
|
5376
|
-
message: `Coverage is below threshold (${toPercentage(coverageRatio)}%).`,
|
|
5377
|
-
impact: round45(testHealthPenalty * 0.35)
|
|
5378
|
-
});
|
|
5379
|
-
}
|
|
5380
|
-
const modularityQuality = clamp01(1 - modularityPenalty);
|
|
5381
|
-
const changeHygieneQuality = clamp01(1 - changeHygienePenalty);
|
|
5382
|
-
const staticAnalysisQuality = clamp01(1 - staticAnalysisPenalty);
|
|
5383
|
-
const complexityQuality = clamp01(1 - complexityPenalty);
|
|
5384
|
-
const duplicationQuality = clamp01(1 - duplicationPenalty);
|
|
5385
|
-
const testHealthQuality = clamp01(1 - testHealthPenalty);
|
|
5384
|
+
const modularityHealth = clamp01(1 - modularityPenalty);
|
|
5385
|
+
const changeHygieneHealth = clamp01(1 - changeHygienePenalty);
|
|
5386
|
+
const testHealthScore = clamp01(1 - testHealthPenalty);
|
|
5387
|
+
const ownershipDistributionHealth = clamp01(1 - ownershipDistributionPenalty);
|
|
5386
5388
|
const normalizedScore = clamp01(
|
|
5387
|
-
|
|
5389
|
+
modularityHealth * DIMENSION_WEIGHTS.modularity + changeHygieneHealth * DIMENSION_WEIGHTS.changeHygiene + testHealthScore * DIMENSION_WEIGHTS.testHealth + ownershipDistributionHealth * DIMENSION_WEIGHTS.ownershipDistribution
|
|
5388
5390
|
);
|
|
5389
5391
|
const topIssues = [...issues].sort(
|
|
5390
5392
|
(a, b) => b.impact - a.impact || a.id.localeCompare(b.id) || a.target.localeCompare(b.target)
|
|
5391
5393
|
).slice(0, 12).map(({ impact: _impact, ...issue }) => issue);
|
|
5392
5394
|
return {
|
|
5393
|
-
|
|
5395
|
+
healthScore: toPercentage(normalizedScore),
|
|
5394
5396
|
normalizedScore: round45(normalizedScore),
|
|
5395
5397
|
dimensions: {
|
|
5396
|
-
modularity: toPercentage(
|
|
5397
|
-
changeHygiene: toPercentage(
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
duplication: toPercentage(duplicationQuality),
|
|
5401
|
-
testHealth: toPercentage(testHealthQuality)
|
|
5398
|
+
modularity: toPercentage(modularityHealth),
|
|
5399
|
+
changeHygiene: toPercentage(changeHygieneHealth),
|
|
5400
|
+
testHealth: toPercentage(testHealthScore),
|
|
5401
|
+
ownershipDistribution: toPercentage(ownershipDistributionHealth)
|
|
5402
5402
|
},
|
|
5403
5403
|
topIssues,
|
|
5404
5404
|
trace: {
|
|
5405
|
-
schemaVersion:
|
|
5405
|
+
schemaVersion: HEALTH_TRACE_VERSION,
|
|
5406
5406
|
dimensions: [
|
|
5407
|
-
createDimensionTrace("modularity",
|
|
5408
|
-
createDimensionTrace("changeHygiene",
|
|
5409
|
-
createDimensionTrace("
|
|
5410
|
-
createDimensionTrace(
|
|
5411
|
-
|
|
5412
|
-
|
|
5407
|
+
createDimensionTrace("modularity", modularityHealth, modularityFactors),
|
|
5408
|
+
createDimensionTrace("changeHygiene", changeHygieneHealth, changeHygieneFactors),
|
|
5409
|
+
createDimensionTrace("testHealth", testHealthScore, testHealthFactors),
|
|
5410
|
+
createDimensionTrace(
|
|
5411
|
+
"ownershipDistribution",
|
|
5412
|
+
ownershipDistributionHealth,
|
|
5413
|
+
ownershipDistributionFactors
|
|
5414
|
+
)
|
|
5413
5415
|
]
|
|
5414
5416
|
}
|
|
5415
5417
|
};
|
|
@@ -6653,8 +6655,8 @@ var evaluateRepositoryRisk = (input, options = {}) => {
|
|
|
6653
6655
|
};
|
|
6654
6656
|
|
|
6655
6657
|
// src/application/run-analyze-command.ts
|
|
6656
|
-
var resolveTargetPath = (inputPath, cwd) =>
|
|
6657
|
-
var
|
|
6658
|
+
var resolveTargetPath = (inputPath, cwd) => resolve3(cwd, inputPath ?? ".");
|
|
6659
|
+
var scoringProfileConfig = {
|
|
6658
6660
|
default: void 0,
|
|
6659
6661
|
personal: {
|
|
6660
6662
|
evolutionFactorWeights: {
|
|
@@ -6666,8 +6668,17 @@ var riskProfileConfig = {
|
|
|
6666
6668
|
}
|
|
6667
6669
|
}
|
|
6668
6670
|
};
|
|
6669
|
-
var
|
|
6670
|
-
|
|
6671
|
+
var healthProfileConfig = {
|
|
6672
|
+
default: void 0,
|
|
6673
|
+
personal: {
|
|
6674
|
+
ownershipPenaltyMultiplier: 0.25
|
|
6675
|
+
}
|
|
6676
|
+
};
|
|
6677
|
+
var resolveRiskConfigForProfile = (scoringProfile) => {
|
|
6678
|
+
return scoringProfileConfig[scoringProfile ?? "default"];
|
|
6679
|
+
};
|
|
6680
|
+
var resolveHealthConfigForProfile = (scoringProfile) => {
|
|
6681
|
+
return healthProfileConfig[scoringProfile ?? "default"];
|
|
6671
6682
|
};
|
|
6672
6683
|
var createExternalProgressReporter = (logger) => {
|
|
6673
6684
|
let lastLoggedProgress = 0;
|
|
@@ -6822,16 +6833,10 @@ var collectAnalysisInputs = async (inputPath, authorIdentityMode, options = {},
|
|
|
6822
6833
|
} else {
|
|
6823
6834
|
logger.warn(`external analysis unavailable: ${external.reason}`);
|
|
6824
6835
|
}
|
|
6825
|
-
logger.info("collecting quality signals");
|
|
6826
|
-
const qualitySignals = await collectQualitySignals(targetPath, structural, logger);
|
|
6827
|
-
logger.debug(
|
|
6828
|
-
`quality signals: todoFixmeCommentCount=${qualitySignals.todoFixmeCommentCount ?? 0}, eslintErrors=${qualitySignals.eslint?.errorCount ?? 0}, tscErrors=${qualitySignals.typescript?.errorCount ?? 0}`
|
|
6829
|
-
);
|
|
6830
6836
|
return {
|
|
6831
6837
|
structural,
|
|
6832
6838
|
evolution,
|
|
6833
|
-
external
|
|
6834
|
-
qualitySignals
|
|
6839
|
+
external
|
|
6835
6840
|
};
|
|
6836
6841
|
};
|
|
6837
6842
|
var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logger = createSilentLogger()) => {
|
|
@@ -6842,32 +6847,33 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logg
|
|
|
6842
6847
|
logger
|
|
6843
6848
|
);
|
|
6844
6849
|
logger.info("computing risk summary");
|
|
6845
|
-
const riskConfig = resolveRiskConfigForProfile(options.
|
|
6850
|
+
const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
|
|
6851
|
+
const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
|
|
6846
6852
|
const risk = computeRepositoryRiskSummary({
|
|
6847
6853
|
structural: analysisInputs.structural,
|
|
6848
6854
|
evolution: analysisInputs.evolution,
|
|
6849
6855
|
external: analysisInputs.external,
|
|
6850
6856
|
...riskConfig === void 0 ? {} : { config: riskConfig }
|
|
6851
6857
|
});
|
|
6852
|
-
const
|
|
6858
|
+
const health = computeRepositoryHealthSummary({
|
|
6853
6859
|
structural: analysisInputs.structural,
|
|
6854
6860
|
evolution: analysisInputs.evolution,
|
|
6855
|
-
|
|
6861
|
+
...healthConfig === void 0 ? {} : { config: healthConfig }
|
|
6856
6862
|
});
|
|
6857
6863
|
logger.info(
|
|
6858
|
-
`analysis completed (riskScore=${risk.riskScore},
|
|
6864
|
+
`analysis completed (riskScore=${risk.riskScore}, healthScore=${health.healthScore})`
|
|
6859
6865
|
);
|
|
6860
6866
|
return {
|
|
6861
6867
|
structural: analysisInputs.structural,
|
|
6862
6868
|
evolution: analysisInputs.evolution,
|
|
6863
6869
|
external: analysisInputs.external,
|
|
6864
6870
|
risk,
|
|
6865
|
-
|
|
6871
|
+
health
|
|
6866
6872
|
};
|
|
6867
6873
|
};
|
|
6868
6874
|
|
|
6869
6875
|
// src/application/run-check-command.ts
|
|
6870
|
-
import { readFile as readFile3, writeFile as
|
|
6876
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
6871
6877
|
|
|
6872
6878
|
// src/application/build-analysis-snapshot.ts
|
|
6873
6879
|
var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logger) => {
|
|
@@ -6879,7 +6885,8 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
|
|
|
6879
6885
|
},
|
|
6880
6886
|
logger
|
|
6881
6887
|
);
|
|
6882
|
-
const riskConfig = resolveRiskConfigForProfile(options.
|
|
6888
|
+
const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
|
|
6889
|
+
const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
|
|
6883
6890
|
const evaluation = evaluateRepositoryRisk(
|
|
6884
6891
|
{
|
|
6885
6892
|
structural: analysisInputs.structural,
|
|
@@ -6894,10 +6901,10 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
|
|
|
6894
6901
|
evolution: analysisInputs.evolution,
|
|
6895
6902
|
external: analysisInputs.external,
|
|
6896
6903
|
risk: evaluation.summary,
|
|
6897
|
-
|
|
6904
|
+
health: computeRepositoryHealthSummary({
|
|
6898
6905
|
structural: analysisInputs.structural,
|
|
6899
6906
|
evolution: analysisInputs.evolution,
|
|
6900
|
-
|
|
6907
|
+
...healthConfig === void 0 ? {} : { config: healthConfig }
|
|
6901
6908
|
})
|
|
6902
6909
|
};
|
|
6903
6910
|
return createSnapshot({
|
|
@@ -6906,7 +6913,7 @@ var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logge
|
|
|
6906
6913
|
analysisConfig: {
|
|
6907
6914
|
authorIdentityMode,
|
|
6908
6915
|
includeTrace: options.includeTrace,
|
|
6909
|
-
|
|
6916
|
+
scoringProfile: options.scoringProfile ?? "default",
|
|
6910
6917
|
recentWindowDays: analysisInputs.evolution.available ? analysisInputs.evolution.metrics.recentWindowDays : options.recentWindowDays ?? null
|
|
6911
6918
|
}
|
|
6912
6919
|
});
|
|
@@ -6941,7 +6948,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
|
|
|
6941
6948
|
authorIdentityMode,
|
|
6942
6949
|
{
|
|
6943
6950
|
includeTrace: options.includeTrace,
|
|
6944
|
-
...options.
|
|
6951
|
+
...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
|
|
6945
6952
|
...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
|
|
6946
6953
|
},
|
|
6947
6954
|
logger
|
|
@@ -6976,7 +6983,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
|
|
|
6976
6983
|
options.outputFormat
|
|
6977
6984
|
);
|
|
6978
6985
|
if (options.outputPath !== void 0) {
|
|
6979
|
-
await
|
|
6986
|
+
await writeFile3(options.outputPath, rendered, "utf8");
|
|
6980
6987
|
logger.info(`check output written: ${options.outputPath}`);
|
|
6981
6988
|
}
|
|
6982
6989
|
return {
|
|
@@ -6989,8 +6996,8 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
|
|
|
6989
6996
|
};
|
|
6990
6997
|
|
|
6991
6998
|
// src/application/run-ci-command.ts
|
|
6992
|
-
import { readFile as readFile4, writeFile as
|
|
6993
|
-
import { relative as
|
|
6999
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
7000
|
+
import { relative as relative2, resolve as resolve4 } from "path";
|
|
6994
7001
|
var isPathOutsideBase = (value) => {
|
|
6995
7002
|
return value === ".." || value.startsWith("../") || value.startsWith("..\\");
|
|
6996
7003
|
};
|
|
@@ -7003,20 +7010,20 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
|
|
|
7003
7010
|
if (options.baselineSha !== void 0 && options.baselineRef !== "auto") {
|
|
7004
7011
|
throw new GovernanceConfigurationError("baseline-sha requires --baseline-ref auto");
|
|
7005
7012
|
}
|
|
7006
|
-
const resolvedTargetPath =
|
|
7013
|
+
const resolvedTargetPath = resolve4(inputPath ?? process.cwd());
|
|
7007
7014
|
logger.info("building current snapshot");
|
|
7008
7015
|
const current = await buildAnalysisSnapshot(
|
|
7009
7016
|
inputPath,
|
|
7010
7017
|
authorIdentityMode,
|
|
7011
7018
|
{
|
|
7012
7019
|
includeTrace: options.includeTrace,
|
|
7013
|
-
...options.
|
|
7020
|
+
...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
|
|
7014
7021
|
...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
|
|
7015
7022
|
},
|
|
7016
7023
|
logger
|
|
7017
7024
|
);
|
|
7018
7025
|
if (options.snapshotPath !== void 0) {
|
|
7019
|
-
await
|
|
7026
|
+
await writeFile4(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
|
|
7020
7027
|
logger.info(`snapshot written: ${options.snapshotPath}`);
|
|
7021
7028
|
}
|
|
7022
7029
|
let baseline;
|
|
@@ -7057,19 +7064,19 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
|
|
|
7057
7064
|
repositoryPath: resolvedTargetPath,
|
|
7058
7065
|
baselineRef,
|
|
7059
7066
|
analyzeWorktree: async (worktreePath, repositoryRoot) => {
|
|
7060
|
-
const relativeTargetPath =
|
|
7067
|
+
const relativeTargetPath = relative2(repositoryRoot, resolvedTargetPath);
|
|
7061
7068
|
if (isPathOutsideBase(relativeTargetPath)) {
|
|
7062
7069
|
throw new GovernanceConfigurationError(
|
|
7063
7070
|
`target path is outside git repository root: ${resolvedTargetPath}`
|
|
7064
7071
|
);
|
|
7065
7072
|
}
|
|
7066
|
-
const baselineTargetPath = relativeTargetPath.length === 0 || relativeTargetPath === "." ? worktreePath :
|
|
7073
|
+
const baselineTargetPath = relativeTargetPath.length === 0 || relativeTargetPath === "." ? worktreePath : resolve4(worktreePath, relativeTargetPath);
|
|
7067
7074
|
return buildAnalysisSnapshot(
|
|
7068
7075
|
baselineTargetPath,
|
|
7069
7076
|
authorIdentityMode,
|
|
7070
7077
|
{
|
|
7071
7078
|
includeTrace: options.includeTrace,
|
|
7072
|
-
...options.
|
|
7079
|
+
...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
|
|
7073
7080
|
...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
|
|
7074
7081
|
},
|
|
7075
7082
|
logger
|
|
@@ -7111,7 +7118,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
|
|
|
7111
7118
|
|
|
7112
7119
|
${ciMarkdown}`;
|
|
7113
7120
|
if (options.reportPath !== void 0) {
|
|
7114
|
-
await
|
|
7121
|
+
await writeFile4(options.reportPath, markdownSummary, "utf8");
|
|
7115
7122
|
logger.info(`report written: ${options.reportPath}`);
|
|
7116
7123
|
}
|
|
7117
7124
|
const machineReadable = {
|
|
@@ -7123,7 +7130,7 @@ ${ciMarkdown}`;
|
|
|
7123
7130
|
exitCode: gateResult.exitCode
|
|
7124
7131
|
};
|
|
7125
7132
|
if (options.jsonOutputPath !== void 0) {
|
|
7126
|
-
await
|
|
7133
|
+
await writeFile4(options.jsonOutputPath, JSON.stringify(machineReadable, null, 2), "utf8");
|
|
7127
7134
|
logger.info(`ci machine output written: ${options.jsonOutputPath}`);
|
|
7128
7135
|
}
|
|
7129
7136
|
return {
|
|
@@ -7137,7 +7144,7 @@ ${ciMarkdown}`;
|
|
|
7137
7144
|
};
|
|
7138
7145
|
|
|
7139
7146
|
// src/application/run-report-command.ts
|
|
7140
|
-
import { readFile as readFile5, writeFile as
|
|
7147
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
7141
7148
|
var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
|
|
7142
7149
|
logger.info("building analysis snapshot");
|
|
7143
7150
|
const current = await buildAnalysisSnapshot(
|
|
@@ -7145,13 +7152,13 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
|
|
|
7145
7152
|
authorIdentityMode,
|
|
7146
7153
|
{
|
|
7147
7154
|
includeTrace: options.includeTrace,
|
|
7148
|
-
...options.
|
|
7155
|
+
...options.scoringProfile === void 0 ? {} : { scoringProfile: options.scoringProfile },
|
|
7149
7156
|
...options.recentWindowDays === void 0 ? {} : { recentWindowDays: options.recentWindowDays }
|
|
7150
7157
|
},
|
|
7151
7158
|
logger
|
|
7152
7159
|
);
|
|
7153
7160
|
if (options.snapshotPath !== void 0) {
|
|
7154
|
-
await
|
|
7161
|
+
await writeFile5(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
|
|
7155
7162
|
logger.info(`snapshot written: ${options.snapshotPath}`);
|
|
7156
7163
|
}
|
|
7157
7164
|
let report;
|
|
@@ -7166,7 +7173,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
|
|
|
7166
7173
|
}
|
|
7167
7174
|
const rendered = formatReport(report, options.format);
|
|
7168
7175
|
if (options.outputPath !== void 0) {
|
|
7169
|
-
await
|
|
7176
|
+
await writeFile5(options.outputPath, rendered, "utf8");
|
|
7170
7177
|
logger.info(`report written: ${options.outputPath}`);
|
|
7171
7178
|
}
|
|
7172
7179
|
return { report, rendered };
|
|
@@ -7202,7 +7209,8 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
|
|
|
7202
7209
|
logger
|
|
7203
7210
|
);
|
|
7204
7211
|
logger.info("computing explainable risk summary");
|
|
7205
|
-
const riskConfig = resolveRiskConfigForProfile(options.
|
|
7212
|
+
const riskConfig = resolveRiskConfigForProfile(options.scoringProfile);
|
|
7213
|
+
const healthConfig = resolveHealthConfigForProfile(options.scoringProfile);
|
|
7206
7214
|
const evaluation = evaluateRepositoryRisk(
|
|
7207
7215
|
{
|
|
7208
7216
|
structural: analysisInputs.structural,
|
|
@@ -7220,14 +7228,14 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
|
|
|
7220
7228
|
evolution: analysisInputs.evolution,
|
|
7221
7229
|
external: analysisInputs.external,
|
|
7222
7230
|
risk: evaluation.summary,
|
|
7223
|
-
|
|
7231
|
+
health: computeRepositoryHealthSummary({
|
|
7224
7232
|
structural: analysisInputs.structural,
|
|
7225
7233
|
evolution: analysisInputs.evolution,
|
|
7226
|
-
|
|
7234
|
+
...healthConfig === void 0 ? {} : { config: healthConfig }
|
|
7227
7235
|
})
|
|
7228
7236
|
};
|
|
7229
7237
|
logger.info(
|
|
7230
|
-
`explanation completed (riskScore=${summary.risk.riskScore},
|
|
7238
|
+
`explanation completed (riskScore=${summary.risk.riskScore}, healthScore=${summary.health.healthScore})`
|
|
7231
7239
|
);
|
|
7232
7240
|
return {
|
|
7233
7241
|
summary,
|
|
@@ -7238,7 +7246,7 @@ var runExplainCommand = async (inputPath, authorIdentityMode, options, logger =
|
|
|
7238
7246
|
|
|
7239
7247
|
// src/index.ts
|
|
7240
7248
|
var program = new Command();
|
|
7241
|
-
var packageJsonPath =
|
|
7249
|
+
var packageJsonPath = resolve5(dirname2(fileURLToPath(import.meta.url)), "../package.json");
|
|
7242
7250
|
var { version } = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
7243
7251
|
var parseRecentWindowDays = (value) => {
|
|
7244
7252
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -7284,7 +7292,7 @@ var renderReportHighlightsText = (report) => {
|
|
|
7284
7292
|
lines.push("Repository Summary");
|
|
7285
7293
|
lines.push(` target: ${report.repository.targetPath}`);
|
|
7286
7294
|
lines.push(` riskScore: ${report.repository.riskScore}`);
|
|
7287
|
-
lines.push(`
|
|
7295
|
+
lines.push(` healthScore: ${report.health.healthScore}`);
|
|
7288
7296
|
lines.push(` normalizedScore: ${report.repository.normalizedScore}`);
|
|
7289
7297
|
lines.push(` riskTier: ${report.repository.riskTier}`);
|
|
7290
7298
|
lines.push("");
|
|
@@ -7300,7 +7308,7 @@ var renderReportHighlightsMarkdown = (report) => {
|
|
|
7300
7308
|
lines.push("## Repository Summary");
|
|
7301
7309
|
lines.push(`- target: \`${report.repository.targetPath}\``);
|
|
7302
7310
|
lines.push(`- riskScore: \`${report.repository.riskScore}\``);
|
|
7303
|
-
lines.push(`-
|
|
7311
|
+
lines.push(`- healthScore: \`${report.health.healthScore}\``);
|
|
7304
7312
|
lines.push(`- normalizedScore: \`${report.repository.normalizedScore}\``);
|
|
7305
7313
|
lines.push(`- riskTier: \`${report.repository.riskTier}\``);
|
|
7306
7314
|
lines.push("");
|
|
@@ -7318,7 +7326,7 @@ var renderCompactText = (report, explainSummary) => {
|
|
|
7318
7326
|
lines.push("Repository");
|
|
7319
7327
|
lines.push(` target: ${report.repository.targetPath}`);
|
|
7320
7328
|
lines.push(` riskScore: ${report.repository.riskScore}`);
|
|
7321
|
-
lines.push(`
|
|
7329
|
+
lines.push(` healthScore: ${report.health.healthScore}`);
|
|
7322
7330
|
lines.push(` riskTier: ${report.repository.riskTier}`);
|
|
7323
7331
|
lines.push(
|
|
7324
7332
|
` dimensions: structural=${report.repository.dimensionScores.structural ?? "n/a"}, evolution=${report.repository.dimensionScores.evolution ?? "n/a"}, external=${report.repository.dimensionScores.external ?? "n/a"}, interactions=${report.repository.dimensionScores.interactions ?? "n/a"}`
|
|
@@ -7344,7 +7352,7 @@ var renderCompactMarkdown = (report, explainSummary) => {
|
|
|
7344
7352
|
lines.push("## Repository");
|
|
7345
7353
|
lines.push(`- target: \`${report.repository.targetPath}\``);
|
|
7346
7354
|
lines.push(`- riskScore: \`${report.repository.riskScore}\``);
|
|
7347
|
-
lines.push(`-
|
|
7355
|
+
lines.push(`- healthScore: \`${report.health.healthScore}\``);
|
|
7348
7356
|
lines.push(`- riskTier: \`${report.repository.riskTier}\``);
|
|
7349
7357
|
lines.push(
|
|
7350
7358
|
`- dimensions: structural=\`${report.repository.dimensionScores.structural ?? "n/a"}\`, evolution=\`${report.repository.dimensionScores.evolution ?? "n/a"}\`, external=\`${report.repository.dimensionScores.external ?? "n/a"}\`, interactions=\`${report.repository.dimensionScores.interactions ?? "n/a"}\``
|
|
@@ -7363,12 +7371,12 @@ var renderCompactMarkdown = (report, explainSummary) => {
|
|
|
7363
7371
|
}
|
|
7364
7372
|
return lines.join("\n");
|
|
7365
7373
|
};
|
|
7366
|
-
var
|
|
7367
|
-
"--
|
|
7368
|
-
"
|
|
7374
|
+
var scoringProfileOption = () => new Option(
|
|
7375
|
+
"--scoring-profile <profile>",
|
|
7376
|
+
"scoring profile: default (balanced) or personal (down-weights single-maintainer ownership penalties for risk and health ownership)"
|
|
7369
7377
|
).choices(["default", "personal"]).default("default");
|
|
7370
7378
|
program.name("codesentinel").description("Structural and evolutionary risk analysis for TypeScript/JavaScript codebases").version(version);
|
|
7371
|
-
program.command("analyze").argument("[path]", "path to the project to analyze").addOption(
|
|
7379
|
+
program.command("analyze").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
|
|
7372
7380
|
new Option(
|
|
7373
7381
|
"--author-identity <mode>",
|
|
7374
7382
|
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
@@ -7391,7 +7399,7 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
|
|
|
7391
7399
|
const summary = await runAnalyzeCommand(
|
|
7392
7400
|
path,
|
|
7393
7401
|
options.authorIdentity,
|
|
7394
|
-
{ recentWindowDays: options.recentWindowDays,
|
|
7402
|
+
{ recentWindowDays: options.recentWindowDays, scoringProfile: options.scoringProfile },
|
|
7395
7403
|
logger
|
|
7396
7404
|
);
|
|
7397
7405
|
const outputMode = options.json === true ? "json" : options.output;
|
|
@@ -7399,7 +7407,7 @@ program.command("analyze").argument("[path]", "path to the project to analyze").
|
|
|
7399
7407
|
`);
|
|
7400
7408
|
}
|
|
7401
7409
|
);
|
|
7402
|
-
program.command("explain").argument("[path]", "path to the project to analyze").addOption(
|
|
7410
|
+
program.command("explain").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
|
|
7403
7411
|
new Option(
|
|
7404
7412
|
"--author-identity <mode>",
|
|
7405
7413
|
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
@@ -7425,7 +7433,7 @@ program.command("explain").argument("[path]", "path to the project to analyze").
|
|
|
7425
7433
|
...options.module === void 0 ? {} : { module: options.module },
|
|
7426
7434
|
top: Number.isFinite(top) ? top : 5,
|
|
7427
7435
|
recentWindowDays: options.recentWindowDays,
|
|
7428
|
-
|
|
7436
|
+
scoringProfile: options.scoringProfile,
|
|
7429
7437
|
format: options.format
|
|
7430
7438
|
};
|
|
7431
7439
|
const result = await runExplainCommand(path, options.authorIdentity, explainOptions, logger);
|
|
@@ -7463,7 +7471,7 @@ program.command("dependency-risk").argument("<dependency>", "dependency spec to
|
|
|
7463
7471
|
`);
|
|
7464
7472
|
}
|
|
7465
7473
|
);
|
|
7466
|
-
program.command("report").argument("[path]", "path to the project to analyze").addOption(
|
|
7474
|
+
program.command("report").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
|
|
7467
7475
|
new Option(
|
|
7468
7476
|
"--author-identity <mode>",
|
|
7469
7477
|
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
@@ -7492,7 +7500,7 @@ program.command("report").argument("[path]", "path to the project to analyze").a
|
|
|
7492
7500
|
...options.compare === void 0 ? {} : { comparePath: options.compare },
|
|
7493
7501
|
...options.snapshot === void 0 ? {} : { snapshotPath: options.snapshot },
|
|
7494
7502
|
includeTrace: options.trace,
|
|
7495
|
-
|
|
7503
|
+
scoringProfile: options.scoringProfile,
|
|
7496
7504
|
recentWindowDays: options.recentWindowDays
|
|
7497
7505
|
},
|
|
7498
7506
|
logger
|
|
@@ -7503,7 +7511,7 @@ program.command("report").argument("[path]", "path to the project to analyze").a
|
|
|
7503
7511
|
}
|
|
7504
7512
|
}
|
|
7505
7513
|
);
|
|
7506
|
-
program.command("run").argument("[path]", "path to the project to analyze").addOption(
|
|
7514
|
+
program.command("run").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
|
|
7507
7515
|
new Option(
|
|
7508
7516
|
"--author-identity <mode>",
|
|
7509
7517
|
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
@@ -7535,7 +7543,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
|
|
|
7535
7543
|
top: Number.isFinite(top) ? top : 5,
|
|
7536
7544
|
format: options.format,
|
|
7537
7545
|
recentWindowDays: options.recentWindowDays,
|
|
7538
|
-
|
|
7546
|
+
scoringProfile: options.scoringProfile
|
|
7539
7547
|
},
|
|
7540
7548
|
logger
|
|
7541
7549
|
);
|
|
@@ -7544,7 +7552,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
|
|
|
7544
7552
|
...options.trace === true ? { trace: explain.trace } : {}
|
|
7545
7553
|
});
|
|
7546
7554
|
if (options.snapshot !== void 0) {
|
|
7547
|
-
await
|
|
7555
|
+
await writeFile6(options.snapshot, JSON.stringify(snapshot, null, 2), "utf8");
|
|
7548
7556
|
logger.info(`snapshot written: ${options.snapshot}`);
|
|
7549
7557
|
}
|
|
7550
7558
|
const report = options.compare === void 0 ? createReport(snapshot) : createReport(
|
|
@@ -7593,7 +7601,7 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
|
|
|
7593
7601
|
},
|
|
7594
7602
|
report: {
|
|
7595
7603
|
repository: report.repository,
|
|
7596
|
-
|
|
7604
|
+
health: report.health,
|
|
7597
7605
|
hotspots: report.hotspots.slice(0, 5),
|
|
7598
7606
|
structural: report.structural,
|
|
7599
7607
|
external: report.external
|
|
@@ -7742,27 +7750,27 @@ var parseMainBranches = (options) => {
|
|
|
7742
7750
|
};
|
|
7743
7751
|
var buildGateConfigFromOptions = (options) => {
|
|
7744
7752
|
const maxRiskDelta = parseGateNumber(options.maxRiskDelta, "--max-risk-delta");
|
|
7745
|
-
const
|
|
7753
|
+
const maxHealthDelta = parseGateNumber(options.maxHealthDelta, "--max-health-delta");
|
|
7746
7754
|
const maxNewHotspots = parseGateNumber(options.maxNewHotspots, "--max-new-hotspots");
|
|
7747
7755
|
const maxRiskScore = parseGateNumber(options.maxRiskScore, "--max-risk-score");
|
|
7748
|
-
const
|
|
7756
|
+
const minHealthScore = parseGateNumber(options.minHealthScore, "--min-health-score");
|
|
7749
7757
|
const newHotspotScoreThreshold = parseGateNumber(
|
|
7750
7758
|
options.newHotspotScoreThreshold,
|
|
7751
7759
|
"--new-hotspot-score-threshold"
|
|
7752
7760
|
);
|
|
7753
7761
|
return {
|
|
7754
7762
|
...maxRiskDelta === void 0 ? {} : { maxRiskDelta },
|
|
7755
|
-
...
|
|
7763
|
+
...maxHealthDelta === void 0 ? {} : { maxHealthDelta },
|
|
7756
7764
|
...options.noNewCycles === true ? { noNewCycles: true } : {},
|
|
7757
7765
|
...options.noNewHighRiskDeps === true ? { noNewHighRiskDeps: true } : {},
|
|
7758
7766
|
...maxNewHotspots === void 0 ? {} : { maxNewHotspots },
|
|
7759
7767
|
...maxRiskScore === void 0 ? {} : { maxRiskScore },
|
|
7760
|
-
...
|
|
7768
|
+
...minHealthScore === void 0 ? {} : { minHealthScore },
|
|
7761
7769
|
...newHotspotScoreThreshold === void 0 ? {} : { newHotspotScoreThreshold },
|
|
7762
7770
|
failOn: options.failOn
|
|
7763
7771
|
};
|
|
7764
7772
|
};
|
|
7765
|
-
program.command("check").argument("[path]", "path to the project to analyze").addOption(
|
|
7773
|
+
program.command("check").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
|
|
7766
7774
|
new Option(
|
|
7767
7775
|
"--author-identity <mode>",
|
|
7768
7776
|
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
@@ -7773,9 +7781,9 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
|
|
|
7773
7781
|
"log verbosity: silent, error, warn, info, debug (logs are written to stderr)"
|
|
7774
7782
|
).choices(["silent", "error", "warn", "info", "debug"]).default(parseLogLevel(process.env["CODESENTINEL_LOG_LEVEL"]))
|
|
7775
7783
|
).option("--compare <baseline>", "baseline snapshot path").option("--max-risk-delta <value>", "maximum allowed normalized risk score increase").option(
|
|
7776
|
-
"--max-
|
|
7777
|
-
"maximum allowed normalized
|
|
7778
|
-
).option("--no-new-cycles", "fail if new structural cycles are introduced").option("--no-new-high-risk-deps", "fail if new high-risk direct dependencies are introduced").option("--max-new-hotspots <count>", "maximum allowed number of new hotspots").option("--new-hotspot-score-threshold <score>", "minimum hotspot score to count as new hotspot").option("--max-risk-score <score>", "absolute risk score limit (0..100)").option("--min-
|
|
7784
|
+
"--max-health-delta <value>",
|
|
7785
|
+
"maximum allowed normalized health score regression versus baseline (requires --compare)"
|
|
7786
|
+
).option("--no-new-cycles", "fail if new structural cycles are introduced").option("--no-new-high-risk-deps", "fail if new high-risk direct dependencies are introduced").option("--max-new-hotspots <count>", "maximum allowed number of new hotspots").option("--new-hotspot-score-threshold <score>", "minimum hotspot score to count as new hotspot").option("--max-risk-score <score>", "absolute risk score limit (0..100)").option("--min-health-score <score>", "minimum health score threshold (0..100)").addOption(
|
|
7779
7787
|
new Option("--fail-on <level>", "failing severity threshold").choices(["error", "warn"]).default("error")
|
|
7780
7788
|
).addOption(
|
|
7781
7789
|
new Option("--format <mode>", "output format: text, json, md").choices(["text", "json", "md"]).default("text")
|
|
@@ -7795,7 +7803,7 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
|
|
|
7795
7803
|
{
|
|
7796
7804
|
...options.compare === void 0 ? {} : { baselinePath: options.compare },
|
|
7797
7805
|
includeTrace: options.trace,
|
|
7798
|
-
|
|
7806
|
+
scoringProfile: options.scoringProfile,
|
|
7799
7807
|
recentWindowDays: options.recentWindowDays,
|
|
7800
7808
|
gateConfig,
|
|
7801
7809
|
outputFormat: options.format,
|
|
@@ -7819,7 +7827,7 @@ program.command("check").argument("[path]", "path to the project to analyze").ad
|
|
|
7819
7827
|
}
|
|
7820
7828
|
}
|
|
7821
7829
|
);
|
|
7822
|
-
program.command("ci").argument("[path]", "path to the project to analyze").addOption(
|
|
7830
|
+
program.command("ci").argument("[path]", "path to the project to analyze").addOption(scoringProfileOption()).addOption(
|
|
7823
7831
|
new Option(
|
|
7824
7832
|
"--author-identity <mode>",
|
|
7825
7833
|
"author identity mode: likely_merge (heuristic) or strict_email (deterministic)"
|
|
@@ -7844,9 +7852,9 @@ program.command("ci").argument("[path]", "path to the project to analyze").addOp
|
|
|
7844
7852
|
"--main-branches <names>",
|
|
7845
7853
|
"comma-separated default branch candidates for auto baseline resolution (for example: main,master)"
|
|
7846
7854
|
).option("--snapshot <path>", "write current snapshot JSON to path").option("--report <path>", "write markdown CI summary report").option("--json-output <path>", "write machine-readable CI JSON output").option("--max-risk-delta <value>", "maximum allowed normalized risk score increase").option(
|
|
7847
|
-
"--max-
|
|
7848
|
-
"maximum allowed normalized
|
|
7849
|
-
).option("--no-new-cycles", "fail if new structural cycles are introduced").option("--no-new-high-risk-deps", "fail if new high-risk direct dependencies are introduced").option("--max-new-hotspots <count>", "maximum allowed number of new hotspots").option("--new-hotspot-score-threshold <score>", "minimum hotspot score to count as new hotspot").option("--max-risk-score <score>", "absolute risk score limit (0..100)").option("--min-
|
|
7855
|
+
"--max-health-delta <value>",
|
|
7856
|
+
"maximum allowed normalized health score regression versus baseline"
|
|
7857
|
+
).option("--no-new-cycles", "fail if new structural cycles are introduced").option("--no-new-high-risk-deps", "fail if new high-risk direct dependencies are introduced").option("--max-new-hotspots <count>", "maximum allowed number of new hotspots").option("--new-hotspot-score-threshold <score>", "minimum hotspot score to count as new hotspot").option("--max-risk-score <score>", "absolute risk score limit (0..100)").option("--min-health-score <score>", "minimum health score threshold (0..100)").addOption(
|
|
7850
7858
|
new Option("--fail-on <level>", "failing severity threshold").choices(["error", "warn"]).default("error")
|
|
7851
7859
|
).option("--no-trace", "disable trace embedding in generated snapshot").addOption(
|
|
7852
7860
|
new Option(
|
|
@@ -7871,7 +7879,7 @@ program.command("ci").argument("[path]", "path to the project to analyze").addOp
|
|
|
7871
7879
|
...options.report === void 0 ? {} : { reportPath: options.report },
|
|
7872
7880
|
...options.jsonOutput === void 0 ? {} : { jsonOutputPath: options.jsonOutput },
|
|
7873
7881
|
includeTrace: options.trace,
|
|
7874
|
-
|
|
7882
|
+
scoringProfile: options.scoringProfile,
|
|
7875
7883
|
recentWindowDays: options.recentWindowDays,
|
|
7876
7884
|
gateConfig
|
|
7877
7885
|
},
|