@getcodesentinel/codesentinel 1.17.2 → 1.17.4
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 -11
- package/dist/index.js +82 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -419,12 +419,12 @@ Score direction:
|
|
|
419
419
|
- Report views also include derived tiers: `riskTier` and `healthTier`.
|
|
420
420
|
- `health.trace`: per-dimension factor traces with normalized metrics and evidence.
|
|
421
421
|
|
|
422
|
-
Health
|
|
422
|
+
Health dimensions:
|
|
423
423
|
|
|
424
|
-
- `modularity
|
|
425
|
-
- `changeHygiene
|
|
426
|
-
- `testHealth
|
|
427
|
-
- `ownershipDistribution
|
|
424
|
+
- `modularity`: cycle density + fan/centrality concentration + structural-hotspot overlap.
|
|
425
|
+
- `changeHygiene`: churn/volatility concentration + dense co-change clusters.
|
|
426
|
+
- `testHealth`: test presence + test-to-source ratio + testing directory presence.
|
|
427
|
+
- `ownershipDistribution`: top-author share + author entropy + single-author dominance signals.
|
|
428
428
|
|
|
429
429
|
Signal ingestion (deterministic, local):
|
|
430
430
|
|
|
@@ -438,18 +438,30 @@ Interpretation notes:
|
|
|
438
438
|
- Scores are meant for within-repo prioritization and trend tracking.
|
|
439
439
|
- Full model details and limits are in `packages/risk-engine/README.md`.
|
|
440
440
|
|
|
441
|
-
### Score Guide
|
|
441
|
+
### Risk Score Guide
|
|
442
442
|
|
|
443
443
|
Use these ranges as operational guidance:
|
|
444
444
|
|
|
445
|
-
- `0-20`: low fragility.
|
|
446
|
-
- `20-40`: moderate fragility.
|
|
447
|
-
- `40-60`: elevated fragility (prioritize top hotspots).
|
|
448
|
-
- `60-80`: high fragility (expect higher change
|
|
449
|
-
- `80-100`: very high fragility (
|
|
445
|
+
- `0-20`: low fragility (architectural and change pressure signals are generally contained).
|
|
446
|
+
- `20-40`: moderate fragility (localized hotspots exist; monitor trend direction and concentration).
|
|
447
|
+
- `40-60`: elevated fragility (prioritize top hotspots before introducing major concurrent change).
|
|
448
|
+
- `60-80`: high fragility (expect higher coordination cost, regressions, and change coupling across areas).
|
|
449
|
+
- `80-100`: very high fragility (treat as immediate triage; focus on stabilization before further expansion).
|
|
450
450
|
|
|
451
451
|
These ranges are heuristics for triage, not incident probability.
|
|
452
452
|
|
|
453
|
+
### Health Score Guide
|
|
454
|
+
|
|
455
|
+
Use these ranges as operational guidance:
|
|
456
|
+
|
|
457
|
+
- `0-20`: critical health posture (maintainability pressure is highly concentrated and debt is likely compounding).
|
|
458
|
+
- `20-40`: weak health posture (key maintainability bottlenecks are visible; prioritize stabilization work).
|
|
459
|
+
- `40-60`: fair health posture (baseline is workable, but concentrated architecture/change pressure can still slow delivery).
|
|
460
|
+
- `60-80`: good health posture (most maintainability signals are stable, with targeted improvements still valuable).
|
|
461
|
+
- `80-100`: excellent health posture (maintainability pressure is broadly distributed and sustainably controlled over time).
|
|
462
|
+
|
|
463
|
+
These ranges are heuristics for prioritization, not absolute quality guarantees.
|
|
464
|
+
|
|
453
465
|
### What Moves Scores
|
|
454
466
|
|
|
455
467
|
`risk.riskScore` and `risk.fileScores[*].score` increase when:
|
package/dist/index.js
CHANGED
|
@@ -703,6 +703,14 @@ var DEFAULT_OPTIONS = {
|
|
|
703
703
|
maxEntryBytes: 4 * 1024 * 1024,
|
|
704
704
|
sweepIntervalWrites: 25
|
|
705
705
|
};
|
|
706
|
+
var DEFAULT_BUCKET = "default";
|
|
707
|
+
var normalizeBucket = (value) => {
|
|
708
|
+
const trimmed = value.trim().toLowerCase();
|
|
709
|
+
if (trimmed.length === 0) {
|
|
710
|
+
return DEFAULT_BUCKET;
|
|
711
|
+
}
|
|
712
|
+
return trimmed.replace(/[^a-z0-9_-]/g, "_");
|
|
713
|
+
};
|
|
706
714
|
var FileCacheStore = class {
|
|
707
715
|
constructor(directoryPath, options = DEFAULT_OPTIONS) {
|
|
708
716
|
this.directoryPath = directoryPath;
|
|
@@ -712,13 +720,22 @@ var FileCacheStore = class {
|
|
|
712
720
|
inFlightWrites = /* @__PURE__ */ new Map();
|
|
713
721
|
writesSinceSweep = 0;
|
|
714
722
|
sweepPromise = Promise.resolve();
|
|
723
|
+
bucketForKey(key) {
|
|
724
|
+
const configured = this.options.bucketForKey?.(key);
|
|
725
|
+
if (configured === void 0) {
|
|
726
|
+
return DEFAULT_BUCKET;
|
|
727
|
+
}
|
|
728
|
+
return normalizeBucket(configured);
|
|
729
|
+
}
|
|
715
730
|
toEntryPath(key) {
|
|
731
|
+
const bucket = this.bucketForKey(key);
|
|
716
732
|
const digest = createHash("sha256").update(key).digest("hex");
|
|
717
|
-
return join3(this.directoryPath, `${digest}.json`);
|
|
733
|
+
return join3(this.directoryPath, bucket, `${digest}.json`);
|
|
718
734
|
}
|
|
719
735
|
async writeEntry(key, entry) {
|
|
720
736
|
const filePath = this.toEntryPath(key);
|
|
721
|
-
|
|
737
|
+
const bucketPath = join3(this.directoryPath, this.bucketForKey(key));
|
|
738
|
+
await mkdir(bucketPath, { recursive: true });
|
|
722
739
|
const tempPath = `${filePath}.tmp`;
|
|
723
740
|
const payload = {
|
|
724
741
|
key,
|
|
@@ -745,34 +762,74 @@ var FileCacheStore = class {
|
|
|
745
762
|
await this.sweepPromise;
|
|
746
763
|
}
|
|
747
764
|
async evictToSizeLimit() {
|
|
765
|
+
const nowMs = Date.now();
|
|
748
766
|
let entries;
|
|
749
767
|
try {
|
|
750
|
-
const
|
|
768
|
+
const bucketEntries = await readdir(this.directoryPath, { withFileTypes: true });
|
|
769
|
+
const bucketNames = bucketEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
751
770
|
entries = (await Promise.all(
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
return
|
|
771
|
+
bucketNames.map(async (bucket) => {
|
|
772
|
+
const bucketPath = join3(this.directoryPath, bucket);
|
|
773
|
+
const files = await readdir(bucketPath, { withFileTypes: true });
|
|
774
|
+
return await Promise.all(
|
|
775
|
+
files.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map(async (entry) => {
|
|
776
|
+
const path = join3(bucketPath, entry.name);
|
|
777
|
+
const info = await stat(path);
|
|
778
|
+
return {
|
|
779
|
+
path,
|
|
780
|
+
size: info.size,
|
|
781
|
+
mtimeMs: info.mtimeMs,
|
|
782
|
+
bucket
|
|
783
|
+
};
|
|
784
|
+
})
|
|
785
|
+
);
|
|
756
786
|
})
|
|
757
|
-
)).filter((entry) => Number.isFinite(entry.size) && entry.size > 0);
|
|
787
|
+
)).flat().filter((entry) => Number.isFinite(entry.size) && entry.size > 0);
|
|
758
788
|
} catch {
|
|
759
789
|
return;
|
|
760
790
|
}
|
|
791
|
+
if (this.options.maxAgeMsByBucket !== void 0) {
|
|
792
|
+
const retained = [];
|
|
793
|
+
let deletedAny2 = false;
|
|
794
|
+
for (const entry of entries) {
|
|
795
|
+
const maxAgeMs = this.options.maxAgeMsByBucket[entry.bucket];
|
|
796
|
+
const expired = typeof maxAgeMs === "number" && Number.isFinite(maxAgeMs) && nowMs - entry.mtimeMs > maxAgeMs;
|
|
797
|
+
if (!expired) {
|
|
798
|
+
retained.push(entry);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
await unlink(entry.path);
|
|
803
|
+
deletedAny2 = true;
|
|
804
|
+
} catch {
|
|
805
|
+
retained.push(entry);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
entries = retained;
|
|
809
|
+
if (deletedAny2) {
|
|
810
|
+
this.byKey.clear();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
761
813
|
let totalBytes = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
762
814
|
if (totalBytes <= this.options.maxBytes) {
|
|
763
815
|
return;
|
|
764
816
|
}
|
|
765
817
|
entries.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
818
|
+
let deletedAny = false;
|
|
766
819
|
for (const entry of entries) {
|
|
767
820
|
if (totalBytes <= this.options.maxBytes) {
|
|
768
821
|
break;
|
|
769
822
|
}
|
|
770
823
|
try {
|
|
771
824
|
await unlink(entry.path);
|
|
825
|
+
deletedAny = true;
|
|
772
826
|
totalBytes -= entry.size;
|
|
773
827
|
} catch {
|
|
774
828
|
}
|
|
775
829
|
}
|
|
830
|
+
if (deletedAny) {
|
|
831
|
+
this.byKey.clear();
|
|
832
|
+
}
|
|
776
833
|
}
|
|
777
834
|
async get(key) {
|
|
778
835
|
if (this.byKey.has(key)) {
|
|
@@ -808,7 +865,7 @@ var FileCacheStore = class {
|
|
|
808
865
|
};
|
|
809
866
|
var SIX_HOURS_MS = 6 * 60 * 60 * 1e3;
|
|
810
867
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
811
|
-
var DEFAULT_MAX_BYTES =
|
|
868
|
+
var DEFAULT_MAX_BYTES = 20 * 1024 * 1024;
|
|
812
869
|
var DEFAULT_MAX_ENTRY_BYTES = 4 * 1024 * 1024;
|
|
813
870
|
var DEFAULT_SWEEP_INTERVAL_WRITES = 25;
|
|
814
871
|
var cacheStoreSingleton;
|
|
@@ -835,6 +892,8 @@ var getNpmMetadataCacheStore = () => {
|
|
|
835
892
|
return cacheStoreSingleton;
|
|
836
893
|
}
|
|
837
894
|
const path = join4(resolveCodesentinelCacheDir(process.env), "npm-metadata-v2");
|
|
895
|
+
const packumentTtlMs = getPackumentCacheTtlMs();
|
|
896
|
+
const downloadsTtlMs = getWeeklyDownloadsCacheTtlMs();
|
|
838
897
|
cacheStoreSingleton = new FileCacheStore(path, {
|
|
839
898
|
maxBytes: parsePositiveIntegerFromEnv(
|
|
840
899
|
process.env["CODESENTINEL_CACHE_MAX_BYTES"],
|
|
@@ -847,7 +906,20 @@ var getNpmMetadataCacheStore = () => {
|
|
|
847
906
|
sweepIntervalWrites: parsePositiveIntegerFromEnv(
|
|
848
907
|
process.env["CODESENTINEL_CACHE_SWEEP_INTERVAL_WRITES"],
|
|
849
908
|
DEFAULT_SWEEP_INTERVAL_WRITES
|
|
850
|
-
)
|
|
909
|
+
),
|
|
910
|
+
bucketForKey: (key) => {
|
|
911
|
+
if (key.startsWith("npm:downloads:last-week:")) {
|
|
912
|
+
return "downloads";
|
|
913
|
+
}
|
|
914
|
+
if (key.startsWith("npm:packument:")) {
|
|
915
|
+
return "packument";
|
|
916
|
+
}
|
|
917
|
+
return "default";
|
|
918
|
+
},
|
|
919
|
+
maxAgeMsByBucket: {
|
|
920
|
+
downloads: downloadsTtlMs,
|
|
921
|
+
packument: packumentTtlMs
|
|
922
|
+
}
|
|
851
923
|
});
|
|
852
924
|
return cacheStoreSingleton;
|
|
853
925
|
};
|