@getcodesentinel/codesentinel 1.17.0 → 1.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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 fetchJsonWithRetry(`https://registry.npmjs.org/${encodedName}`, {
899
- retries: MAX_RETRIES,
900
- baseDelayMs: RETRY_BASE_DELAY_MS
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 versions = packument.versions ?? {};
908
- const versionKeys = Object.keys(versions);
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 && versions[requested] !== void 0) {
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 && versions[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 && versions[matched] !== void 0) {
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 && versions[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 manifest = (packument.versions ?? {})[resolved.version] ?? {};
1020
- const dependencies = Object.entries(manifest.dependencies ?? {}).filter(
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 ONE_DAY_MS = 24 * 60 * 60 * 1e3;
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 fetchJsonWithRetry(
1320
- `https://api.npmjs.org/downloads/point/last-week/${encodedName}`,
1321
- { retries: MAX_RETRIES2, baseDelayMs: RETRY_BASE_DELAY_MS2 }
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 fetchJsonWithRetry(
1340
- `https://registry.npmjs.org/${encodedName}`,
1341
- { retries: MAX_RETRIES2, baseDelayMs: RETRY_BASE_DELAY_MS2 }
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) / ONE_DAY_MS));
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 / ONE_DAY_MS);
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;
@@ -1417,6 +1700,21 @@ var toRiskTier = (score) => {
1417
1700
  }
1418
1701
  return "very_high";
1419
1702
  };
1703
+ var toHealthTier = (score) => {
1704
+ if (score < 20) {
1705
+ return "critical";
1706
+ }
1707
+ if (score < 40) {
1708
+ return "weak";
1709
+ }
1710
+ if (score < 60) {
1711
+ return "fair";
1712
+ }
1713
+ if (score < 80) {
1714
+ return "good";
1715
+ }
1716
+ return "excellent";
1717
+ };
1420
1718
  var factorLabelById = {
1421
1719
  "repository.structural": "Structural complexity",
1422
1720
  "repository.evolution": "Change volatility",
@@ -1653,6 +1951,7 @@ var createReport = (snapshot, diff) => {
1653
1951
  riskScore: snapshot.analysis.risk.riskScore,
1654
1952
  normalizedScore: snapshot.analysis.risk.normalizedScore,
1655
1953
  riskTier: toRiskTier(snapshot.analysis.risk.riskScore),
1954
+ healthTier: toHealthTier(snapshot.analysis.health.healthScore),
1656
1955
  confidence: repositoryConfidence(snapshot),
1657
1956
  dimensionScores: repositoryDimensionScores(snapshot)
1658
1957
  },
@@ -1720,6 +2019,7 @@ var renderTextReport = (report) => {
1720
2019
  lines.push(` riskScore: ${report.repository.riskScore}`);
1721
2020
  lines.push(` normalizedScore: ${report.repository.normalizedScore}`);
1722
2021
  lines.push(` riskTier: ${report.repository.riskTier}`);
2022
+ lines.push(` healthTier: ${report.repository.healthTier}`);
1723
2023
  lines.push(` confidence: ${report.repository.confidence ?? "n/a"}`);
1724
2024
  lines.push("");
1725
2025
  lines.push("Dimension Scores (0-100)");
@@ -1813,6 +2113,7 @@ var renderMarkdownReport = (report) => {
1813
2113
  lines.push(`- riskScore: \`${report.repository.riskScore}\``);
1814
2114
  lines.push(`- normalizedScore: \`${report.repository.normalizedScore}\``);
1815
2115
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
2116
+ lines.push(`- healthTier: \`${report.repository.healthTier}\``);
1816
2117
  lines.push(`- confidence: \`${report.repository.confidence ?? "n/a"}\``);
1817
2118
  lines.push("");
1818
2119
  lines.push("## Dimension Scores (0-100)");
@@ -1925,7 +2226,7 @@ var formatReport = (report, format) => {
1925
2226
 
1926
2227
  // ../governance/dist/index.js
1927
2228
  import { mkdirSync, rmSync } from "fs";
1928
- import { basename, join as join2, resolve } from "path";
2229
+ import { basename, join as join5, resolve } from "path";
1929
2230
  import { execFile } from "child_process";
1930
2231
  import { promisify } from "util";
1931
2232
  var EXIT_CODES = {
@@ -2446,7 +2747,7 @@ var tryRunGit = async (repositoryPath, args) => {
2446
2747
  }
2447
2748
  };
2448
2749
  var buildWorktreePath = (repoRoot, sha) => {
2449
- const tmpRoot = join2(repoRoot, SENTINEL_TMP_DIR, WORKTREE_DIR);
2750
+ const tmpRoot = join5(repoRoot, SENTINEL_TMP_DIR, WORKTREE_DIR);
2450
2751
  mkdirSync(tmpRoot, { recursive: true });
2451
2752
  const baseName = `baseline-${sha.slice(0, 12)}-${process.pid}`;
2452
2753
  const candidate = resolve(tmpRoot, baseName);
@@ -2538,11 +2839,26 @@ var resolveAutoBaselineRef = async (input) => {
2538
2839
 
2539
2840
  // src/index.ts
2540
2841
  import { readFileSync as readFileSync2 } from "fs";
2541
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
2842
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
2542
2843
  import { dirname as dirname2, resolve as resolve5 } from "path";
2543
2844
  import { fileURLToPath } from "url";
2544
2845
 
2545
2846
  // src/application/format-analyze-output.ts
2847
+ var toHealthTier2 = (score) => {
2848
+ if (score < 20) {
2849
+ return "critical";
2850
+ }
2851
+ if (score < 40) {
2852
+ return "weak";
2853
+ }
2854
+ if (score < 60) {
2855
+ return "fair";
2856
+ }
2857
+ if (score < 80) {
2858
+ return "good";
2859
+ }
2860
+ return "excellent";
2861
+ };
2546
2862
  var createSummaryShape = (summary) => ({
2547
2863
  targetPath: summary.structural.targetPath,
2548
2864
  structural: summary.structural.metrics,
@@ -2582,6 +2898,7 @@ var createSummaryShape = (summary) => ({
2582
2898
  },
2583
2899
  health: {
2584
2900
  healthScore: summary.health.healthScore,
2901
+ healthTier: toHealthTier2(summary.health.healthScore),
2585
2902
  normalizedScore: summary.health.normalizedScore,
2586
2903
  dimensions: summary.health.dimensions,
2587
2904
  topIssues: summary.health.topIssues.slice(0, 5)
@@ -2931,13 +3248,13 @@ var parseLogLevel = (value) => {
2931
3248
 
2932
3249
  // src/application/check-for-updates.ts
2933
3250
  import { spawn } from "child_process";
2934
- import { mkdir, readFile, writeFile } from "fs/promises";
2935
- import { homedir } from "os";
2936
- import { dirname, join as join3 } from "path";
3251
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
3252
+ import { homedir as homedir2 } from "os";
3253
+ import { dirname, join as join6 } from "path";
2937
3254
  import { stderr, stdin } from "process";
2938
3255
  import { clearScreenDown, cursorTo, emitKeypressEvents } from "readline";
2939
3256
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2940
- var UPDATE_CACHE_PATH = join3(homedir(), ".cache", "codesentinel", "update-check.json");
3257
+ var UPDATE_CACHE_PATH = join6(homedir2(), ".cache", "codesentinel", "update-check.json");
2941
3258
  var SEMVER_PATTERN = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<prerelease>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
2942
3259
  var ANSI = {
2943
3260
  reset: "\x1B[0m",
@@ -3061,7 +3378,7 @@ var parseNpmViewVersionOutput = (output) => {
3061
3378
  };
3062
3379
  var readCache = async () => {
3063
3380
  try {
3064
- const raw = await readFile(UPDATE_CACHE_PATH, "utf8");
3381
+ const raw = await readFile2(UPDATE_CACHE_PATH, "utf8");
3065
3382
  const parsed = JSON.parse(raw);
3066
3383
  if (typeof parsed === "object" && parsed !== null && typeof parsed.lastCheckedAt === "string") {
3067
3384
  return { lastCheckedAt: parsed.lastCheckedAt };
@@ -3072,8 +3389,8 @@ var readCache = async () => {
3072
3389
  return null;
3073
3390
  };
3074
3391
  var writeCache = async (cache) => {
3075
- await mkdir(dirname(UPDATE_CACHE_PATH), { recursive: true });
3076
- await writeFile(UPDATE_CACHE_PATH, JSON.stringify(cache), "utf8");
3392
+ await mkdir2(dirname(UPDATE_CACHE_PATH), { recursive: true });
3393
+ await writeFile2(UPDATE_CACHE_PATH, JSON.stringify(cache), "utf8");
3077
3394
  };
3078
3395
  var shouldRunUpdateCheck = (input) => {
3079
3396
  if (!input.isInteractive) {
@@ -6590,7 +6907,7 @@ var runAnalyzeCommand = async (inputPath, authorIdentityMode, options = {}, logg
6590
6907
  };
6591
6908
 
6592
6909
  // src/application/run-check-command.ts
6593
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
6910
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
6594
6911
 
6595
6912
  // src/application/build-analysis-snapshot.ts
6596
6913
  var buildAnalysisSnapshot = async (inputPath, authorIdentityMode, options, logger) => {
@@ -6674,7 +6991,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6674
6991
  let diff;
6675
6992
  if (options.baselinePath !== void 0) {
6676
6993
  logger.info(`loading baseline snapshot: ${options.baselinePath}`);
6677
- const baselineRaw = await readFile2(options.baselinePath, "utf8");
6994
+ const baselineRaw = await readFile3(options.baselinePath, "utf8");
6678
6995
  try {
6679
6996
  baseline = parseSnapshot(baselineRaw);
6680
6997
  } catch (error) {
@@ -6700,7 +7017,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6700
7017
  options.outputFormat
6701
7018
  );
6702
7019
  if (options.outputPath !== void 0) {
6703
- await writeFile2(options.outputPath, rendered, "utf8");
7020
+ await writeFile3(options.outputPath, rendered, "utf8");
6704
7021
  logger.info(`check output written: ${options.outputPath}`);
6705
7022
  }
6706
7023
  return {
@@ -6713,7 +7030,7 @@ var runCheckCommand = async (inputPath, authorIdentityMode, options, logger = cr
6713
7030
  };
6714
7031
 
6715
7032
  // src/application/run-ci-command.ts
6716
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
7033
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
6717
7034
  import { relative as relative2, resolve as resolve4 } from "path";
6718
7035
  var isPathOutsideBase = (value) => {
6719
7036
  return value === ".." || value.startsWith("../") || value.startsWith("..\\");
@@ -6740,7 +7057,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
6740
7057
  logger
6741
7058
  );
6742
7059
  if (options.snapshotPath !== void 0) {
6743
- await writeFile3(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
7060
+ await writeFile4(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
6744
7061
  logger.info(`snapshot written: ${options.snapshotPath}`);
6745
7062
  }
6746
7063
  let baseline;
@@ -6813,7 +7130,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
6813
7130
  diff = compareSnapshots(current, baseline);
6814
7131
  } else if (options.baselinePath !== void 0) {
6815
7132
  logger.info(`loading baseline snapshot: ${options.baselinePath}`);
6816
- const baselineRaw = await readFile3(options.baselinePath, "utf8");
7133
+ const baselineRaw = await readFile4(options.baselinePath, "utf8");
6817
7134
  try {
6818
7135
  baseline = parseSnapshot(baselineRaw);
6819
7136
  } catch (error) {
@@ -6835,7 +7152,7 @@ var runCiCommand = async (inputPath, authorIdentityMode, options, logger = creat
6835
7152
 
6836
7153
  ${ciMarkdown}`;
6837
7154
  if (options.reportPath !== void 0) {
6838
- await writeFile3(options.reportPath, markdownSummary, "utf8");
7155
+ await writeFile4(options.reportPath, markdownSummary, "utf8");
6839
7156
  logger.info(`report written: ${options.reportPath}`);
6840
7157
  }
6841
7158
  const machineReadable = {
@@ -6847,7 +7164,7 @@ ${ciMarkdown}`;
6847
7164
  exitCode: gateResult.exitCode
6848
7165
  };
6849
7166
  if (options.jsonOutputPath !== void 0) {
6850
- await writeFile3(options.jsonOutputPath, JSON.stringify(machineReadable, null, 2), "utf8");
7167
+ await writeFile4(options.jsonOutputPath, JSON.stringify(machineReadable, null, 2), "utf8");
6851
7168
  logger.info(`ci machine output written: ${options.jsonOutputPath}`);
6852
7169
  }
6853
7170
  return {
@@ -6861,7 +7178,7 @@ ${ciMarkdown}`;
6861
7178
  };
6862
7179
 
6863
7180
  // src/application/run-report-command.ts
6864
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
7181
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
6865
7182
  var runReportCommand = async (inputPath, authorIdentityMode, options, logger = createSilentLogger()) => {
6866
7183
  logger.info("building analysis snapshot");
6867
7184
  const current = await buildAnalysisSnapshot(
@@ -6875,7 +7192,7 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
6875
7192
  logger
6876
7193
  );
6877
7194
  if (options.snapshotPath !== void 0) {
6878
- await writeFile4(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
7195
+ await writeFile5(options.snapshotPath, JSON.stringify(current, null, 2), "utf8");
6879
7196
  logger.info(`snapshot written: ${options.snapshotPath}`);
6880
7197
  }
6881
7198
  let report;
@@ -6883,14 +7200,14 @@ var runReportCommand = async (inputPath, authorIdentityMode, options, logger = c
6883
7200
  report = createReport(current);
6884
7201
  } else {
6885
7202
  logger.info(`loading baseline snapshot: ${options.comparePath}`);
6886
- const baselineRaw = await readFile4(options.comparePath, "utf8");
7203
+ const baselineRaw = await readFile5(options.comparePath, "utf8");
6887
7204
  const baseline = parseSnapshot(baselineRaw);
6888
7205
  const diff = compareSnapshots(current, baseline);
6889
7206
  report = createReport(current, diff);
6890
7207
  }
6891
7208
  const rendered = formatReport(report, options.format);
6892
7209
  if (options.outputPath !== void 0) {
6893
- await writeFile4(options.outputPath, rendered, "utf8");
7210
+ await writeFile5(options.outputPath, rendered, "utf8");
6894
7211
  logger.info(`report written: ${options.outputPath}`);
6895
7212
  }
6896
7213
  return { report, rendered };
@@ -7012,6 +7329,7 @@ var renderReportHighlightsText = (report) => {
7012
7329
  lines.push(` healthScore: ${report.health.healthScore}`);
7013
7330
  lines.push(` normalizedScore: ${report.repository.normalizedScore}`);
7014
7331
  lines.push(` riskTier: ${report.repository.riskTier}`);
7332
+ lines.push(` healthTier: ${report.repository.healthTier}`);
7015
7333
  lines.push("");
7016
7334
  lines.push("Top Hotspots");
7017
7335
  for (const hotspot of report.hotspots.slice(0, 5)) {
@@ -7028,6 +7346,7 @@ var renderReportHighlightsMarkdown = (report) => {
7028
7346
  lines.push(`- healthScore: \`${report.health.healthScore}\``);
7029
7347
  lines.push(`- normalizedScore: \`${report.repository.normalizedScore}\``);
7030
7348
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
7349
+ lines.push(`- healthTier: \`${report.repository.healthTier}\``);
7031
7350
  lines.push("");
7032
7351
  lines.push("## Top Hotspots");
7033
7352
  for (const hotspot of report.hotspots.slice(0, 5)) {
@@ -7045,6 +7364,7 @@ var renderCompactText = (report, explainSummary) => {
7045
7364
  lines.push(` riskScore: ${report.repository.riskScore}`);
7046
7365
  lines.push(` healthScore: ${report.health.healthScore}`);
7047
7366
  lines.push(` riskTier: ${report.repository.riskTier}`);
7367
+ lines.push(` healthTier: ${report.repository.healthTier}`);
7048
7368
  lines.push(
7049
7369
  ` 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"}`
7050
7370
  );
@@ -7071,6 +7391,7 @@ var renderCompactMarkdown = (report, explainSummary) => {
7071
7391
  lines.push(`- riskScore: \`${report.repository.riskScore}\``);
7072
7392
  lines.push(`- healthScore: \`${report.health.healthScore}\``);
7073
7393
  lines.push(`- riskTier: \`${report.repository.riskTier}\``);
7394
+ lines.push(`- healthTier: \`${report.repository.healthTier}\``);
7074
7395
  lines.push(
7075
7396
  `- 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"}\``
7076
7397
  );
@@ -7269,12 +7590,12 @@ program.command("run").argument("[path]", "path to the project to analyze").addO
7269
7590
  ...options.trace === true ? { trace: explain.trace } : {}
7270
7591
  });
7271
7592
  if (options.snapshot !== void 0) {
7272
- await writeFile5(options.snapshot, JSON.stringify(snapshot, null, 2), "utf8");
7593
+ await writeFile6(options.snapshot, JSON.stringify(snapshot, null, 2), "utf8");
7273
7594
  logger.info(`snapshot written: ${options.snapshot}`);
7274
7595
  }
7275
7596
  const report = options.compare === void 0 ? createReport(snapshot) : createReport(
7276
7597
  snapshot,
7277
- compareSnapshots(snapshot, parseSnapshot(await readFile5(options.compare, "utf8")))
7598
+ compareSnapshots(snapshot, parseSnapshot(await readFile6(options.compare, "utf8")))
7278
7599
  );
7279
7600
  if (options.format === "json") {
7280
7601
  const analyzeSummaryOutput = formatAnalyzeOutput(explain.summary, "summary");