ctxloom-pro 1.0.21 → 1.0.23
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/apps/dashboard/dist/dashboard/client/assets/index-BvSIeKR7.css +1 -0
- package/apps/dashboard/dist/dashboard/client/assets/index-Ci5_ZKGm.js +143 -0
- package/apps/dashboard/dist/dashboard/client/index.html +2 -2
- package/apps/dashboard/dist/server/index.js +403 -176
- package/dist/{chunk-EHFVPRTN.js → chunk-ZTC3QWIL.js} +351 -141
- package/dist/index.js +6 -6
- package/dist/{src-IHPQZOZE.js → src-ZA7RCGPQ.js} +24 -2
- package/package.json +1 -1
- package/apps/dashboard/dist/dashboard/client/assets/index-BrBqXw9P.js +0 -143
- package/apps/dashboard/dist/dashboard/client/assets/index-DcRDqUOZ.css +0 -1
|
@@ -3406,8 +3406,8 @@ var CoChangeIndex = class _CoChangeIndex {
|
|
|
3406
3406
|
if (event.isBulk || event.isMerge) return;
|
|
3407
3407
|
const paths = event.files.map((f) => f.path);
|
|
3408
3408
|
if (paths.length === 0) return;
|
|
3409
|
-
for (const
|
|
3410
|
-
this.nodeCounts.set(
|
|
3409
|
+
for (const path30 of paths) {
|
|
3410
|
+
this.nodeCounts.set(path30, (this.nodeCounts.get(path30) ?? 0) + 1);
|
|
3411
3411
|
}
|
|
3412
3412
|
for (let i = 0; i < paths.length; i++) {
|
|
3413
3413
|
for (let j = i + 1; j < paths.length; j++) {
|
|
@@ -3554,8 +3554,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3554
3554
|
*/
|
|
3555
3555
|
snapshot() {
|
|
3556
3556
|
const nodes = {};
|
|
3557
|
-
for (const [
|
|
3558
|
-
nodes[
|
|
3557
|
+
for (const [path30, raw] of this.nodes) {
|
|
3558
|
+
nodes[path30] = {
|
|
3559
3559
|
commits: raw.commits,
|
|
3560
3560
|
churnLines: raw.churnLines,
|
|
3561
3561
|
bugCommits: raw.bugCommits,
|
|
@@ -3570,8 +3570,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3570
3570
|
*/
|
|
3571
3571
|
static load(s) {
|
|
3572
3572
|
const idx = new _ChurnIndex();
|
|
3573
|
-
for (const [
|
|
3574
|
-
idx.nodes.set(
|
|
3573
|
+
for (const [path30, raw] of Object.entries(s.nodes)) {
|
|
3574
|
+
idx.nodes.set(path30, {
|
|
3575
3575
|
commits: raw.commits,
|
|
3576
3576
|
churnLines: raw.churnLines,
|
|
3577
3577
|
bugCommits: raw.bugCommits,
|
|
@@ -3584,8 +3584,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3584
3584
|
// -------------------------------------------------------------------------
|
|
3585
3585
|
// Private helpers
|
|
3586
3586
|
// -------------------------------------------------------------------------
|
|
3587
|
-
getOrCreate(
|
|
3588
|
-
const existing = this.nodes.get(
|
|
3587
|
+
getOrCreate(path30) {
|
|
3588
|
+
const existing = this.nodes.get(path30);
|
|
3589
3589
|
if (existing !== void 0) return existing;
|
|
3590
3590
|
const fresh = {
|
|
3591
3591
|
commits: 0,
|
|
@@ -3594,7 +3594,7 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3594
3594
|
authorCounts: {},
|
|
3595
3595
|
lastTouch: 0
|
|
3596
3596
|
};
|
|
3597
|
-
this.nodes.set(
|
|
3597
|
+
this.nodes.set(path30, fresh);
|
|
3598
3598
|
return fresh;
|
|
3599
3599
|
}
|
|
3600
3600
|
};
|
|
@@ -3675,12 +3675,12 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3675
3675
|
*/
|
|
3676
3676
|
snapshot() {
|
|
3677
3677
|
const nodes = {};
|
|
3678
|
-
for (const [
|
|
3678
|
+
for (const [path30, raw] of this.nodes) {
|
|
3679
3679
|
const authorWeights = {};
|
|
3680
3680
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3681
3681
|
authorWeights[email] = { ...entry };
|
|
3682
3682
|
}
|
|
3683
|
-
nodes[
|
|
3683
|
+
nodes[path30] = { authorWeights, lastTouch: raw.lastTouch };
|
|
3684
3684
|
}
|
|
3685
3685
|
return { version: 1, nodes };
|
|
3686
3686
|
}
|
|
@@ -3689,23 +3689,23 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3689
3689
|
*/
|
|
3690
3690
|
static load(s) {
|
|
3691
3691
|
const idx = new _OwnershipIndex();
|
|
3692
|
-
for (const [
|
|
3692
|
+
for (const [path30, raw] of Object.entries(s.nodes)) {
|
|
3693
3693
|
const authorWeights = {};
|
|
3694
3694
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3695
3695
|
authorWeights[email] = { ...entry };
|
|
3696
3696
|
}
|
|
3697
|
-
idx.nodes.set(
|
|
3697
|
+
idx.nodes.set(path30, { authorWeights, lastTouch: raw.lastTouch });
|
|
3698
3698
|
}
|
|
3699
3699
|
return idx;
|
|
3700
3700
|
}
|
|
3701
3701
|
// -------------------------------------------------------------------------
|
|
3702
3702
|
// Private helpers
|
|
3703
3703
|
// -------------------------------------------------------------------------
|
|
3704
|
-
getOrCreate(
|
|
3705
|
-
const existing = this.nodes.get(
|
|
3704
|
+
getOrCreate(path30) {
|
|
3705
|
+
const existing = this.nodes.get(path30);
|
|
3706
3706
|
if (existing !== void 0) return existing;
|
|
3707
3707
|
const fresh = { authorWeights: {}, lastTouch: 0 };
|
|
3708
|
-
this.nodes.set(
|
|
3708
|
+
this.nodes.set(path30, fresh);
|
|
3709
3709
|
return fresh;
|
|
3710
3710
|
}
|
|
3711
3711
|
};
|
|
@@ -3880,19 +3880,121 @@ function isEnoent(err) {
|
|
|
3880
3880
|
// packages/core/src/trends/TrendsRecorder.ts
|
|
3881
3881
|
import path11 from "path";
|
|
3882
3882
|
import fs10 from "fs/promises";
|
|
3883
|
+
|
|
3884
|
+
// packages/core/src/risk/scoring.ts
|
|
3885
|
+
var RISK_WEIGHTS = { churn: 0.4, bug: 0.3, coupling: 0.3 };
|
|
3886
|
+
var BAND_PCT = { critical: 0.05, high: 0.15, medium: 0.35 };
|
|
3887
|
+
var SCORE_FLOOR = 0.05;
|
|
3888
|
+
var SILO_BUS_FACTOR = 1;
|
|
3889
|
+
function percentile(values, p) {
|
|
3890
|
+
if (values.length === 0) return 0;
|
|
3891
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3892
|
+
const idx = Math.min(sorted.length - 1, Math.floor(sorted.length * p));
|
|
3893
|
+
return sorted[idx];
|
|
3894
|
+
}
|
|
3895
|
+
function computeRiskCaps(samples, p = 0.9) {
|
|
3896
|
+
return {
|
|
3897
|
+
churn: percentile(samples.map((s) => s.churnLines), p),
|
|
3898
|
+
coupling: percentile(samples.map((s) => s.couplingFanOut), p)
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
function computeRiskBreakdown(m, caps) {
|
|
3902
|
+
const churnDenom = Math.max(caps.churn, 1);
|
|
3903
|
+
const couplingDenom = Math.max(caps.coupling, 1);
|
|
3904
|
+
return {
|
|
3905
|
+
churn: Math.min(1, m.churnLines / churnDenom),
|
|
3906
|
+
bugDensity: Math.min(1, m.bugDensity * 2),
|
|
3907
|
+
coupling: Math.min(1, m.couplingFanOut / couplingDenom)
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
function scoreFromBreakdown(b) {
|
|
3911
|
+
return b.churn * RISK_WEIGHTS.churn + b.bugDensity * RISK_WEIGHTS.bug + b.coupling * RISK_WEIGHTS.coupling;
|
|
3912
|
+
}
|
|
3913
|
+
function isSiloed(m) {
|
|
3914
|
+
return m.busFactor <= SILO_BUS_FACTOR;
|
|
3915
|
+
}
|
|
3916
|
+
function assignLabelsByPercentile(scores) {
|
|
3917
|
+
const n = scores.length;
|
|
3918
|
+
const bands = { criticalCount: 0, highCount: 0, mediumCount: 0, lowCount: 0, totalRanked: n };
|
|
3919
|
+
if (n === 0) return { labels: [], bands };
|
|
3920
|
+
const ranked = scores.map((s, i) => ({ s, i })).sort((a, b) => b.s - a.s);
|
|
3921
|
+
const criticalCutoff = Math.max(1, Math.ceil(n * BAND_PCT.critical));
|
|
3922
|
+
const highCutoff = Math.max(criticalCutoff + 1, Math.ceil(n * BAND_PCT.high));
|
|
3923
|
+
const mediumCutoff = Math.max(highCutoff + 1, Math.ceil(n * BAND_PCT.medium));
|
|
3924
|
+
const labels = new Array(n).fill("low");
|
|
3925
|
+
for (let rank = 0; rank < n; rank++) {
|
|
3926
|
+
const { s, i } = ranked[rank];
|
|
3927
|
+
if (s < SCORE_FLOOR) {
|
|
3928
|
+
labels[i] = "low";
|
|
3929
|
+
bands.lowCount++;
|
|
3930
|
+
continue;
|
|
3931
|
+
}
|
|
3932
|
+
if (rank < criticalCutoff) {
|
|
3933
|
+
labels[i] = "critical";
|
|
3934
|
+
bands.criticalCount++;
|
|
3935
|
+
} else if (rank < highCutoff) {
|
|
3936
|
+
labels[i] = "high";
|
|
3937
|
+
bands.highCount++;
|
|
3938
|
+
} else if (rank < mediumCutoff) {
|
|
3939
|
+
labels[i] = "medium";
|
|
3940
|
+
bands.mediumCount++;
|
|
3941
|
+
} else {
|
|
3942
|
+
labels[i] = "low";
|
|
3943
|
+
bands.lowCount++;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
return { labels, bands };
|
|
3947
|
+
}
|
|
3948
|
+
function scoreAll(samples) {
|
|
3949
|
+
const caps = computeRiskCaps(samples);
|
|
3950
|
+
const intermediate = samples.map((raw) => {
|
|
3951
|
+
const breakdown = computeRiskBreakdown(raw, caps);
|
|
3952
|
+
return { raw, breakdown, score: scoreFromBreakdown(breakdown) };
|
|
3953
|
+
});
|
|
3954
|
+
const { labels, bands } = assignLabelsByPercentile(intermediate.map((i) => i.score));
|
|
3955
|
+
const scored = intermediate.map((i, idx) => ({
|
|
3956
|
+
raw: i.raw,
|
|
3957
|
+
breakdown: i.breakdown,
|
|
3958
|
+
score: i.score,
|
|
3959
|
+
label: labels[idx],
|
|
3960
|
+
siloed: isSiloed(i.raw)
|
|
3961
|
+
}));
|
|
3962
|
+
return { scored, caps, bands };
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
// packages/core/src/trends/TrendsRecorder.ts
|
|
3883
3966
|
var FILE_SUBPATH = path11.join(".ctxloom", "trends", "snapshots.jsonl");
|
|
3967
|
+
var FILE_RISKS_SUBPATH = path11.join(".ctxloom", "trends", "file-risks.jsonl");
|
|
3884
3968
|
var ENTRY_PATTERN = /(^|\/)(index|main|server|app|cli)\.[^/]+$/;
|
|
3885
3969
|
var SEVEN_DAYS_SECONDS = 7 * 24 * 3600;
|
|
3886
3970
|
var COLLAPSE_WINDOW_SECONDS = 300;
|
|
3887
3971
|
var PERCENT_THRESHOLD = 0.01;
|
|
3972
|
+
var FILE_SCORE_DELTA = 0.02;
|
|
3888
3973
|
var INTEGER_FLOOR_FIELDS = [
|
|
3889
3974
|
"totalFiles",
|
|
3890
3975
|
"totalEdges",
|
|
3891
3976
|
"deadFiles",
|
|
3892
3977
|
"highRiskFiles"
|
|
3893
3978
|
];
|
|
3894
|
-
function
|
|
3895
|
-
const { graph, overlay
|
|
3979
|
+
function gatherPerFileMetrics(opts) {
|
|
3980
|
+
const { graph, overlay } = opts;
|
|
3981
|
+
return graph.allFiles().map((f) => {
|
|
3982
|
+
const churn = overlay.churn.statsFor(f);
|
|
3983
|
+
const ownership = overlay.ownership.statsFor(f);
|
|
3984
|
+
const coupled = overlay.coChange.topFor({ node: f, limit: 100, minConfidence: 0.1 });
|
|
3985
|
+
return {
|
|
3986
|
+
file: f,
|
|
3987
|
+
raw: {
|
|
3988
|
+
churnLines: churn?.churnLines ?? 0,
|
|
3989
|
+
bugDensity: churn?.bugDensity ?? 0,
|
|
3990
|
+
busFactor: ownership?.busFactor ?? 1,
|
|
3991
|
+
couplingFanOut: coupled.length
|
|
3992
|
+
}
|
|
3993
|
+
};
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
function computeAggregates(opts, unixSeconds) {
|
|
3997
|
+
const { graph, gitEnabled } = opts;
|
|
3896
3998
|
const files = graph.allFiles();
|
|
3897
3999
|
let deadFiles = 0;
|
|
3898
4000
|
for (const f of files) {
|
|
@@ -3901,51 +4003,62 @@ function computeMetrics(opts, unixSeconds) {
|
|
|
3901
4003
|
}
|
|
3902
4004
|
if (!gitEnabled) {
|
|
3903
4005
|
return {
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
4006
|
+
metrics: {
|
|
4007
|
+
totalFiles: files.length,
|
|
4008
|
+
totalEdges: graph.edgeCount(),
|
|
4009
|
+
deadFiles,
|
|
4010
|
+
avgBusFactor: null,
|
|
4011
|
+
highRiskFiles: null,
|
|
4012
|
+
churnLinesLast7d: null
|
|
4013
|
+
},
|
|
4014
|
+
perFileScores: []
|
|
3910
4015
|
};
|
|
3911
4016
|
}
|
|
3912
4017
|
let busSum = 0;
|
|
3913
4018
|
let busCount = 0;
|
|
3914
|
-
for (const f of overlay.ownership.allNodes()) {
|
|
3915
|
-
const stats = overlay.ownership.statsFor(f);
|
|
4019
|
+
for (const f of opts.overlay.ownership.allNodes()) {
|
|
4020
|
+
const stats = opts.overlay.ownership.statsFor(f);
|
|
3916
4021
|
if (stats !== null) {
|
|
3917
4022
|
busSum += stats.busFactor;
|
|
3918
4023
|
busCount++;
|
|
3919
4024
|
}
|
|
3920
4025
|
}
|
|
3921
4026
|
const avgBusFactor = busCount > 0 ? busSum / busCount : 0;
|
|
3922
|
-
let highRiskFiles = 0;
|
|
3923
|
-
let churnLinesLast7d = 0;
|
|
3924
4027
|
const sevenDaysAgo = unixSeconds - SEVEN_DAYS_SECONDS;
|
|
4028
|
+
let churnLinesLast7d = 0;
|
|
3925
4029
|
for (const f of files) {
|
|
3926
|
-
const churn = overlay.churn.statsFor(f);
|
|
3927
|
-
const ownership = overlay.ownership.statsFor(f);
|
|
3928
|
-
const coupled = overlay.coChange.topFor({ node: f, limit: 100, minConfidence: 0.1 });
|
|
3929
|
-
const churnLines = churn?.churnLines ?? 0;
|
|
3930
|
-
const bugDensity = churn?.bugDensity ?? 0;
|
|
3931
|
-
const busFactor = ownership?.busFactor ?? 1;
|
|
3932
|
-
const churnPart = Math.min(1, churnLines / 1e3);
|
|
3933
|
-
const bugPart = Math.min(1, bugDensity * 2);
|
|
3934
|
-
const busPart = busFactor <= 1 ? 1 : busFactor <= 2 ? 0.5 : 0;
|
|
3935
|
-
const couplingPart = Math.min(1, coupled.length / 10);
|
|
3936
|
-
const score = churnPart * 0.3 + bugPart * 0.3 + busPart * 0.2 + couplingPart * 0.2;
|
|
3937
|
-
if (score > 0.6) highRiskFiles++;
|
|
4030
|
+
const churn = opts.overlay.churn.statsFor(f);
|
|
3938
4031
|
if (churn && churn.lastTouch >= sevenDaysAgo) {
|
|
3939
4032
|
churnLinesLast7d += churn.churnLines;
|
|
3940
4033
|
}
|
|
3941
4034
|
}
|
|
4035
|
+
const perFile = gatherPerFileMetrics(opts);
|
|
4036
|
+
const { scored } = scoreAll(perFile.map((p) => p.raw));
|
|
4037
|
+
let highRiskFiles = 0;
|
|
4038
|
+
const perFileScores = [];
|
|
4039
|
+
for (let i = 0; i < scored.length; i++) {
|
|
4040
|
+
const s = scored[i];
|
|
4041
|
+
const file = perFile[i].file;
|
|
4042
|
+
if (s.label === "critical" || s.label === "high") highRiskFiles++;
|
|
4043
|
+
if (s.score >= SCORE_FLOOR) {
|
|
4044
|
+
perFileScores.push({
|
|
4045
|
+
unixSeconds,
|
|
4046
|
+
file,
|
|
4047
|
+
score: Math.round(s.score * 100) / 100,
|
|
4048
|
+
label: s.label
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
3942
4052
|
return {
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
4053
|
+
metrics: {
|
|
4054
|
+
totalFiles: files.length,
|
|
4055
|
+
totalEdges: graph.edgeCount(),
|
|
4056
|
+
deadFiles,
|
|
4057
|
+
avgBusFactor: Math.round(avgBusFactor * 100) / 100,
|
|
4058
|
+
highRiskFiles,
|
|
4059
|
+
churnLinesLast7d
|
|
4060
|
+
},
|
|
4061
|
+
perFileScores
|
|
3949
4062
|
};
|
|
3950
4063
|
}
|
|
3951
4064
|
async function ensureDir(dir) {
|
|
@@ -4016,14 +4129,46 @@ async function overwriteLastLine(filePath, replacement) {
|
|
|
4016
4129
|
const head = idx >= 0 ? trimmed.slice(0, idx + 1) : "";
|
|
4017
4130
|
await fs10.writeFile(filePath, head + replacement + "\n");
|
|
4018
4131
|
}
|
|
4132
|
+
async function loadLastScoresPerFile(filePath) {
|
|
4133
|
+
const out = /* @__PURE__ */ new Map();
|
|
4134
|
+
try {
|
|
4135
|
+
const raw = await fs10.readFile(filePath, "utf-8");
|
|
4136
|
+
for (const line of raw.split("\n")) {
|
|
4137
|
+
if (!line.trim()) continue;
|
|
4138
|
+
try {
|
|
4139
|
+
const p = JSON.parse(line);
|
|
4140
|
+
if (typeof p?.file === "string" && typeof p.score === "number") {
|
|
4141
|
+
out.set(p.file, p);
|
|
4142
|
+
}
|
|
4143
|
+
} catch {
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
} catch {
|
|
4147
|
+
}
|
|
4148
|
+
return out;
|
|
4149
|
+
}
|
|
4150
|
+
async function appendFilteredPerFileScores(filePath, next, prev) {
|
|
4151
|
+
const lines = [];
|
|
4152
|
+
for (const p of next) {
|
|
4153
|
+
const last = prev.get(p.file);
|
|
4154
|
+
const labelChanged = last?.label !== p.label;
|
|
4155
|
+
const scoreMoved = last === void 0 || Math.abs(p.score - last.score) >= FILE_SCORE_DELTA;
|
|
4156
|
+
if (labelChanged || scoreMoved) lines.push(JSON.stringify(p));
|
|
4157
|
+
}
|
|
4158
|
+
if (lines.length === 0) return 0;
|
|
4159
|
+
await ensureDir(path11.dirname(filePath));
|
|
4160
|
+
await fs10.appendFile(filePath, lines.join("\n") + "\n");
|
|
4161
|
+
return lines.length;
|
|
4162
|
+
}
|
|
4019
4163
|
async function recordTrendSnapshot(opts) {
|
|
4020
4164
|
const now = opts.now ?? Date.now;
|
|
4021
4165
|
const ms = now();
|
|
4022
4166
|
const unixSeconds = Math.floor(ms / 1e3);
|
|
4023
4167
|
const timestamp = new Date(ms).toISOString();
|
|
4024
4168
|
const filePath = path11.join(opts.rootDir, FILE_SUBPATH);
|
|
4169
|
+
const fileRisksPath = path11.join(opts.rootDir, FILE_RISKS_SUBPATH);
|
|
4025
4170
|
try {
|
|
4026
|
-
const metrics =
|
|
4171
|
+
const { metrics, perFileScores } = computeAggregates(opts, unixSeconds);
|
|
4027
4172
|
const snapshot = {
|
|
4028
4173
|
timestamp,
|
|
4029
4174
|
unixSeconds,
|
|
@@ -4038,6 +4183,10 @@ async function recordTrendSnapshot(opts) {
|
|
|
4038
4183
|
} else {
|
|
4039
4184
|
await fs10.appendFile(filePath, JSON.stringify(snapshot) + "\n");
|
|
4040
4185
|
}
|
|
4186
|
+
if (perFileScores.length > 0) {
|
|
4187
|
+
const lastScores = await loadLastScoresPerFile(fileRisksPath);
|
|
4188
|
+
await appendFilteredPerFileScores(fileRisksPath, perFileScores, lastScores);
|
|
4189
|
+
}
|
|
4041
4190
|
return snapshot;
|
|
4042
4191
|
} catch (err) {
|
|
4043
4192
|
logger.warn("Failed to record trend snapshot", {
|
|
@@ -4099,8 +4248,58 @@ async function loadTrendSeries(opts) {
|
|
|
4099
4248
|
return { snapshots: tail, gitEnabled, totalCount };
|
|
4100
4249
|
}
|
|
4101
4250
|
|
|
4251
|
+
// packages/core/src/trends/FileRiskStore.ts
|
|
4252
|
+
import path13 from "path";
|
|
4253
|
+
import fs12 from "fs/promises";
|
|
4254
|
+
var FILE_SUBPATH3 = path13.join(".ctxloom", "trends", "file-risks.jsonl");
|
|
4255
|
+
var DEFAULT_LIMIT3 = 200;
|
|
4256
|
+
var VALID_LABELS = /* @__PURE__ */ new Set(["low", "medium", "high", "critical"]);
|
|
4257
|
+
function isValidPoint(value) {
|
|
4258
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4259
|
+
const v = value;
|
|
4260
|
+
return typeof v.unixSeconds === "number" && typeof v.file === "string" && typeof v.score === "number" && typeof v.label === "string" && VALID_LABELS.has(v.label);
|
|
4261
|
+
}
|
|
4262
|
+
async function loadFileRiskHistory(opts) {
|
|
4263
|
+
const filePath = path13.join(opts.rootDir, FILE_SUBPATH3);
|
|
4264
|
+
const limit = opts.limit ?? DEFAULT_LIMIT3;
|
|
4265
|
+
const sinceUnixSeconds = opts.sinceUnixSeconds ?? 0;
|
|
4266
|
+
let raw;
|
|
4267
|
+
try {
|
|
4268
|
+
raw = await fs12.readFile(filePath, "utf-8");
|
|
4269
|
+
} catch {
|
|
4270
|
+
return { file: opts.file, points: [], totalCount: 0 };
|
|
4271
|
+
}
|
|
4272
|
+
const allMatching = [];
|
|
4273
|
+
let totalCount = 0;
|
|
4274
|
+
let malformed = 0;
|
|
4275
|
+
for (const line of raw.split("\n")) {
|
|
4276
|
+
if (!line.trim()) continue;
|
|
4277
|
+
let value;
|
|
4278
|
+
try {
|
|
4279
|
+
value = JSON.parse(line);
|
|
4280
|
+
} catch {
|
|
4281
|
+
malformed++;
|
|
4282
|
+
continue;
|
|
4283
|
+
}
|
|
4284
|
+
if (!isValidPoint(value)) {
|
|
4285
|
+
malformed++;
|
|
4286
|
+
continue;
|
|
4287
|
+
}
|
|
4288
|
+
if (value.file !== opts.file) continue;
|
|
4289
|
+
totalCount++;
|
|
4290
|
+
if (value.unixSeconds < sinceUnixSeconds) continue;
|
|
4291
|
+
allMatching.push(value);
|
|
4292
|
+
}
|
|
4293
|
+
if (malformed > 0) {
|
|
4294
|
+
logger.warn("Skipped malformed file-risk rows", { file: filePath, count: malformed });
|
|
4295
|
+
}
|
|
4296
|
+
allMatching.sort((a, b) => a.unixSeconds - b.unixSeconds);
|
|
4297
|
+
const tail = allMatching.length > limit ? allMatching.slice(allMatching.length - limit) : allMatching;
|
|
4298
|
+
return { file: opts.file, points: tail, totalCount };
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4102
4301
|
// packages/core/src/ast/Skeletonizer.ts
|
|
4103
|
-
import
|
|
4302
|
+
import fs13 from "fs";
|
|
4104
4303
|
var Skeletonizer = class {
|
|
4105
4304
|
parser;
|
|
4106
4305
|
constructor() {
|
|
@@ -4142,7 +4341,7 @@ var Skeletonizer = class {
|
|
|
4142
4341
|
* "deliberately skipped" from "skeletonized to empty".
|
|
4143
4342
|
*/
|
|
4144
4343
|
async skeletonize(filePath) {
|
|
4145
|
-
const fileSource =
|
|
4344
|
+
const fileSource = fs13.readFileSync(filePath, "utf-8");
|
|
4146
4345
|
const fileLines = fileSource.split("\n");
|
|
4147
4346
|
const MAX_INPUT_BYTES = 256 * 1024;
|
|
4148
4347
|
if (fileSource.length > MAX_INPUT_BYTES) {
|
|
@@ -4206,7 +4405,7 @@ ${methodLines.join("\n")}
|
|
|
4206
4405
|
*/
|
|
4207
4406
|
async skeletonizeXML(filePath) {
|
|
4208
4407
|
const nodes = await this.parser.parse(filePath);
|
|
4209
|
-
const fileLines =
|
|
4408
|
+
const fileLines = fs13.readFileSync(filePath, "utf-8").split("\n");
|
|
4210
4409
|
const parts = [`<skeleton file="${filePath}">`];
|
|
4211
4410
|
for (const node of nodes) {
|
|
4212
4411
|
switch (node.type) {
|
|
@@ -4358,7 +4557,7 @@ function registerFileTool(registry, ctx) {
|
|
|
4358
4557
|
|
|
4359
4558
|
// packages/core/src/tools/context-packet.ts
|
|
4360
4559
|
import { z as z3 } from "zod";
|
|
4361
|
-
import
|
|
4560
|
+
import path14 from "path";
|
|
4362
4561
|
var Schema3 = z3.object({
|
|
4363
4562
|
target_file: z3.string().describe("Relative path to the primary file"),
|
|
4364
4563
|
mode: z3.enum(["edit", "read"]).optional().default("edit").describe("Context mode")
|
|
@@ -4388,7 +4587,7 @@ function registerContextPacketTool(registry, ctx) {
|
|
|
4388
4587
|
const skeletons = await Promise.all(
|
|
4389
4588
|
imports.map(async (dep) => {
|
|
4390
4589
|
try {
|
|
4391
|
-
const absDep =
|
|
4590
|
+
const absDep = path14.resolve(ctx.projectRoot, dep);
|
|
4392
4591
|
const sk = await skeletonizer.skeletonize(absDep);
|
|
4393
4592
|
return `
|
|
4394
4593
|
<!-- ${dep} -->
|
|
@@ -4419,7 +4618,7 @@ ${sk}`;
|
|
|
4419
4618
|
import { z as z4 } from "zod";
|
|
4420
4619
|
|
|
4421
4620
|
// packages/core/src/tools/findCallers.ts
|
|
4422
|
-
import
|
|
4621
|
+
import path15 from "path";
|
|
4423
4622
|
function escapeXML3(text) {
|
|
4424
4623
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4425
4624
|
}
|
|
@@ -5217,7 +5416,7 @@ function registerArchitectureOverviewTool(registry, ctx) {
|
|
|
5217
5416
|
|
|
5218
5417
|
// packages/core/src/tools/knowledge-gaps.ts
|
|
5219
5418
|
import { z as z12 } from "zod";
|
|
5220
|
-
import
|
|
5419
|
+
import path16 from "path";
|
|
5221
5420
|
var Schema12 = z12.object({
|
|
5222
5421
|
min_importers: z12.number().min(1).max(50).optional().default(3).describe(
|
|
5223
5422
|
"Minimum importers to qualify as an untested hub (default: 3)"
|
|
@@ -5266,7 +5465,7 @@ function registerKnowledgeGapsTool(registry, ctx) {
|
|
|
5266
5465
|
const testFiles = new Set(files.filter((f) => TEST_PATTERN2.test(f)));
|
|
5267
5466
|
const testedBases = /* @__PURE__ */ new Set();
|
|
5268
5467
|
for (const tf of testFiles) {
|
|
5269
|
-
const base =
|
|
5468
|
+
const base = path16.basename(tf).replace(/\.(test|spec)\.[^.]+$/, "").replace(/\.[^.]+$/, "");
|
|
5270
5469
|
if (base) testedBases.add(base);
|
|
5271
5470
|
}
|
|
5272
5471
|
const isolated = [];
|
|
@@ -5284,7 +5483,7 @@ function registerKnowledgeGapsTool(registry, ctx) {
|
|
|
5284
5483
|
deadCode.push(file);
|
|
5285
5484
|
}
|
|
5286
5485
|
if (importers >= min_importers) {
|
|
5287
|
-
const base =
|
|
5486
|
+
const base = path16.basename(file).replace(/\.[^.]+$/, "");
|
|
5288
5487
|
if (!testedBases.has(base)) {
|
|
5289
5488
|
untestedHubs.push({ file, importers });
|
|
5290
5489
|
}
|
|
@@ -5708,8 +5907,8 @@ function registerGitDiffReviewTool(registry, ctx) {
|
|
|
5708
5907
|
|
|
5709
5908
|
// packages/core/src/tools/refactor-preview.ts
|
|
5710
5909
|
import { z as z17 } from "zod";
|
|
5711
|
-
import
|
|
5712
|
-
import
|
|
5910
|
+
import fs14 from "fs";
|
|
5911
|
+
import path17 from "path";
|
|
5713
5912
|
var Schema17 = z17.object({
|
|
5714
5913
|
symbol: z17.string().min(1).describe("Symbol name to rename (exact match, case-sensitive)"),
|
|
5715
5914
|
new_name: z17.string().min(1).describe("New name for the symbol"),
|
|
@@ -5723,7 +5922,7 @@ function escapeXML17(text) {
|
|
|
5723
5922
|
function scanFile(filePath, symbol, newName) {
|
|
5724
5923
|
let content;
|
|
5725
5924
|
try {
|
|
5726
|
-
content =
|
|
5925
|
+
content = fs14.readFileSync(filePath, "utf-8");
|
|
5727
5926
|
} catch {
|
|
5728
5927
|
return [];
|
|
5729
5928
|
}
|
|
@@ -5786,7 +5985,7 @@ function registerRefactorPreviewTool(registry, ctx) {
|
|
|
5786
5985
|
const fileChanges = [];
|
|
5787
5986
|
let totalOccurrences = 0;
|
|
5788
5987
|
for (const relPath of candidates) {
|
|
5789
|
-
const absPath =
|
|
5988
|
+
const absPath = path17.join(ctx.projectRoot, relPath);
|
|
5790
5989
|
const occurrences = scanFile(absPath, symbol, new_name);
|
|
5791
5990
|
if (occurrences.length > 0) {
|
|
5792
5991
|
fileChanges.push({ filePath: relPath, occurrences });
|
|
@@ -5953,8 +6152,8 @@ function registerExecutionFlowTool(registry, ctx) {
|
|
|
5953
6152
|
|
|
5954
6153
|
// packages/core/src/tools/cross-repo-search.ts
|
|
5955
6154
|
import { z as z19 } from "zod";
|
|
5956
|
-
import
|
|
5957
|
-
import
|
|
6155
|
+
import fs15 from "fs";
|
|
6156
|
+
import path18 from "path";
|
|
5958
6157
|
var RepoRegistry = class {
|
|
5959
6158
|
filePath;
|
|
5960
6159
|
repos;
|
|
@@ -5964,16 +6163,16 @@ var RepoRegistry = class {
|
|
|
5964
6163
|
}
|
|
5965
6164
|
load() {
|
|
5966
6165
|
try {
|
|
5967
|
-
if (!
|
|
5968
|
-
return JSON.parse(
|
|
6166
|
+
if (!fs15.existsSync(this.filePath)) return [];
|
|
6167
|
+
return JSON.parse(fs15.readFileSync(this.filePath, "utf-8"));
|
|
5969
6168
|
} catch {
|
|
5970
6169
|
return [];
|
|
5971
6170
|
}
|
|
5972
6171
|
}
|
|
5973
6172
|
save() {
|
|
5974
|
-
const dir =
|
|
5975
|
-
if (!
|
|
5976
|
-
|
|
6173
|
+
const dir = path18.dirname(this.filePath);
|
|
6174
|
+
if (!fs15.existsSync(dir)) fs15.mkdirSync(dir, { recursive: true });
|
|
6175
|
+
fs15.writeFileSync(this.filePath, JSON.stringify(this.repos, null, 2), "utf-8");
|
|
5977
6176
|
}
|
|
5978
6177
|
list() {
|
|
5979
6178
|
return [...this.repos];
|
|
@@ -5983,7 +6182,7 @@ var RepoRegistry = class {
|
|
|
5983
6182
|
const entry = {
|
|
5984
6183
|
root,
|
|
5985
6184
|
dbPath,
|
|
5986
|
-
name:
|
|
6185
|
+
name: path18.basename(root),
|
|
5987
6186
|
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5988
6187
|
};
|
|
5989
6188
|
if (existing >= 0) {
|
|
@@ -6011,7 +6210,7 @@ function escapeXML19(text) {
|
|
|
6011
6210
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6012
6211
|
}
|
|
6013
6212
|
function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
|
|
6014
|
-
const repoRegistryPath = registryFilePath ??
|
|
6213
|
+
const repoRegistryPath = registryFilePath ?? path18.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".ctxloom", "repos.json");
|
|
6015
6214
|
registry.register(
|
|
6016
6215
|
"ctx_cross_repo_search",
|
|
6017
6216
|
{
|
|
@@ -6114,8 +6313,8 @@ function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
|
|
|
6114
6313
|
|
|
6115
6314
|
// packages/core/src/tools/apply-refactor.ts
|
|
6116
6315
|
import { z as z20 } from "zod";
|
|
6117
|
-
import
|
|
6118
|
-
import
|
|
6316
|
+
import fs16 from "fs";
|
|
6317
|
+
import path19 from "path";
|
|
6119
6318
|
var Schema20 = z20.object({
|
|
6120
6319
|
symbol: z20.string().min(1).describe("Symbol name to rename (exact, case-sensitive)"),
|
|
6121
6320
|
new_name: z20.string().min(1).describe("New name for the symbol"),
|
|
@@ -6132,7 +6331,7 @@ function escapeXML20(text) {
|
|
|
6132
6331
|
function applyToFile(absPath, symbol, newName, dryRun) {
|
|
6133
6332
|
let content;
|
|
6134
6333
|
try {
|
|
6135
|
-
content =
|
|
6334
|
+
content = fs16.readFileSync(absPath, "utf-8");
|
|
6136
6335
|
} catch {
|
|
6137
6336
|
return 0;
|
|
6138
6337
|
}
|
|
@@ -6141,7 +6340,7 @@ function applyToFile(absPath, symbol, newName, dryRun) {
|
|
|
6141
6340
|
const occurrences = (content.match(regex) ?? []).length;
|
|
6142
6341
|
if (occurrences === 0) return 0;
|
|
6143
6342
|
if (!dryRun) {
|
|
6144
|
-
|
|
6343
|
+
fs16.writeFileSync(absPath, content.replace(regex, newName), "utf-8");
|
|
6145
6344
|
}
|
|
6146
6345
|
return occurrences;
|
|
6147
6346
|
}
|
|
@@ -6181,7 +6380,7 @@ function registerApplyRefactorTool(registry, ctx) {
|
|
|
6181
6380
|
const results = [];
|
|
6182
6381
|
let totalOccurrences = 0;
|
|
6183
6382
|
for (const relPath of candidates) {
|
|
6184
|
-
const absPath =
|
|
6383
|
+
const absPath = path19.join(ctx.projectRoot, relPath);
|
|
6185
6384
|
const count = applyToFile(absPath, symbol, new_name, dry_run);
|
|
6186
6385
|
if (count > 0) {
|
|
6187
6386
|
results.push({ filePath: relPath, occurrences: count, written: !dry_run });
|
|
@@ -6314,8 +6513,8 @@ function registerDetectChangesTool(registry, ctx) {
|
|
|
6314
6513
|
|
|
6315
6514
|
// packages/core/src/tools/full-text-search.ts
|
|
6316
6515
|
import { z as z22 } from "zod";
|
|
6317
|
-
import
|
|
6318
|
-
import
|
|
6516
|
+
import fs17 from "fs";
|
|
6517
|
+
import path20 from "path";
|
|
6319
6518
|
var Schema22 = z22.object({
|
|
6320
6519
|
query: z22.string().min(1).describe("Search term \u2014 literal or /regex/"),
|
|
6321
6520
|
mode: z22.enum(["hybrid", "keyword", "semantic"]).optional().default("hybrid"),
|
|
@@ -6340,7 +6539,7 @@ function buildPattern(query, caseSensitive) {
|
|
|
6340
6539
|
function scanFile2(absPath, pattern, contextLines) {
|
|
6341
6540
|
let content;
|
|
6342
6541
|
try {
|
|
6343
|
-
content =
|
|
6542
|
+
content = fs17.readFileSync(absPath, "utf-8");
|
|
6344
6543
|
} catch {
|
|
6345
6544
|
return null;
|
|
6346
6545
|
}
|
|
@@ -6406,7 +6605,7 @@ function registerFullTextSearchTool(registry, ctx) {
|
|
|
6406
6605
|
const files = graph.allFiles();
|
|
6407
6606
|
const keywordResults = [];
|
|
6408
6607
|
for (const relPath of files) {
|
|
6409
|
-
const absPath =
|
|
6608
|
+
const absPath = path20.join(ctx.projectRoot, relPath);
|
|
6410
6609
|
const hit = scanFile2(absPath, pattern, context_lines);
|
|
6411
6610
|
if (hit) {
|
|
6412
6611
|
keywordResults.push({
|
|
@@ -6626,8 +6825,8 @@ function registerGetWorkflowTool(registry, _ctx) {
|
|
|
6626
6825
|
}
|
|
6627
6826
|
|
|
6628
6827
|
// packages/core/src/tools/graph-snapshot.ts
|
|
6629
|
-
import
|
|
6630
|
-
import
|
|
6828
|
+
import fs18 from "fs";
|
|
6829
|
+
import path21 from "path";
|
|
6631
6830
|
import { z as z25 } from "zod";
|
|
6632
6831
|
var schema = z25.object({
|
|
6633
6832
|
name: z25.string().min(1).max(64).regex(/^[\w.-]+$/, "Name may only contain letters, digits, dots, underscores, hyphens").describe(
|
|
@@ -6638,13 +6837,13 @@ var schema = z25.object({
|
|
|
6638
6837
|
)
|
|
6639
6838
|
});
|
|
6640
6839
|
function saveNamedSnapshot(graph, name, rootDir, overwrite = false) {
|
|
6641
|
-
const snapshotsDir =
|
|
6642
|
-
|
|
6643
|
-
const snapshotPath =
|
|
6644
|
-
if (!snapshotPath.startsWith(snapshotsDir +
|
|
6840
|
+
const snapshotsDir = path21.resolve(rootDir, ".ctxloom", "snapshots");
|
|
6841
|
+
fs18.mkdirSync(snapshotsDir, { recursive: true });
|
|
6842
|
+
const snapshotPath = path21.resolve(snapshotsDir, `${name}.json`);
|
|
6843
|
+
if (!snapshotPath.startsWith(snapshotsDir + path21.sep)) {
|
|
6645
6844
|
throw new Error(`Invalid snapshot name: "${name}"`);
|
|
6646
6845
|
}
|
|
6647
|
-
if (
|
|
6846
|
+
if (fs18.existsSync(snapshotPath) && !overwrite) {
|
|
6648
6847
|
throw new Error(`Snapshot "${name}" already exists. Pass overwrite: true to replace it.`);
|
|
6649
6848
|
}
|
|
6650
6849
|
const files = graph.allFiles();
|
|
@@ -6660,13 +6859,13 @@ function saveNamedSnapshot(graph, name, rootDir, overwrite = false) {
|
|
|
6660
6859
|
forwardEdges
|
|
6661
6860
|
};
|
|
6662
6861
|
const tmp = snapshotPath + ".tmp";
|
|
6663
|
-
|
|
6664
|
-
|
|
6862
|
+
fs18.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
6863
|
+
fs18.renameSync(tmp, snapshotPath);
|
|
6665
6864
|
}
|
|
6666
6865
|
function listNamedSnapshots(rootDir) {
|
|
6667
|
-
const snapshotsDir =
|
|
6668
|
-
if (!
|
|
6669
|
-
return
|
|
6866
|
+
const snapshotsDir = path21.join(rootDir, ".ctxloom", "snapshots");
|
|
6867
|
+
if (!fs18.existsSync(snapshotsDir)) return [];
|
|
6868
|
+
return fs18.readdirSync(snapshotsDir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).sort();
|
|
6670
6869
|
}
|
|
6671
6870
|
function registerGraphSnapshotTool(registry, ctx) {
|
|
6672
6871
|
registry.register(
|
|
@@ -6714,24 +6913,24 @@ function registerGraphSnapshotTool(registry, ctx) {
|
|
|
6714
6913
|
}
|
|
6715
6914
|
|
|
6716
6915
|
// packages/core/src/tools/graph-diff.ts
|
|
6717
|
-
import
|
|
6718
|
-
import
|
|
6916
|
+
import fs19 from "fs";
|
|
6917
|
+
import path22 from "path";
|
|
6719
6918
|
import { z as z26 } from "zod";
|
|
6720
6919
|
var schema2 = z26.object({
|
|
6721
6920
|
baseline: z26.string().min(1).describe('Name of the baseline snapshot (the "before" state).'),
|
|
6722
6921
|
current: z26.string().min(1).describe('Name of the current snapshot (the "after" state).')
|
|
6723
6922
|
});
|
|
6724
6923
|
function loadSnapshot(name, rootDir) {
|
|
6725
|
-
const snapshotsDir =
|
|
6726
|
-
const snapshotPath =
|
|
6727
|
-
if (!snapshotPath.startsWith(snapshotsDir +
|
|
6924
|
+
const snapshotsDir = path22.resolve(rootDir, ".ctxloom", "snapshots");
|
|
6925
|
+
const snapshotPath = path22.resolve(snapshotsDir, `${name}.json`);
|
|
6926
|
+
if (!snapshotPath.startsWith(snapshotsDir + path22.sep)) {
|
|
6728
6927
|
throw new Error(`Invalid snapshot name: "${name}"`);
|
|
6729
6928
|
}
|
|
6730
|
-
if (!
|
|
6929
|
+
if (!fs19.existsSync(snapshotPath)) {
|
|
6731
6930
|
throw new Error(`Snapshot "${name}" not found. Run ctx_graph_snapshot first.`);
|
|
6732
6931
|
}
|
|
6733
6932
|
try {
|
|
6734
|
-
return JSON.parse(
|
|
6933
|
+
return JSON.parse(fs19.readFileSync(snapshotPath, "utf-8"));
|
|
6735
6934
|
} catch (e) {
|
|
6736
6935
|
throw new Error(`Snapshot "${name}" is corrupted: ${e instanceof Error ? e.message : String(e)}`);
|
|
6737
6936
|
}
|
|
@@ -7096,8 +7295,8 @@ var RulesConfigError = class extends Error {
|
|
|
7096
7295
|
};
|
|
7097
7296
|
|
|
7098
7297
|
// packages/core/src/rules/loadConfig.ts
|
|
7099
|
-
import
|
|
7100
|
-
import
|
|
7298
|
+
import fs20 from "fs/promises";
|
|
7299
|
+
import path23 from "path";
|
|
7101
7300
|
import yaml from "js-yaml";
|
|
7102
7301
|
import { z as z30 } from "zod";
|
|
7103
7302
|
var RuleSchema = z30.object({
|
|
@@ -7112,10 +7311,10 @@ var RulesConfigSchema = z30.object({
|
|
|
7112
7311
|
rules: z30.array(RuleSchema).default([])
|
|
7113
7312
|
});
|
|
7114
7313
|
async function loadRulesConfig(rootDir) {
|
|
7115
|
-
const filePath =
|
|
7314
|
+
const filePath = path23.join(rootDir, ".ctxloom", "rules.yml");
|
|
7116
7315
|
let raw;
|
|
7117
7316
|
try {
|
|
7118
|
-
raw = await
|
|
7317
|
+
raw = await fs20.readFile(filePath, "utf-8");
|
|
7119
7318
|
} catch (err) {
|
|
7120
7319
|
if (err.code === "ENOENT") return null;
|
|
7121
7320
|
throw new RulesConfigError(`Failed to read rules config: ${String(err)}`);
|
|
@@ -7480,8 +7679,8 @@ function createToolRegistry(ctx) {
|
|
|
7480
7679
|
}
|
|
7481
7680
|
|
|
7482
7681
|
// packages/core/src/tools/ruleManager.ts
|
|
7483
|
-
import
|
|
7484
|
-
import
|
|
7682
|
+
import fs21 from "fs";
|
|
7683
|
+
import path24 from "path";
|
|
7485
7684
|
var RULE_FILES = [
|
|
7486
7685
|
".cursorrules",
|
|
7487
7686
|
"CLAUDE.md",
|
|
@@ -7505,30 +7704,30 @@ var RuleManager = class {
|
|
|
7505
7704
|
if (this.cachedRules) return this.cachedRules;
|
|
7506
7705
|
const rules = [];
|
|
7507
7706
|
for (const ruleFile of RULE_FILES) {
|
|
7508
|
-
const fullPath =
|
|
7707
|
+
const fullPath = path24.join(this.projectRoot, ruleFile);
|
|
7509
7708
|
try {
|
|
7510
7709
|
this.pathValidator.validate(fullPath);
|
|
7511
|
-
if (
|
|
7512
|
-
const stat =
|
|
7710
|
+
if (fs21.existsSync(fullPath)) {
|
|
7711
|
+
const stat = fs21.statSync(fullPath);
|
|
7513
7712
|
if (stat.isFile()) {
|
|
7514
|
-
const content =
|
|
7713
|
+
const content = fs21.readFileSync(fullPath, "utf-8");
|
|
7515
7714
|
rules.push({
|
|
7516
7715
|
name: ruleFile,
|
|
7517
7716
|
path: ruleFile,
|
|
7518
7717
|
content
|
|
7519
7718
|
});
|
|
7520
7719
|
} else if (stat.isDirectory()) {
|
|
7521
|
-
const dirEntries =
|
|
7720
|
+
const dirEntries = fs21.readdirSync(fullPath);
|
|
7522
7721
|
for (const entry of dirEntries) {
|
|
7523
|
-
const entryPath =
|
|
7722
|
+
const entryPath = path24.join(fullPath, entry);
|
|
7524
7723
|
try {
|
|
7525
7724
|
this.pathValidator.validate(entryPath);
|
|
7526
7725
|
} catch {
|
|
7527
7726
|
continue;
|
|
7528
7727
|
}
|
|
7529
|
-
const entryStat =
|
|
7728
|
+
const entryStat = fs21.statSync(entryPath);
|
|
7530
7729
|
if (entryStat.isFile()) {
|
|
7531
|
-
const content =
|
|
7730
|
+
const content = fs21.readFileSync(entryPath, "utf-8");
|
|
7532
7731
|
rules.push({
|
|
7533
7732
|
name: `${ruleFile}/${entry}`,
|
|
7534
7733
|
path: `${ruleFile}/${entry}`,
|
|
@@ -7571,8 +7770,8 @@ var RuleManager = class {
|
|
|
7571
7770
|
};
|
|
7572
7771
|
|
|
7573
7772
|
// packages/core/src/review/AuthorResolver.ts
|
|
7574
|
-
import
|
|
7575
|
-
import
|
|
7773
|
+
import fs22 from "fs/promises";
|
|
7774
|
+
import path25 from "path";
|
|
7576
7775
|
import yaml2 from "js-yaml";
|
|
7577
7776
|
var AuthorResolver = class {
|
|
7578
7777
|
constructor(ctxloomDir) {
|
|
@@ -7597,8 +7796,8 @@ var AuthorResolver = class {
|
|
|
7597
7796
|
/** Write a new mapping to the cache file. */
|
|
7598
7797
|
async writeCache(email, handle) {
|
|
7599
7798
|
this.cache = { ...this.cache, [email]: handle };
|
|
7600
|
-
await
|
|
7601
|
-
|
|
7799
|
+
await fs22.writeFile(
|
|
7800
|
+
path25.join(this.ctxloomDir, "authors-cache.json"),
|
|
7602
7801
|
JSON.stringify(this.cache, null, 2)
|
|
7603
7802
|
);
|
|
7604
7803
|
}
|
|
@@ -7607,9 +7806,9 @@ var AuthorResolver = class {
|
|
|
7607
7806
|
return emails.filter((e) => this.resolve(e) === void 0);
|
|
7608
7807
|
}
|
|
7609
7808
|
async loadYml() {
|
|
7610
|
-
const file =
|
|
7809
|
+
const file = path25.join(this.ctxloomDir, "authors.yml");
|
|
7611
7810
|
try {
|
|
7612
|
-
const raw = await
|
|
7811
|
+
const raw = await fs22.readFile(file, "utf8");
|
|
7613
7812
|
const parsed = yaml2.load(raw);
|
|
7614
7813
|
if (!parsed) return;
|
|
7615
7814
|
this.mappings = parsed.mappings ?? {};
|
|
@@ -7618,9 +7817,9 @@ var AuthorResolver = class {
|
|
|
7618
7817
|
}
|
|
7619
7818
|
}
|
|
7620
7819
|
async loadCache() {
|
|
7621
|
-
const file =
|
|
7820
|
+
const file = path25.join(this.ctxloomDir, "authors-cache.json");
|
|
7622
7821
|
try {
|
|
7623
|
-
const raw = await
|
|
7822
|
+
const raw = await fs22.readFile(file, "utf8");
|
|
7624
7823
|
this.cache = JSON.parse(raw);
|
|
7625
7824
|
} catch {
|
|
7626
7825
|
}
|
|
@@ -7645,8 +7844,8 @@ async function resolveViaGitHubApi(email, owner, repo, token) {
|
|
|
7645
7844
|
}
|
|
7646
7845
|
|
|
7647
7846
|
// packages/core/src/review/CodeownersWriter.ts
|
|
7648
|
-
import
|
|
7649
|
-
import
|
|
7847
|
+
import fs23 from "fs/promises";
|
|
7848
|
+
import path26 from "path";
|
|
7650
7849
|
var MARKER_START = "# <ctxloom:start> \u2014 managed by ctxloom review-suggest; do not edit between markers";
|
|
7651
7850
|
var MARKER_START_DETECT = "# <ctxloom:start>";
|
|
7652
7851
|
var MARKER_END = "# <ctxloom:end>";
|
|
@@ -7678,15 +7877,15 @@ ${block}
|
|
|
7678
7877
|
async function generateCODEOWNERS(codeownersPath, rules) {
|
|
7679
7878
|
let existing = "";
|
|
7680
7879
|
try {
|
|
7681
|
-
existing = await
|
|
7880
|
+
existing = await fs23.readFile(codeownersPath, "utf8");
|
|
7682
7881
|
} catch {
|
|
7683
7882
|
}
|
|
7684
7883
|
const block = buildCodeownersBlock(rules);
|
|
7685
7884
|
return mergeIntoFile(existing, block);
|
|
7686
7885
|
}
|
|
7687
7886
|
async function writeCODEOWNERS(codeownersPath, content) {
|
|
7688
|
-
await
|
|
7689
|
-
await
|
|
7887
|
+
await fs23.mkdir(path26.dirname(codeownersPath), { recursive: true });
|
|
7888
|
+
await fs23.writeFile(codeownersPath, content, "utf8");
|
|
7690
7889
|
}
|
|
7691
7890
|
|
|
7692
7891
|
// packages/core/src/review/types.ts
|
|
@@ -7871,8 +8070,8 @@ function matchGlob(pattern, value) {
|
|
|
7871
8070
|
}
|
|
7872
8071
|
|
|
7873
8072
|
// packages/core/src/review/loadConfig.ts
|
|
7874
|
-
import
|
|
7875
|
-
import
|
|
8073
|
+
import fs24 from "fs/promises";
|
|
8074
|
+
import path27 from "path";
|
|
7876
8075
|
import yaml3 from "js-yaml";
|
|
7877
8076
|
function freshDefaults() {
|
|
7878
8077
|
return {
|
|
@@ -7883,9 +8082,9 @@ function freshDefaults() {
|
|
|
7883
8082
|
};
|
|
7884
8083
|
}
|
|
7885
8084
|
async function loadReviewConfig(root) {
|
|
7886
|
-
const file =
|
|
8085
|
+
const file = path27.join(root, ".ctxloom", "review.yml");
|
|
7887
8086
|
try {
|
|
7888
|
-
const raw = await
|
|
8087
|
+
const raw = await fs24.readFile(file, "utf8");
|
|
7889
8088
|
const parsed = yaml3.load(raw);
|
|
7890
8089
|
if (!parsed) return freshDefaults();
|
|
7891
8090
|
return {
|
|
@@ -7900,13 +8099,13 @@ async function loadReviewConfig(root) {
|
|
|
7900
8099
|
}
|
|
7901
8100
|
|
|
7902
8101
|
// packages/core/src/security/PathValidator.ts
|
|
7903
|
-
import
|
|
7904
|
-
import
|
|
8102
|
+
import path28 from "path";
|
|
8103
|
+
import fs25 from "fs";
|
|
7905
8104
|
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
7906
8105
|
var PathValidator = class {
|
|
7907
8106
|
canonicalRoot;
|
|
7908
8107
|
constructor(projectRoot) {
|
|
7909
|
-
this.canonicalRoot =
|
|
8108
|
+
this.canonicalRoot = fs25.realpathSync(path28.resolve(projectRoot));
|
|
7910
8109
|
}
|
|
7911
8110
|
/**
|
|
7912
8111
|
* Validates that the given input path resolves within the project root.
|
|
@@ -7916,14 +8115,14 @@ var PathValidator = class {
|
|
|
7916
8115
|
* @throws Error if the path escapes the project root
|
|
7917
8116
|
*/
|
|
7918
8117
|
validate(inputPath) {
|
|
7919
|
-
const resolved =
|
|
8118
|
+
const resolved = path28.resolve(this.canonicalRoot, inputPath);
|
|
7920
8119
|
let canonical;
|
|
7921
8120
|
try {
|
|
7922
|
-
canonical =
|
|
8121
|
+
canonical = fs25.realpathSync(resolved);
|
|
7923
8122
|
} catch {
|
|
7924
8123
|
canonical = resolved;
|
|
7925
8124
|
}
|
|
7926
|
-
if (!canonical.startsWith(this.canonicalRoot +
|
|
8125
|
+
if (!canonical.startsWith(this.canonicalRoot + path28.sep) && canonical !== this.canonicalRoot) {
|
|
7927
8126
|
throw new Error(
|
|
7928
8127
|
`Path traversal blocked: "${inputPath}" resolves outside of the project root`
|
|
7929
8128
|
);
|
|
@@ -7940,7 +8139,7 @@ var PathValidator = class {
|
|
|
7940
8139
|
* Converts an absolute path to a relative path from the project root.
|
|
7941
8140
|
*/
|
|
7942
8141
|
toRelative(absolutePath) {
|
|
7943
|
-
return
|
|
8142
|
+
return path28.relative(this.canonicalRoot, absolutePath);
|
|
7944
8143
|
}
|
|
7945
8144
|
/**
|
|
7946
8145
|
* Validates and reads a file, returning its content.
|
|
@@ -7948,11 +8147,11 @@ var PathValidator = class {
|
|
|
7948
8147
|
*/
|
|
7949
8148
|
readFile(inputPath) {
|
|
7950
8149
|
const absPath = this.validate(inputPath);
|
|
7951
|
-
const stat =
|
|
8150
|
+
const stat = fs25.statSync(absPath);
|
|
7952
8151
|
if (stat.size > MAX_FILE_SIZE) {
|
|
7953
8152
|
throw new Error(`File too large: ${inputPath} (${Math.round(stat.size / 1024)}KB, max ${MAX_FILE_SIZE / 1024 / 1024}MB)`);
|
|
7954
8153
|
}
|
|
7955
|
-
return
|
|
8154
|
+
return fs25.readFileSync(absPath, "utf-8");
|
|
7956
8155
|
}
|
|
7957
8156
|
/**
|
|
7958
8157
|
* Checks if a path exists and is within the project root.
|
|
@@ -8105,7 +8304,7 @@ var EmailAlreadyUsedError = class extends Error {
|
|
|
8105
8304
|
|
|
8106
8305
|
// packages/core/src/license/LicenseStore.ts
|
|
8107
8306
|
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSync } from "fs";
|
|
8108
|
-
import
|
|
8307
|
+
import path29 from "path";
|
|
8109
8308
|
|
|
8110
8309
|
// packages/core/src/license/types.ts
|
|
8111
8310
|
import { z as z32 } from "zod";
|
|
@@ -8126,7 +8325,7 @@ var LicenseFileSchema = z32.object({
|
|
|
8126
8325
|
|
|
8127
8326
|
// packages/core/src/license/LicenseStore.ts
|
|
8128
8327
|
function licenseFilePath(home) {
|
|
8129
|
-
return
|
|
8328
|
+
return path29.join(home, ".ctxloom", "license.json");
|
|
8130
8329
|
}
|
|
8131
8330
|
var LicenseStore = class {
|
|
8132
8331
|
filePath;
|
|
@@ -8149,7 +8348,7 @@ var LicenseStore = class {
|
|
|
8149
8348
|
}
|
|
8150
8349
|
}
|
|
8151
8350
|
async write(license) {
|
|
8152
|
-
mkdirSync(
|
|
8351
|
+
mkdirSync(path29.dirname(this.filePath), { recursive: true });
|
|
8153
8352
|
writeFileSync(this.filePath, JSON.stringify(license, null, 2), {
|
|
8154
8353
|
encoding: "utf8",
|
|
8155
8354
|
mode: 384
|
|
@@ -8481,8 +8680,19 @@ export {
|
|
|
8481
8680
|
ChurnIndex,
|
|
8482
8681
|
OwnershipIndex,
|
|
8483
8682
|
GitOverlayStore,
|
|
8683
|
+
RISK_WEIGHTS,
|
|
8684
|
+
BAND_PCT,
|
|
8685
|
+
SCORE_FLOOR,
|
|
8686
|
+
SILO_BUS_FACTOR,
|
|
8687
|
+
computeRiskCaps,
|
|
8688
|
+
computeRiskBreakdown,
|
|
8689
|
+
scoreFromBreakdown,
|
|
8690
|
+
isSiloed,
|
|
8691
|
+
assignLabelsByPercentile,
|
|
8692
|
+
scoreAll,
|
|
8484
8693
|
recordTrendSnapshot,
|
|
8485
8694
|
loadTrendSeries,
|
|
8695
|
+
loadFileRiskHistory,
|
|
8486
8696
|
Skeletonizer,
|
|
8487
8697
|
ToolRegistry,
|
|
8488
8698
|
detectChanges,
|
|
@@ -8531,4 +8741,4 @@ export {
|
|
|
8531
8741
|
track,
|
|
8532
8742
|
captureError
|
|
8533
8743
|
};
|
|
8534
|
-
//# sourceMappingURL=chunk-
|
|
8744
|
+
//# sourceMappingURL=chunk-ZTC3QWIL.js.map
|