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
|
@@ -148,8 +148,8 @@ var init_embedder = __esm({
|
|
|
148
148
|
// ../../packages/core/src/db/VectorStore.ts
|
|
149
149
|
import lancedb from "@lancedb/lancedb";
|
|
150
150
|
import { makeArrowTable } from "@lancedb/lancedb";
|
|
151
|
-
import
|
|
152
|
-
import
|
|
151
|
+
import path15 from "path";
|
|
152
|
+
import fs15 from "fs";
|
|
153
153
|
var init_VectorStore = __esm({
|
|
154
154
|
"../../packages/core/src/db/VectorStore.ts"() {
|
|
155
155
|
"use strict";
|
|
@@ -160,12 +160,12 @@ var init_VectorStore = __esm({
|
|
|
160
160
|
// server/index.ts
|
|
161
161
|
import express from "express";
|
|
162
162
|
import cors from "cors";
|
|
163
|
-
import
|
|
164
|
-
import
|
|
163
|
+
import path38 from "path";
|
|
164
|
+
import fs30 from "fs";
|
|
165
165
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
166
166
|
|
|
167
167
|
// server/loader.ts
|
|
168
|
-
import
|
|
168
|
+
import path32 from "path";
|
|
169
169
|
|
|
170
170
|
// ../../packages/core/src/graph/DependencyGraph.ts
|
|
171
171
|
import fs7 from "fs";
|
|
@@ -3124,8 +3124,8 @@ var CoChangeIndex = class _CoChangeIndex {
|
|
|
3124
3124
|
if (event.isBulk || event.isMerge) return;
|
|
3125
3125
|
const paths = event.files.map((f) => f.path);
|
|
3126
3126
|
if (paths.length === 0) return;
|
|
3127
|
-
for (const
|
|
3128
|
-
this.nodeCounts.set(
|
|
3127
|
+
for (const path39 of paths) {
|
|
3128
|
+
this.nodeCounts.set(path39, (this.nodeCounts.get(path39) ?? 0) + 1);
|
|
3129
3129
|
}
|
|
3130
3130
|
for (let i = 0; i < paths.length; i++) {
|
|
3131
3131
|
for (let j = i + 1; j < paths.length; j++) {
|
|
@@ -3272,8 +3272,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3272
3272
|
*/
|
|
3273
3273
|
snapshot() {
|
|
3274
3274
|
const nodes = {};
|
|
3275
|
-
for (const [
|
|
3276
|
-
nodes[
|
|
3275
|
+
for (const [path39, raw] of this.nodes) {
|
|
3276
|
+
nodes[path39] = {
|
|
3277
3277
|
commits: raw.commits,
|
|
3278
3278
|
churnLines: raw.churnLines,
|
|
3279
3279
|
bugCommits: raw.bugCommits,
|
|
@@ -3288,8 +3288,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3288
3288
|
*/
|
|
3289
3289
|
static load(s) {
|
|
3290
3290
|
const idx = new _ChurnIndex();
|
|
3291
|
-
for (const [
|
|
3292
|
-
idx.nodes.set(
|
|
3291
|
+
for (const [path39, raw] of Object.entries(s.nodes)) {
|
|
3292
|
+
idx.nodes.set(path39, {
|
|
3293
3293
|
commits: raw.commits,
|
|
3294
3294
|
churnLines: raw.churnLines,
|
|
3295
3295
|
bugCommits: raw.bugCommits,
|
|
@@ -3302,8 +3302,8 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3302
3302
|
// -------------------------------------------------------------------------
|
|
3303
3303
|
// Private helpers
|
|
3304
3304
|
// -------------------------------------------------------------------------
|
|
3305
|
-
getOrCreate(
|
|
3306
|
-
const existing = this.nodes.get(
|
|
3305
|
+
getOrCreate(path39) {
|
|
3306
|
+
const existing = this.nodes.get(path39);
|
|
3307
3307
|
if (existing !== void 0) return existing;
|
|
3308
3308
|
const fresh = {
|
|
3309
3309
|
commits: 0,
|
|
@@ -3312,7 +3312,7 @@ var ChurnIndex = class _ChurnIndex {
|
|
|
3312
3312
|
authorCounts: {},
|
|
3313
3313
|
lastTouch: 0
|
|
3314
3314
|
};
|
|
3315
|
-
this.nodes.set(
|
|
3315
|
+
this.nodes.set(path39, fresh);
|
|
3316
3316
|
return fresh;
|
|
3317
3317
|
}
|
|
3318
3318
|
};
|
|
@@ -3393,12 +3393,12 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3393
3393
|
*/
|
|
3394
3394
|
snapshot() {
|
|
3395
3395
|
const nodes = {};
|
|
3396
|
-
for (const [
|
|
3396
|
+
for (const [path39, raw] of this.nodes) {
|
|
3397
3397
|
const authorWeights = {};
|
|
3398
3398
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3399
3399
|
authorWeights[email] = { ...entry };
|
|
3400
3400
|
}
|
|
3401
|
-
nodes[
|
|
3401
|
+
nodes[path39] = { authorWeights, lastTouch: raw.lastTouch };
|
|
3402
3402
|
}
|
|
3403
3403
|
return { version: 1, nodes };
|
|
3404
3404
|
}
|
|
@@ -3407,23 +3407,23 @@ var OwnershipIndex = class _OwnershipIndex {
|
|
|
3407
3407
|
*/
|
|
3408
3408
|
static load(s) {
|
|
3409
3409
|
const idx = new _OwnershipIndex();
|
|
3410
|
-
for (const [
|
|
3410
|
+
for (const [path39, raw] of Object.entries(s.nodes)) {
|
|
3411
3411
|
const authorWeights = {};
|
|
3412
3412
|
for (const [email, entry] of Object.entries(raw.authorWeights)) {
|
|
3413
3413
|
authorWeights[email] = { ...entry };
|
|
3414
3414
|
}
|
|
3415
|
-
idx.nodes.set(
|
|
3415
|
+
idx.nodes.set(path39, { authorWeights, lastTouch: raw.lastTouch });
|
|
3416
3416
|
}
|
|
3417
3417
|
return idx;
|
|
3418
3418
|
}
|
|
3419
3419
|
// -------------------------------------------------------------------------
|
|
3420
3420
|
// Private helpers
|
|
3421
3421
|
// -------------------------------------------------------------------------
|
|
3422
|
-
getOrCreate(
|
|
3423
|
-
const existing = this.nodes.get(
|
|
3422
|
+
getOrCreate(path39) {
|
|
3423
|
+
const existing = this.nodes.get(path39);
|
|
3424
3424
|
if (existing !== void 0) return existing;
|
|
3425
3425
|
const fresh = { authorWeights: {}, lastTouch: 0 };
|
|
3426
|
-
this.nodes.set(
|
|
3426
|
+
this.nodes.set(path39, fresh);
|
|
3427
3427
|
return fresh;
|
|
3428
3428
|
}
|
|
3429
3429
|
};
|
|
@@ -3599,19 +3599,121 @@ function isEnoent(err) {
|
|
|
3599
3599
|
init_logger();
|
|
3600
3600
|
import path12 from "path";
|
|
3601
3601
|
import fs11 from "fs/promises";
|
|
3602
|
+
|
|
3603
|
+
// ../../packages/core/src/risk/scoring.ts
|
|
3604
|
+
var RISK_WEIGHTS = { churn: 0.4, bug: 0.3, coupling: 0.3 };
|
|
3605
|
+
var BAND_PCT = { critical: 0.05, high: 0.15, medium: 0.35 };
|
|
3606
|
+
var SCORE_FLOOR = 0.05;
|
|
3607
|
+
var SILO_BUS_FACTOR = 1;
|
|
3608
|
+
function percentile(values, p) {
|
|
3609
|
+
if (values.length === 0) return 0;
|
|
3610
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3611
|
+
const idx = Math.min(sorted.length - 1, Math.floor(sorted.length * p));
|
|
3612
|
+
return sorted[idx];
|
|
3613
|
+
}
|
|
3614
|
+
function computeRiskCaps(samples, p = 0.9) {
|
|
3615
|
+
return {
|
|
3616
|
+
churn: percentile(samples.map((s) => s.churnLines), p),
|
|
3617
|
+
coupling: percentile(samples.map((s) => s.couplingFanOut), p)
|
|
3618
|
+
};
|
|
3619
|
+
}
|
|
3620
|
+
function computeRiskBreakdown(m, caps) {
|
|
3621
|
+
const churnDenom = Math.max(caps.churn, 1);
|
|
3622
|
+
const couplingDenom = Math.max(caps.coupling, 1);
|
|
3623
|
+
return {
|
|
3624
|
+
churn: Math.min(1, m.churnLines / churnDenom),
|
|
3625
|
+
bugDensity: Math.min(1, m.bugDensity * 2),
|
|
3626
|
+
coupling: Math.min(1, m.couplingFanOut / couplingDenom)
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
function scoreFromBreakdown(b) {
|
|
3630
|
+
return b.churn * RISK_WEIGHTS.churn + b.bugDensity * RISK_WEIGHTS.bug + b.coupling * RISK_WEIGHTS.coupling;
|
|
3631
|
+
}
|
|
3632
|
+
function isSiloed(m) {
|
|
3633
|
+
return m.busFactor <= SILO_BUS_FACTOR;
|
|
3634
|
+
}
|
|
3635
|
+
function assignLabelsByPercentile(scores) {
|
|
3636
|
+
const n = scores.length;
|
|
3637
|
+
const bands = { criticalCount: 0, highCount: 0, mediumCount: 0, lowCount: 0, totalRanked: n };
|
|
3638
|
+
if (n === 0) return { labels: [], bands };
|
|
3639
|
+
const ranked = scores.map((s, i) => ({ s, i })).sort((a, b) => b.s - a.s);
|
|
3640
|
+
const criticalCutoff = Math.max(1, Math.ceil(n * BAND_PCT.critical));
|
|
3641
|
+
const highCutoff = Math.max(criticalCutoff + 1, Math.ceil(n * BAND_PCT.high));
|
|
3642
|
+
const mediumCutoff = Math.max(highCutoff + 1, Math.ceil(n * BAND_PCT.medium));
|
|
3643
|
+
const labels = new Array(n).fill("low");
|
|
3644
|
+
for (let rank = 0; rank < n; rank++) {
|
|
3645
|
+
const { s, i } = ranked[rank];
|
|
3646
|
+
if (s < SCORE_FLOOR) {
|
|
3647
|
+
labels[i] = "low";
|
|
3648
|
+
bands.lowCount++;
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
if (rank < criticalCutoff) {
|
|
3652
|
+
labels[i] = "critical";
|
|
3653
|
+
bands.criticalCount++;
|
|
3654
|
+
} else if (rank < highCutoff) {
|
|
3655
|
+
labels[i] = "high";
|
|
3656
|
+
bands.highCount++;
|
|
3657
|
+
} else if (rank < mediumCutoff) {
|
|
3658
|
+
labels[i] = "medium";
|
|
3659
|
+
bands.mediumCount++;
|
|
3660
|
+
} else {
|
|
3661
|
+
labels[i] = "low";
|
|
3662
|
+
bands.lowCount++;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return { labels, bands };
|
|
3666
|
+
}
|
|
3667
|
+
function scoreAll(samples) {
|
|
3668
|
+
const caps = computeRiskCaps(samples);
|
|
3669
|
+
const intermediate = samples.map((raw) => {
|
|
3670
|
+
const breakdown = computeRiskBreakdown(raw, caps);
|
|
3671
|
+
return { raw, breakdown, score: scoreFromBreakdown(breakdown) };
|
|
3672
|
+
});
|
|
3673
|
+
const { labels, bands } = assignLabelsByPercentile(intermediate.map((i) => i.score));
|
|
3674
|
+
const scored = intermediate.map((i, idx) => ({
|
|
3675
|
+
raw: i.raw,
|
|
3676
|
+
breakdown: i.breakdown,
|
|
3677
|
+
score: i.score,
|
|
3678
|
+
label: labels[idx],
|
|
3679
|
+
siloed: isSiloed(i.raw)
|
|
3680
|
+
}));
|
|
3681
|
+
return { scored, caps, bands };
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// ../../packages/core/src/trends/TrendsRecorder.ts
|
|
3602
3685
|
var FILE_SUBPATH = path12.join(".ctxloom", "trends", "snapshots.jsonl");
|
|
3686
|
+
var FILE_RISKS_SUBPATH = path12.join(".ctxloom", "trends", "file-risks.jsonl");
|
|
3603
3687
|
var ENTRY_PATTERN = /(^|\/)(index|main|server|app|cli)\.[^/]+$/;
|
|
3604
3688
|
var SEVEN_DAYS_SECONDS = 7 * 24 * 3600;
|
|
3605
3689
|
var COLLAPSE_WINDOW_SECONDS = 300;
|
|
3606
3690
|
var PERCENT_THRESHOLD = 0.01;
|
|
3691
|
+
var FILE_SCORE_DELTA = 0.02;
|
|
3607
3692
|
var INTEGER_FLOOR_FIELDS = [
|
|
3608
3693
|
"totalFiles",
|
|
3609
3694
|
"totalEdges",
|
|
3610
3695
|
"deadFiles",
|
|
3611
3696
|
"highRiskFiles"
|
|
3612
3697
|
];
|
|
3613
|
-
function
|
|
3614
|
-
const { graph, overlay
|
|
3698
|
+
function gatherPerFileMetrics(opts) {
|
|
3699
|
+
const { graph, overlay } = opts;
|
|
3700
|
+
return graph.allFiles().map((f) => {
|
|
3701
|
+
const churn = overlay.churn.statsFor(f);
|
|
3702
|
+
const ownership = overlay.ownership.statsFor(f);
|
|
3703
|
+
const coupled = overlay.coChange.topFor({ node: f, limit: 100, minConfidence: 0.1 });
|
|
3704
|
+
return {
|
|
3705
|
+
file: f,
|
|
3706
|
+
raw: {
|
|
3707
|
+
churnLines: churn?.churnLines ?? 0,
|
|
3708
|
+
bugDensity: churn?.bugDensity ?? 0,
|
|
3709
|
+
busFactor: ownership?.busFactor ?? 1,
|
|
3710
|
+
couplingFanOut: coupled.length
|
|
3711
|
+
}
|
|
3712
|
+
};
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
function computeAggregates(opts, unixSeconds) {
|
|
3716
|
+
const { graph, gitEnabled } = opts;
|
|
3615
3717
|
const files = graph.allFiles();
|
|
3616
3718
|
let deadFiles = 0;
|
|
3617
3719
|
for (const f of files) {
|
|
@@ -3620,51 +3722,62 @@ function computeMetrics(opts, unixSeconds) {
|
|
|
3620
3722
|
}
|
|
3621
3723
|
if (!gitEnabled) {
|
|
3622
3724
|
return {
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3725
|
+
metrics: {
|
|
3726
|
+
totalFiles: files.length,
|
|
3727
|
+
totalEdges: graph.edgeCount(),
|
|
3728
|
+
deadFiles,
|
|
3729
|
+
avgBusFactor: null,
|
|
3730
|
+
highRiskFiles: null,
|
|
3731
|
+
churnLinesLast7d: null
|
|
3732
|
+
},
|
|
3733
|
+
perFileScores: []
|
|
3629
3734
|
};
|
|
3630
3735
|
}
|
|
3631
3736
|
let busSum = 0;
|
|
3632
3737
|
let busCount = 0;
|
|
3633
|
-
for (const f of overlay.ownership.allNodes()) {
|
|
3634
|
-
const stats = overlay.ownership.statsFor(f);
|
|
3738
|
+
for (const f of opts.overlay.ownership.allNodes()) {
|
|
3739
|
+
const stats = opts.overlay.ownership.statsFor(f);
|
|
3635
3740
|
if (stats !== null) {
|
|
3636
3741
|
busSum += stats.busFactor;
|
|
3637
3742
|
busCount++;
|
|
3638
3743
|
}
|
|
3639
3744
|
}
|
|
3640
3745
|
const avgBusFactor = busCount > 0 ? busSum / busCount : 0;
|
|
3641
|
-
let highRiskFiles = 0;
|
|
3642
|
-
let churnLinesLast7d = 0;
|
|
3643
3746
|
const sevenDaysAgo = unixSeconds - SEVEN_DAYS_SECONDS;
|
|
3747
|
+
let churnLinesLast7d = 0;
|
|
3644
3748
|
for (const f of files) {
|
|
3645
|
-
const churn = overlay.churn.statsFor(f);
|
|
3646
|
-
const ownership = overlay.ownership.statsFor(f);
|
|
3647
|
-
const coupled = overlay.coChange.topFor({ node: f, limit: 100, minConfidence: 0.1 });
|
|
3648
|
-
const churnLines = churn?.churnLines ?? 0;
|
|
3649
|
-
const bugDensity = churn?.bugDensity ?? 0;
|
|
3650
|
-
const busFactor = ownership?.busFactor ?? 1;
|
|
3651
|
-
const churnPart = Math.min(1, churnLines / 1e3);
|
|
3652
|
-
const bugPart = Math.min(1, bugDensity * 2);
|
|
3653
|
-
const busPart = busFactor <= 1 ? 1 : busFactor <= 2 ? 0.5 : 0;
|
|
3654
|
-
const couplingPart = Math.min(1, coupled.length / 10);
|
|
3655
|
-
const score = churnPart * 0.3 + bugPart * 0.3 + busPart * 0.2 + couplingPart * 0.2;
|
|
3656
|
-
if (score > 0.6) highRiskFiles++;
|
|
3749
|
+
const churn = opts.overlay.churn.statsFor(f);
|
|
3657
3750
|
if (churn && churn.lastTouch >= sevenDaysAgo) {
|
|
3658
3751
|
churnLinesLast7d += churn.churnLines;
|
|
3659
3752
|
}
|
|
3660
3753
|
}
|
|
3754
|
+
const perFile = gatherPerFileMetrics(opts);
|
|
3755
|
+
const { scored } = scoreAll(perFile.map((p) => p.raw));
|
|
3756
|
+
let highRiskFiles = 0;
|
|
3757
|
+
const perFileScores = [];
|
|
3758
|
+
for (let i = 0; i < scored.length; i++) {
|
|
3759
|
+
const s = scored[i];
|
|
3760
|
+
const file = perFile[i].file;
|
|
3761
|
+
if (s.label === "critical" || s.label === "high") highRiskFiles++;
|
|
3762
|
+
if (s.score >= SCORE_FLOOR) {
|
|
3763
|
+
perFileScores.push({
|
|
3764
|
+
unixSeconds,
|
|
3765
|
+
file,
|
|
3766
|
+
score: Math.round(s.score * 100) / 100,
|
|
3767
|
+
label: s.label
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3661
3771
|
return {
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3772
|
+
metrics: {
|
|
3773
|
+
totalFiles: files.length,
|
|
3774
|
+
totalEdges: graph.edgeCount(),
|
|
3775
|
+
deadFiles,
|
|
3776
|
+
avgBusFactor: Math.round(avgBusFactor * 100) / 100,
|
|
3777
|
+
highRiskFiles,
|
|
3778
|
+
churnLinesLast7d
|
|
3779
|
+
},
|
|
3780
|
+
perFileScores
|
|
3668
3781
|
};
|
|
3669
3782
|
}
|
|
3670
3783
|
async function ensureDir(dir) {
|
|
@@ -3735,14 +3848,46 @@ async function overwriteLastLine(filePath, replacement) {
|
|
|
3735
3848
|
const head = idx >= 0 ? trimmed.slice(0, idx + 1) : "";
|
|
3736
3849
|
await fs11.writeFile(filePath, head + replacement + "\n");
|
|
3737
3850
|
}
|
|
3851
|
+
async function loadLastScoresPerFile(filePath) {
|
|
3852
|
+
const out = /* @__PURE__ */ new Map();
|
|
3853
|
+
try {
|
|
3854
|
+
const raw = await fs11.readFile(filePath, "utf-8");
|
|
3855
|
+
for (const line of raw.split("\n")) {
|
|
3856
|
+
if (!line.trim()) continue;
|
|
3857
|
+
try {
|
|
3858
|
+
const p = JSON.parse(line);
|
|
3859
|
+
if (typeof p?.file === "string" && typeof p.score === "number") {
|
|
3860
|
+
out.set(p.file, p);
|
|
3861
|
+
}
|
|
3862
|
+
} catch {
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
} catch {
|
|
3866
|
+
}
|
|
3867
|
+
return out;
|
|
3868
|
+
}
|
|
3869
|
+
async function appendFilteredPerFileScores(filePath, next, prev) {
|
|
3870
|
+
const lines = [];
|
|
3871
|
+
for (const p of next) {
|
|
3872
|
+
const last = prev.get(p.file);
|
|
3873
|
+
const labelChanged = last?.label !== p.label;
|
|
3874
|
+
const scoreMoved = last === void 0 || Math.abs(p.score - last.score) >= FILE_SCORE_DELTA;
|
|
3875
|
+
if (labelChanged || scoreMoved) lines.push(JSON.stringify(p));
|
|
3876
|
+
}
|
|
3877
|
+
if (lines.length === 0) return 0;
|
|
3878
|
+
await ensureDir(path12.dirname(filePath));
|
|
3879
|
+
await fs11.appendFile(filePath, lines.join("\n") + "\n");
|
|
3880
|
+
return lines.length;
|
|
3881
|
+
}
|
|
3738
3882
|
async function recordTrendSnapshot(opts) {
|
|
3739
3883
|
const now = opts.now ?? Date.now;
|
|
3740
3884
|
const ms = now();
|
|
3741
3885
|
const unixSeconds = Math.floor(ms / 1e3);
|
|
3742
3886
|
const timestamp2 = new Date(ms).toISOString();
|
|
3743
3887
|
const filePath = path12.join(opts.rootDir, FILE_SUBPATH);
|
|
3888
|
+
const fileRisksPath = path12.join(opts.rootDir, FILE_RISKS_SUBPATH);
|
|
3744
3889
|
try {
|
|
3745
|
-
const metrics =
|
|
3890
|
+
const { metrics, perFileScores } = computeAggregates(opts, unixSeconds);
|
|
3746
3891
|
const snapshot = {
|
|
3747
3892
|
timestamp: timestamp2,
|
|
3748
3893
|
unixSeconds,
|
|
@@ -3757,6 +3902,10 @@ async function recordTrendSnapshot(opts) {
|
|
|
3757
3902
|
} else {
|
|
3758
3903
|
await fs11.appendFile(filePath, JSON.stringify(snapshot) + "\n");
|
|
3759
3904
|
}
|
|
3905
|
+
if (perFileScores.length > 0) {
|
|
3906
|
+
const lastScores = await loadLastScoresPerFile(fileRisksPath);
|
|
3907
|
+
await appendFilteredPerFileScores(fileRisksPath, perFileScores, lastScores);
|
|
3908
|
+
}
|
|
3760
3909
|
return snapshot;
|
|
3761
3910
|
} catch (err) {
|
|
3762
3911
|
logger.warn("Failed to record trend snapshot", {
|
|
@@ -3819,8 +3968,59 @@ async function loadTrendSeries(opts) {
|
|
|
3819
3968
|
return { snapshots: tail, gitEnabled, totalCount };
|
|
3820
3969
|
}
|
|
3821
3970
|
|
|
3971
|
+
// ../../packages/core/src/trends/FileRiskStore.ts
|
|
3972
|
+
init_logger();
|
|
3973
|
+
import path14 from "path";
|
|
3974
|
+
import fs13 from "fs/promises";
|
|
3975
|
+
var FILE_SUBPATH3 = path14.join(".ctxloom", "trends", "file-risks.jsonl");
|
|
3976
|
+
var DEFAULT_LIMIT3 = 200;
|
|
3977
|
+
var VALID_LABELS = /* @__PURE__ */ new Set(["low", "medium", "high", "critical"]);
|
|
3978
|
+
function isValidPoint(value) {
|
|
3979
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3980
|
+
const v = value;
|
|
3981
|
+
return typeof v.unixSeconds === "number" && typeof v.file === "string" && typeof v.score === "number" && typeof v.label === "string" && VALID_LABELS.has(v.label);
|
|
3982
|
+
}
|
|
3983
|
+
async function loadFileRiskHistory(opts) {
|
|
3984
|
+
const filePath = path14.join(opts.rootDir, FILE_SUBPATH3);
|
|
3985
|
+
const limit = opts.limit ?? DEFAULT_LIMIT3;
|
|
3986
|
+
const sinceUnixSeconds = opts.sinceUnixSeconds ?? 0;
|
|
3987
|
+
let raw;
|
|
3988
|
+
try {
|
|
3989
|
+
raw = await fs13.readFile(filePath, "utf-8");
|
|
3990
|
+
} catch {
|
|
3991
|
+
return { file: opts.file, points: [], totalCount: 0 };
|
|
3992
|
+
}
|
|
3993
|
+
const allMatching = [];
|
|
3994
|
+
let totalCount = 0;
|
|
3995
|
+
let malformed = 0;
|
|
3996
|
+
for (const line of raw.split("\n")) {
|
|
3997
|
+
if (!line.trim()) continue;
|
|
3998
|
+
let value;
|
|
3999
|
+
try {
|
|
4000
|
+
value = JSON.parse(line);
|
|
4001
|
+
} catch {
|
|
4002
|
+
malformed++;
|
|
4003
|
+
continue;
|
|
4004
|
+
}
|
|
4005
|
+
if (!isValidPoint(value)) {
|
|
4006
|
+
malformed++;
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
if (value.file !== opts.file) continue;
|
|
4010
|
+
totalCount++;
|
|
4011
|
+
if (value.unixSeconds < sinceUnixSeconds) continue;
|
|
4012
|
+
allMatching.push(value);
|
|
4013
|
+
}
|
|
4014
|
+
if (malformed > 0) {
|
|
4015
|
+
logger.warn("Skipped malformed file-risk rows", { file: filePath, count: malformed });
|
|
4016
|
+
}
|
|
4017
|
+
allMatching.sort((a, b) => a.unixSeconds - b.unixSeconds);
|
|
4018
|
+
const tail = allMatching.length > limit ? allMatching.slice(allMatching.length - limit) : allMatching;
|
|
4019
|
+
return { file: opts.file, points: tail, totalCount };
|
|
4020
|
+
}
|
|
4021
|
+
|
|
3822
4022
|
// ../../packages/core/src/ast/Skeletonizer.ts
|
|
3823
|
-
import
|
|
4023
|
+
import fs14 from "fs";
|
|
3824
4024
|
var Skeletonizer = class {
|
|
3825
4025
|
parser;
|
|
3826
4026
|
constructor() {
|
|
@@ -3862,7 +4062,7 @@ var Skeletonizer = class {
|
|
|
3862
4062
|
* "deliberately skipped" from "skeletonized to empty".
|
|
3863
4063
|
*/
|
|
3864
4064
|
async skeletonize(filePath) {
|
|
3865
|
-
const fileSource =
|
|
4065
|
+
const fileSource = fs14.readFileSync(filePath, "utf-8");
|
|
3866
4066
|
const fileLines = fileSource.split("\n");
|
|
3867
4067
|
const MAX_INPUT_BYTES = 256 * 1024;
|
|
3868
4068
|
if (fileSource.length > MAX_INPUT_BYTES) {
|
|
@@ -3926,7 +4126,7 @@ ${methodLines.join("\n")}
|
|
|
3926
4126
|
*/
|
|
3927
4127
|
async skeletonizeXML(filePath) {
|
|
3928
4128
|
const nodes = await this.parser.parse(filePath);
|
|
3929
|
-
const fileLines =
|
|
4129
|
+
const fileLines = fs14.readFileSync(filePath, "utf-8").split("\n");
|
|
3930
4130
|
const parts = [`<skeleton file="${filePath}">`];
|
|
3931
4131
|
for (const node of nodes) {
|
|
3932
4132
|
switch (node.type) {
|
|
@@ -4461,8 +4661,8 @@ function getErrorMap() {
|
|
|
4461
4661
|
|
|
4462
4662
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
4463
4663
|
var makeIssue = (params) => {
|
|
4464
|
-
const { data, path:
|
|
4465
|
-
const fullPath = [...
|
|
4664
|
+
const { data, path: path39, errorMaps, issueData } = params;
|
|
4665
|
+
const fullPath = [...path39, ...issueData.path || []];
|
|
4466
4666
|
const fullIssue = {
|
|
4467
4667
|
...issueData,
|
|
4468
4668
|
path: fullPath
|
|
@@ -4578,11 +4778,11 @@ var errorUtil;
|
|
|
4578
4778
|
|
|
4579
4779
|
// ../../node_modules/zod/v3/types.js
|
|
4580
4780
|
var ParseInputLazyPath = class {
|
|
4581
|
-
constructor(parent, value,
|
|
4781
|
+
constructor(parent, value, path39, key) {
|
|
4582
4782
|
this._cachedPath = [];
|
|
4583
4783
|
this.parent = parent;
|
|
4584
4784
|
this.data = value;
|
|
4585
|
-
this._path =
|
|
4785
|
+
this._path = path39;
|
|
4586
4786
|
this._key = key;
|
|
4587
4787
|
}
|
|
4588
4788
|
get path() {
|
|
@@ -8035,14 +8235,14 @@ var Schema = external_exports.object({
|
|
|
8035
8235
|
var Schema2 = external_exports.object({ path: external_exports.string().describe("Relative path to the file") });
|
|
8036
8236
|
|
|
8037
8237
|
// ../../packages/core/src/tools/context-packet.ts
|
|
8038
|
-
import
|
|
8238
|
+
import path16 from "path";
|
|
8039
8239
|
var Schema3 = external_exports.object({
|
|
8040
8240
|
target_file: external_exports.string().describe("Relative path to the primary file"),
|
|
8041
8241
|
mode: external_exports.enum(["edit", "read"]).optional().default("edit").describe("Context mode")
|
|
8042
8242
|
});
|
|
8043
8243
|
|
|
8044
8244
|
// ../../packages/core/src/tools/findCallers.ts
|
|
8045
|
-
import
|
|
8245
|
+
import path17 from "path";
|
|
8046
8246
|
|
|
8047
8247
|
// ../../packages/core/src/tools/call-graph.ts
|
|
8048
8248
|
var Schema4 = external_exports.object({
|
|
@@ -8114,7 +8314,7 @@ var Schema11 = external_exports.object({
|
|
|
8114
8314
|
});
|
|
8115
8315
|
|
|
8116
8316
|
// ../../packages/core/src/tools/knowledge-gaps.ts
|
|
8117
|
-
import
|
|
8317
|
+
import path18 from "path";
|
|
8118
8318
|
var Schema12 = external_exports.object({
|
|
8119
8319
|
min_importers: external_exports.number().min(1).max(50).optional().default(3).describe(
|
|
8120
8320
|
"Minimum importers to qualify as an untested hub (default: 3)"
|
|
@@ -8174,8 +8374,8 @@ var Schema16 = external_exports.object({
|
|
|
8174
8374
|
});
|
|
8175
8375
|
|
|
8176
8376
|
// ../../packages/core/src/tools/refactor-preview.ts
|
|
8177
|
-
import
|
|
8178
|
-
import
|
|
8377
|
+
import fs16 from "fs";
|
|
8378
|
+
import path19 from "path";
|
|
8179
8379
|
var Schema17 = external_exports.object({
|
|
8180
8380
|
symbol: external_exports.string().min(1).describe("Symbol name to rename (exact match, case-sensitive)"),
|
|
8181
8381
|
new_name: external_exports.string().min(1).describe("New name for the symbol"),
|
|
@@ -8200,8 +8400,8 @@ var Schema18 = external_exports.object({
|
|
|
8200
8400
|
init_embedder();
|
|
8201
8401
|
init_VectorStore();
|
|
8202
8402
|
init_logger();
|
|
8203
|
-
import
|
|
8204
|
-
import
|
|
8403
|
+
import fs17 from "fs";
|
|
8404
|
+
import path20 from "path";
|
|
8205
8405
|
var Schema19 = external_exports.object({
|
|
8206
8406
|
query: external_exports.string().min(1).describe("Search query \u2014 natural language or code fragment"),
|
|
8207
8407
|
limit: external_exports.number().min(1).max(100).optional().default(10).describe(
|
|
@@ -8213,8 +8413,8 @@ var Schema19 = external_exports.object({
|
|
|
8213
8413
|
});
|
|
8214
8414
|
|
|
8215
8415
|
// ../../packages/core/src/tools/apply-refactor.ts
|
|
8216
|
-
import
|
|
8217
|
-
import
|
|
8416
|
+
import fs18 from "fs";
|
|
8417
|
+
import path21 from "path";
|
|
8218
8418
|
var Schema20 = external_exports.object({
|
|
8219
8419
|
symbol: external_exports.string().min(1).describe("Symbol name to rename (exact, case-sensitive)"),
|
|
8220
8420
|
new_name: external_exports.string().min(1).describe("New name for the symbol"),
|
|
@@ -8241,8 +8441,8 @@ var Schema21 = external_exports.object({
|
|
|
8241
8441
|
});
|
|
8242
8442
|
|
|
8243
8443
|
// ../../packages/core/src/tools/full-text-search.ts
|
|
8244
|
-
import
|
|
8245
|
-
import
|
|
8444
|
+
import fs19 from "fs";
|
|
8445
|
+
import path22 from "path";
|
|
8246
8446
|
var Schema22 = external_exports.object({
|
|
8247
8447
|
query: external_exports.string().min(1).describe("Search term \u2014 literal or /regex/"),
|
|
8248
8448
|
mode: external_exports.enum(["hybrid", "keyword", "semantic"]).optional().default("hybrid"),
|
|
@@ -8267,8 +8467,8 @@ var Schema24 = external_exports.object({
|
|
|
8267
8467
|
});
|
|
8268
8468
|
|
|
8269
8469
|
// ../../packages/core/src/tools/graph-snapshot.ts
|
|
8270
|
-
import
|
|
8271
|
-
import
|
|
8470
|
+
import fs20 from "fs";
|
|
8471
|
+
import path23 from "path";
|
|
8272
8472
|
var schema = external_exports.object({
|
|
8273
8473
|
name: external_exports.string().min(1).max(64).regex(/^[\w.-]+$/, "Name may only contain letters, digits, dots, underscores, hyphens").describe(
|
|
8274
8474
|
'Snapshot name (e.g. "before-refactor", "v1.0"). Used as the filename.'
|
|
@@ -8279,8 +8479,8 @@ var schema = external_exports.object({
|
|
|
8279
8479
|
});
|
|
8280
8480
|
|
|
8281
8481
|
// ../../packages/core/src/tools/graph-diff.ts
|
|
8282
|
-
import
|
|
8283
|
-
import
|
|
8482
|
+
import fs21 from "fs";
|
|
8483
|
+
import path24 from "path";
|
|
8284
8484
|
var schema2 = external_exports.object({
|
|
8285
8485
|
baseline: external_exports.string().min(1).describe('Name of the baseline snapshot (the "before" state).'),
|
|
8286
8486
|
current: external_exports.string().min(1).describe('Name of the current snapshot (the "after" state).')
|
|
@@ -8313,8 +8513,8 @@ var Schema26 = external_exports.object({
|
|
|
8313
8513
|
});
|
|
8314
8514
|
|
|
8315
8515
|
// ../../packages/core/src/rules/loadConfig.ts
|
|
8316
|
-
import
|
|
8317
|
-
import
|
|
8516
|
+
import fs22 from "fs/promises";
|
|
8517
|
+
import path25 from "path";
|
|
8318
8518
|
|
|
8319
8519
|
// ../../node_modules/js-yaml/dist/js-yaml.mjs
|
|
8320
8520
|
function isNothing(subject) {
|
|
@@ -10939,24 +11139,24 @@ var Schema27 = external_exports.object({
|
|
|
10939
11139
|
});
|
|
10940
11140
|
|
|
10941
11141
|
// ../../packages/core/src/tools/ruleManager.ts
|
|
10942
|
-
import
|
|
10943
|
-
import path25 from "path";
|
|
10944
|
-
|
|
10945
|
-
// ../../packages/core/src/review/AuthorResolver.ts
|
|
10946
|
-
import fs23 from "fs/promises";
|
|
11142
|
+
import fs23 from "fs";
|
|
10947
11143
|
import path26 from "path";
|
|
10948
11144
|
|
|
10949
|
-
// ../../packages/core/src/review/
|
|
11145
|
+
// ../../packages/core/src/review/AuthorResolver.ts
|
|
10950
11146
|
import fs24 from "fs/promises";
|
|
10951
11147
|
import path27 from "path";
|
|
10952
11148
|
|
|
10953
|
-
// ../../packages/core/src/review/
|
|
11149
|
+
// ../../packages/core/src/review/CodeownersWriter.ts
|
|
10954
11150
|
import fs25 from "fs/promises";
|
|
10955
11151
|
import path28 from "path";
|
|
10956
11152
|
|
|
10957
|
-
// ../../packages/core/src/
|
|
11153
|
+
// ../../packages/core/src/review/loadConfig.ts
|
|
11154
|
+
import fs26 from "fs/promises";
|
|
10958
11155
|
import path29 from "path";
|
|
10959
|
-
|
|
11156
|
+
|
|
11157
|
+
// ../../packages/core/src/security/PathValidator.ts
|
|
11158
|
+
import path30 from "path";
|
|
11159
|
+
import fs27 from "fs";
|
|
10960
11160
|
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
10961
11161
|
|
|
10962
11162
|
// ../../packages/core/src/index.ts
|
|
@@ -10967,7 +11167,7 @@ init_logger();
|
|
|
10967
11167
|
|
|
10968
11168
|
// ../../packages/core/src/license/LicenseStore.ts
|
|
10969
11169
|
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSync } from "fs";
|
|
10970
|
-
import
|
|
11170
|
+
import path31 from "path";
|
|
10971
11171
|
|
|
10972
11172
|
// ../../packages/core/src/license/types.ts
|
|
10973
11173
|
var FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/;
|
|
@@ -11003,7 +11203,7 @@ var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (typeof __TELEMETRY_SENTRY_DSN__ =
|
|
|
11003
11203
|
|
|
11004
11204
|
// server/loader.ts
|
|
11005
11205
|
async function loadContext(root) {
|
|
11006
|
-
const absRoot =
|
|
11206
|
+
const absRoot = path32.resolve(root);
|
|
11007
11207
|
const overlay = new GitOverlayStore(absRoot);
|
|
11008
11208
|
const gitEnabled = await overlay.loadSnapshot();
|
|
11009
11209
|
const graph = new DependencyGraph();
|
|
@@ -11038,42 +11238,6 @@ async function switchContext(ctx, newRoot) {
|
|
|
11038
11238
|
|
|
11039
11239
|
// server/routes/overview.ts
|
|
11040
11240
|
import { Router } from "express";
|
|
11041
|
-
|
|
11042
|
-
// server/lib/risk.ts
|
|
11043
|
-
var RISK_WEIGHTS = { churn: 0.2, bug: 0.2, bus: 0.4, coupling: 0.2 };
|
|
11044
|
-
function percentile(values, p) {
|
|
11045
|
-
if (values.length === 0) return 0;
|
|
11046
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
11047
|
-
const idx = Math.min(sorted.length - 1, Math.floor(sorted.length * p));
|
|
11048
|
-
return sorted[idx];
|
|
11049
|
-
}
|
|
11050
|
-
function computeRiskCaps(samples, p = 0.9) {
|
|
11051
|
-
return {
|
|
11052
|
-
churn: percentile(samples.map((s) => s.churnLines), p),
|
|
11053
|
-
coupling: percentile(samples.map((s) => s.couplingFanOut), p)
|
|
11054
|
-
};
|
|
11055
|
-
}
|
|
11056
|
-
function computeRiskBreakdown(m, caps) {
|
|
11057
|
-
const churnDenom = Math.max(caps.churn, 1);
|
|
11058
|
-
const couplingDenom = Math.max(caps.coupling, 1);
|
|
11059
|
-
return {
|
|
11060
|
-
churn: Math.min(1, m.churnLines / churnDenom),
|
|
11061
|
-
bugDensity: Math.min(1, m.bugDensity * 2),
|
|
11062
|
-
busFactor: Math.min(1, 1 / Math.max(1, m.busFactor)),
|
|
11063
|
-
coupling: Math.min(1, m.couplingFanOut / couplingDenom)
|
|
11064
|
-
};
|
|
11065
|
-
}
|
|
11066
|
-
function scoreFromBreakdown(b) {
|
|
11067
|
-
return b.churn * RISK_WEIGHTS.churn + b.bugDensity * RISK_WEIGHTS.bug + b.busFactor * RISK_WEIGHTS.bus + b.coupling * RISK_WEIGHTS.coupling;
|
|
11068
|
-
}
|
|
11069
|
-
function riskLabel(score) {
|
|
11070
|
-
if (score > 0.8) return "critical";
|
|
11071
|
-
if (score > 0.6) return "high";
|
|
11072
|
-
if (score > 0.3) return "medium";
|
|
11073
|
-
return "low";
|
|
11074
|
-
}
|
|
11075
|
-
|
|
11076
|
-
// server/routes/overview.ts
|
|
11077
11241
|
function buildOverviewRouter(ctx) {
|
|
11078
11242
|
const router = Router();
|
|
11079
11243
|
router.get("/", (_req, res) => {
|
|
@@ -11101,10 +11265,9 @@ function buildOverviewRouter(ctx) {
|
|
|
11101
11265
|
};
|
|
11102
11266
|
});
|
|
11103
11267
|
const caps = computeRiskCaps(raw);
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
}
|
|
11268
|
+
const scores = raw.map((m) => scoreFromBreakdown(computeRiskBreakdown(m, caps)));
|
|
11269
|
+
const { labels } = assignLabelsByPercentile(scores);
|
|
11270
|
+
for (const label of labels) risk[label]++;
|
|
11108
11271
|
}
|
|
11109
11272
|
const body = {
|
|
11110
11273
|
totalFiles: files.length,
|
|
@@ -11170,7 +11333,12 @@ function buildRiskRouter(ctx) {
|
|
|
11170
11333
|
router.get("/", (_req, res) => {
|
|
11171
11334
|
const { graph, overlay, gitEnabled } = ctx;
|
|
11172
11335
|
if (!gitEnabled) {
|
|
11173
|
-
const body = {
|
|
11336
|
+
const body = {
|
|
11337
|
+
entries: [],
|
|
11338
|
+
overallRiskScore: 0,
|
|
11339
|
+
caps: { churn: 0, coupling: 0 },
|
|
11340
|
+
bands: { criticalCount: 0, highCount: 0, mediumCount: 0, lowCount: 0, totalRanked: 0 }
|
|
11341
|
+
};
|
|
11174
11342
|
return res.json(body);
|
|
11175
11343
|
}
|
|
11176
11344
|
const files = graph.allFiles();
|
|
@@ -11188,24 +11356,26 @@ function buildRiskRouter(ctx) {
|
|
|
11188
11356
|
};
|
|
11189
11357
|
});
|
|
11190
11358
|
const caps = computeRiskCaps(raw);
|
|
11191
|
-
const
|
|
11359
|
+
const scored = raw.map((m) => {
|
|
11192
11360
|
const breakdown = computeRiskBreakdown(m, caps);
|
|
11193
|
-
|
|
11194
|
-
return {
|
|
11195
|
-
file: m.file,
|
|
11196
|
-
riskScore: Math.round(score * 100) / 100,
|
|
11197
|
-
riskLabel: riskLabel(score),
|
|
11198
|
-
churnLines: m.churnLines,
|
|
11199
|
-
bugDensity: m.bugDensity,
|
|
11200
|
-
busFactor: m.busFactor,
|
|
11201
|
-
topOwner: m.topOwner,
|
|
11202
|
-
couplingFanOut: m.couplingFanOut,
|
|
11203
|
-
breakdown
|
|
11204
|
-
};
|
|
11361
|
+
return { m, breakdown, score: scoreFromBreakdown(breakdown) };
|
|
11205
11362
|
});
|
|
11363
|
+
const { labels, bands } = assignLabelsByPercentile(scored.map((s) => s.score));
|
|
11364
|
+
const entries = scored.map(({ m, breakdown, score }, idx) => ({
|
|
11365
|
+
file: m.file,
|
|
11366
|
+
riskScore: Math.round(score * 100) / 100,
|
|
11367
|
+
riskLabel: labels[idx],
|
|
11368
|
+
churnLines: m.churnLines,
|
|
11369
|
+
bugDensity: m.bugDensity,
|
|
11370
|
+
busFactor: m.busFactor,
|
|
11371
|
+
topOwner: m.topOwner,
|
|
11372
|
+
couplingFanOut: m.couplingFanOut,
|
|
11373
|
+
breakdown,
|
|
11374
|
+
siloed: isSiloed(m)
|
|
11375
|
+
}));
|
|
11206
11376
|
entries.sort((a, b) => b.riskScore - a.riskScore);
|
|
11207
11377
|
const overallRiskScore = entries.length > 0 ? Math.round(entries.reduce((s, e) => s + e.riskScore, 0) / entries.length * 100) / 100 : 0;
|
|
11208
|
-
res.json({ entries, overallRiskScore, caps });
|
|
11378
|
+
res.json({ entries, overallRiskScore, caps, bands });
|
|
11209
11379
|
});
|
|
11210
11380
|
return router;
|
|
11211
11381
|
}
|
|
@@ -11286,21 +11456,21 @@ function buildOwnershipRouter(ctx) {
|
|
|
11286
11456
|
|
|
11287
11457
|
// server/routes/file.ts
|
|
11288
11458
|
import { Router as Router7 } from "express";
|
|
11289
|
-
import
|
|
11290
|
-
import
|
|
11459
|
+
import fs28 from "fs/promises";
|
|
11460
|
+
import path33 from "path";
|
|
11291
11461
|
function buildFileRouter(ctx) {
|
|
11292
11462
|
const router = Router7();
|
|
11293
11463
|
router.get("/", async (req, res) => {
|
|
11294
11464
|
const rel = req.query.path;
|
|
11295
11465
|
if (!rel) return res.status(400).json({ error: "missing path" });
|
|
11296
|
-
const abs =
|
|
11297
|
-
const rootBoundary = ctx.root.endsWith(
|
|
11466
|
+
const abs = path33.resolve(ctx.root, rel);
|
|
11467
|
+
const rootBoundary = ctx.root.endsWith(path33.sep) ? ctx.root : ctx.root + path33.sep;
|
|
11298
11468
|
if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
|
|
11299
11469
|
return res.status(403).json({ error: "forbidden" });
|
|
11300
11470
|
}
|
|
11301
11471
|
try {
|
|
11302
|
-
const content = await
|
|
11303
|
-
const ext =
|
|
11472
|
+
const content = await fs28.readFile(abs, "utf-8");
|
|
11473
|
+
const ext = path33.extname(abs).slice(1);
|
|
11304
11474
|
res.json({ content, lines: content.split("\n").length, ext });
|
|
11305
11475
|
} catch {
|
|
11306
11476
|
res.status(404).json({ error: "not found" });
|
|
@@ -11312,7 +11482,7 @@ function buildFileRouter(ctx) {
|
|
|
11312
11482
|
// server/routes/open.ts
|
|
11313
11483
|
import { Router as Router8 } from "express";
|
|
11314
11484
|
import { execFile as execFile2 } from "child_process";
|
|
11315
|
-
import
|
|
11485
|
+
import path34 from "path";
|
|
11316
11486
|
function tryOpen(bin, abs) {
|
|
11317
11487
|
return new Promise((resolve) => {
|
|
11318
11488
|
execFile2(bin, [abs], { timeout: 5e3 }, (err) => resolve(!err));
|
|
@@ -11323,8 +11493,8 @@ function buildOpenRouter(ctx) {
|
|
|
11323
11493
|
router.post("/", async (req, res) => {
|
|
11324
11494
|
const rel = req.body?.path;
|
|
11325
11495
|
if (!rel || typeof rel !== "string") return res.status(400).json({ error: "missing path" });
|
|
11326
|
-
const abs =
|
|
11327
|
-
const rootBoundary = ctx.root.endsWith(
|
|
11496
|
+
const abs = path34.resolve(ctx.root, rel);
|
|
11497
|
+
const rootBoundary = ctx.root.endsWith(path34.sep) ? ctx.root : ctx.root + path34.sep;
|
|
11328
11498
|
if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
|
|
11329
11499
|
return res.status(403).json({ error: "forbidden" });
|
|
11330
11500
|
}
|
|
@@ -11336,8 +11506,8 @@ function buildOpenRouter(ctx) {
|
|
|
11336
11506
|
|
|
11337
11507
|
// server/routes/tokens.ts
|
|
11338
11508
|
import { Router as Router9 } from "express";
|
|
11339
|
-
import
|
|
11340
|
-
import
|
|
11509
|
+
import path35 from "path";
|
|
11510
|
+
import fs29 from "fs";
|
|
11341
11511
|
var CHARS_PER_TOKEN = 4;
|
|
11342
11512
|
var cache = null;
|
|
11343
11513
|
function buildTokensRouter(ctx) {
|
|
@@ -11352,9 +11522,9 @@ function buildTokensRouter(ctx) {
|
|
|
11352
11522
|
let fullChars = 0;
|
|
11353
11523
|
let skeletonChars = 0;
|
|
11354
11524
|
for (const file of files) {
|
|
11355
|
-
const absPath =
|
|
11525
|
+
const absPath = path35.join(ctx.root, file);
|
|
11356
11526
|
try {
|
|
11357
|
-
const content =
|
|
11527
|
+
const content = fs29.readFileSync(absPath, "utf-8");
|
|
11358
11528
|
fullChars += content.length;
|
|
11359
11529
|
const skeleton = await skeletonizer.skeletonize(absPath);
|
|
11360
11530
|
skeletonChars += skeleton.length;
|
|
@@ -11406,19 +11576,66 @@ function buildTrendsRouter(ctx) {
|
|
|
11406
11576
|
return router;
|
|
11407
11577
|
}
|
|
11408
11578
|
|
|
11409
|
-
// server/routes/
|
|
11579
|
+
// server/routes/file-trends.ts
|
|
11410
11580
|
import { Router as Router11 } from "express";
|
|
11411
|
-
|
|
11581
|
+
var RANGE_TO_SECONDS2 = {
|
|
11582
|
+
"7d": 7 * 24 * 3600,
|
|
11583
|
+
"30d": 30 * 24 * 3600,
|
|
11584
|
+
"90d": 90 * 24 * 3600
|
|
11585
|
+
};
|
|
11586
|
+
function parseRange2(raw) {
|
|
11587
|
+
if (raw === "7d" || raw === "30d" || raw === "90d" || raw === "all") return raw;
|
|
11588
|
+
return "90d";
|
|
11589
|
+
}
|
|
11590
|
+
function parseLimit2(raw) {
|
|
11591
|
+
const n = typeof raw === "string" ? Number.parseInt(raw, 10) : NaN;
|
|
11592
|
+
if (!Number.isFinite(n) || n < 1) return 200;
|
|
11593
|
+
return Math.min(n, 2e3);
|
|
11594
|
+
}
|
|
11595
|
+
function buildFileTrendsRouter(ctx) {
|
|
11596
|
+
const router = Router11();
|
|
11597
|
+
router.get("/file", async (req, res) => {
|
|
11598
|
+
const filePath = typeof req.query.path === "string" ? req.query.path : "";
|
|
11599
|
+
if (!filePath) {
|
|
11600
|
+
return res.status(400).json({ error: "missing required query param 'path'" });
|
|
11601
|
+
}
|
|
11602
|
+
const range = parseRange2(req.query.range);
|
|
11603
|
+
const limit = parseLimit2(req.query.limit);
|
|
11604
|
+
const sinceUnixSeconds = range === "all" ? 0 : Math.floor(Date.now() / 1e3) - RANGE_TO_SECONDS2[range];
|
|
11605
|
+
const history = await loadFileRiskHistory({
|
|
11606
|
+
rootDir: ctx.root,
|
|
11607
|
+
file: filePath,
|
|
11608
|
+
sinceUnixSeconds,
|
|
11609
|
+
limit
|
|
11610
|
+
});
|
|
11611
|
+
const body = {
|
|
11612
|
+
file: history.file,
|
|
11613
|
+
points: history.points.map((p) => ({
|
|
11614
|
+
unixSeconds: p.unixSeconds,
|
|
11615
|
+
score: p.score,
|
|
11616
|
+
label: p.label
|
|
11617
|
+
})),
|
|
11618
|
+
totalCount: history.totalCount,
|
|
11619
|
+
gitEnabled: ctx.gitEnabled
|
|
11620
|
+
};
|
|
11621
|
+
res.json(body);
|
|
11622
|
+
});
|
|
11623
|
+
return router;
|
|
11624
|
+
}
|
|
11625
|
+
|
|
11626
|
+
// server/routes/projects.ts
|
|
11627
|
+
import { Router as Router12 } from "express";
|
|
11628
|
+
import path37 from "path";
|
|
11412
11629
|
|
|
11413
11630
|
// server/projects.ts
|
|
11414
11631
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
11415
11632
|
import os4 from "os";
|
|
11416
|
-
import
|
|
11633
|
+
import path36 from "path";
|
|
11417
11634
|
import crypto5 from "crypto";
|
|
11418
11635
|
var HOME = os4.homedir();
|
|
11419
|
-
var REGISTRY_PATH =
|
|
11636
|
+
var REGISTRY_PATH = path36.join(HOME, ".ctxloom", "repos.json");
|
|
11420
11637
|
function slugFor(root) {
|
|
11421
|
-
const abs =
|
|
11638
|
+
const abs = path36.resolve(root);
|
|
11422
11639
|
return crypto5.createHash("sha1").update(abs).digest("hex").slice(0, 12);
|
|
11423
11640
|
}
|
|
11424
11641
|
function readRegistry() {
|
|
@@ -11433,27 +11650,27 @@ function readRegistry() {
|
|
|
11433
11650
|
}
|
|
11434
11651
|
}
|
|
11435
11652
|
function listProjects(defaultRoot) {
|
|
11436
|
-
const absDefault =
|
|
11653
|
+
const absDefault = path36.resolve(defaultRoot);
|
|
11437
11654
|
const out = [
|
|
11438
11655
|
{
|
|
11439
11656
|
slug: slugFor(absDefault),
|
|
11440
|
-
name:
|
|
11657
|
+
name: path36.basename(absDefault) || absDefault,
|
|
11441
11658
|
root: absDefault,
|
|
11442
11659
|
isDefault: true,
|
|
11443
|
-
hasSnapshot: existsSync2(
|
|
11660
|
+
hasSnapshot: existsSync2(path36.join(absDefault, ".ctxloom"))
|
|
11444
11661
|
}
|
|
11445
11662
|
];
|
|
11446
11663
|
const seen = /* @__PURE__ */ new Set([absDefault]);
|
|
11447
11664
|
for (const entry of readRegistry()) {
|
|
11448
|
-
const abs =
|
|
11665
|
+
const abs = path36.resolve(entry.root);
|
|
11449
11666
|
if (seen.has(abs)) continue;
|
|
11450
11667
|
seen.add(abs);
|
|
11451
11668
|
out.push({
|
|
11452
11669
|
slug: slugFor(abs),
|
|
11453
|
-
name: entry.name ?? (
|
|
11670
|
+
name: entry.name ?? (path36.basename(abs) || abs),
|
|
11454
11671
|
root: abs,
|
|
11455
11672
|
isDefault: false,
|
|
11456
|
-
hasSnapshot: existsSync2(
|
|
11673
|
+
hasSnapshot: existsSync2(path36.join(abs, ".ctxloom"))
|
|
11457
11674
|
});
|
|
11458
11675
|
}
|
|
11459
11676
|
return out;
|
|
@@ -11462,7 +11679,7 @@ function listProjects(defaultRoot) {
|
|
|
11462
11679
|
// server/routes/projects.ts
|
|
11463
11680
|
function buildProjectsRouter(deps) {
|
|
11464
11681
|
const { ctx, defaultRoot, onActiveChanged } = deps;
|
|
11465
|
-
const router =
|
|
11682
|
+
const router = Router12();
|
|
11466
11683
|
function annotated() {
|
|
11467
11684
|
const activeSlug = slugFor(ctx.root);
|
|
11468
11685
|
return listProjects(defaultRoot).map((p) => ({
|
|
@@ -11504,7 +11721,7 @@ function buildProjectsRouter(deps) {
|
|
|
11504
11721
|
} catch (err) {
|
|
11505
11722
|
const detail = err instanceof Error ? err.message : String(err);
|
|
11506
11723
|
res.status(500).json({
|
|
11507
|
-
error: `failed to switch to ${
|
|
11724
|
+
error: `failed to switch to ${path37.basename(target.root)}: ${detail}`
|
|
11508
11725
|
});
|
|
11509
11726
|
}
|
|
11510
11727
|
});
|
|
@@ -11512,7 +11729,7 @@ function buildProjectsRouter(deps) {
|
|
|
11512
11729
|
}
|
|
11513
11730
|
|
|
11514
11731
|
// server/index.ts
|
|
11515
|
-
var __dirname2 =
|
|
11732
|
+
var __dirname2 = path38.dirname(fileURLToPath2(import.meta.url));
|
|
11516
11733
|
async function startDashboard(options) {
|
|
11517
11734
|
const { root, port, open } = options;
|
|
11518
11735
|
console.log(`ctxloom dashboard \u2014 loading context from ${root}...`);
|
|
@@ -11541,6 +11758,7 @@ async function startDashboard(options) {
|
|
|
11541
11758
|
app.use("/api/open", buildOpenRouter(ctx));
|
|
11542
11759
|
app.use("/api/tokens", buildTokensRouter(ctx));
|
|
11543
11760
|
app.use("/api/trends", buildTrendsRouter(ctx));
|
|
11761
|
+
app.use("/api/trends", buildFileTrendsRouter(ctx));
|
|
11544
11762
|
app.get("/api/health", (_req, res) => res.json({ ok: true, gitEnabled: ctx.gitEnabled }));
|
|
11545
11763
|
app.get("/api/status", (_req, res) => res.json({
|
|
11546
11764
|
lastIndexed: ctx.lastIndexed.toISOString(),
|
|
@@ -11570,9 +11788,9 @@ async function startDashboard(options) {
|
|
|
11570
11788
|
}
|
|
11571
11789
|
activeWatcher = null;
|
|
11572
11790
|
}
|
|
11573
|
-
const snapshotDir =
|
|
11791
|
+
const snapshotDir = path38.join(targetRoot, ".ctxloom");
|
|
11574
11792
|
try {
|
|
11575
|
-
activeWatcher =
|
|
11793
|
+
activeWatcher = fs30.watch(snapshotDir, (_event, filename) => {
|
|
11576
11794
|
if (!filename || !filename.includes("snapshot")) return;
|
|
11577
11795
|
if (debounce) clearTimeout(debounce);
|
|
11578
11796
|
debounce = setTimeout(async () => {
|
|
@@ -11600,11 +11818,20 @@ async function startDashboard(options) {
|
|
|
11600
11818
|
attachSnapshotWatcher(newRoot);
|
|
11601
11819
|
}
|
|
11602
11820
|
}));
|
|
11603
|
-
const clientDist =
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11821
|
+
const clientDist = path38.join(__dirname2, "../dashboard/client");
|
|
11822
|
+
const clientDistExists = fs30.existsSync(path38.join(clientDist, "index.html"));
|
|
11823
|
+
if (clientDistExists) {
|
|
11824
|
+
app.use(express.static(clientDist, { dotfiles: "allow" }));
|
|
11825
|
+
app.get(/.*/, (_req, res) => {
|
|
11826
|
+
res.sendFile(path38.join(clientDist, "index.html"), { dotfiles: "allow" });
|
|
11827
|
+
});
|
|
11828
|
+
} else {
|
|
11829
|
+
app.get(/^\/(?!api\/).*/, (_req, res) => {
|
|
11830
|
+
res.status(404).type("text/plain").send(
|
|
11831
|
+
"Dashboard client bundle not found. In dev: open http://localhost:5173. For a production preview from this port, run `npm run build:client -w @ctxloom/dashboard`."
|
|
11832
|
+
);
|
|
11833
|
+
});
|
|
11834
|
+
}
|
|
11608
11835
|
app.listen(port, () => {
|
|
11609
11836
|
const url = `http://localhost:${port}`;
|
|
11610
11837
|
console.log(`
|