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.
@@ -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 path29 of paths) {
3410
- this.nodeCounts.set(path29, (this.nodeCounts.get(path29) ?? 0) + 1);
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 [path29, raw] of this.nodes) {
3558
- nodes[path29] = {
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 [path29, raw] of Object.entries(s.nodes)) {
3574
- idx.nodes.set(path29, {
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(path29) {
3588
- const existing = this.nodes.get(path29);
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(path29, fresh);
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 [path29, raw] of this.nodes) {
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[path29] = { authorWeights, lastTouch: raw.lastTouch };
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 [path29, raw] of Object.entries(s.nodes)) {
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(path29, { authorWeights, lastTouch: raw.lastTouch });
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(path29) {
3705
- const existing = this.nodes.get(path29);
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(path29, fresh);
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 computeMetrics(opts, unixSeconds) {
3895
- const { graph, overlay, gitEnabled } = opts;
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
- totalFiles: files.length,
3905
- totalEdges: graph.edgeCount(),
3906
- deadFiles,
3907
- avgBusFactor: null,
3908
- highRiskFiles: null,
3909
- churnLinesLast7d: null
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
- totalFiles: files.length,
3944
- totalEdges: graph.edgeCount(),
3945
- deadFiles,
3946
- avgBusFactor: Math.round(avgBusFactor * 100) / 100,
3947
- highRiskFiles,
3948
- churnLinesLast7d
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 = computeMetrics(opts, unixSeconds);
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 fs12 from "fs";
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 = fs12.readFileSync(filePath, "utf-8");
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 = fs12.readFileSync(filePath, "utf-8").split("\n");
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 path13 from "path";
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 = path13.resolve(ctx.projectRoot, dep);
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 path14 from "path";
4621
+ import path15 from "path";
4423
4622
  function escapeXML3(text) {
4424
4623
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 path15 from "path";
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 = path15.basename(tf).replace(/\.(test|spec)\.[^.]+$/, "").replace(/\.[^.]+$/, "");
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 = path15.basename(file).replace(/\.[^.]+$/, "");
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 fs13 from "fs";
5712
- import path16 from "path";
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 = fs13.readFileSync(filePath, "utf-8");
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 = path16.join(ctx.projectRoot, relPath);
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 fs14 from "fs";
5957
- import path17 from "path";
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 (!fs14.existsSync(this.filePath)) return [];
5968
- return JSON.parse(fs14.readFileSync(this.filePath, "utf-8"));
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 = path17.dirname(this.filePath);
5975
- if (!fs14.existsSync(dir)) fs14.mkdirSync(dir, { recursive: true });
5976
- fs14.writeFileSync(this.filePath, JSON.stringify(this.repos, null, 2), "utf-8");
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: path17.basename(root),
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6012
6211
  }
6013
6212
  function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
6014
- const repoRegistryPath = registryFilePath ?? path17.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".ctxloom", "repos.json");
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 fs15 from "fs";
6118
- import path18 from "path";
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 = fs15.readFileSync(absPath, "utf-8");
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
- fs15.writeFileSync(absPath, content.replace(regex, newName), "utf-8");
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 = path18.join(ctx.projectRoot, relPath);
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 fs16 from "fs";
6318
- import path19 from "path";
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 = fs16.readFileSync(absPath, "utf-8");
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 = path19.join(ctx.projectRoot, relPath);
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 fs17 from "fs";
6630
- import path20 from "path";
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 = path20.resolve(rootDir, ".ctxloom", "snapshots");
6642
- fs17.mkdirSync(snapshotsDir, { recursive: true });
6643
- const snapshotPath = path20.resolve(snapshotsDir, `${name}.json`);
6644
- if (!snapshotPath.startsWith(snapshotsDir + path20.sep)) {
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 (fs17.existsSync(snapshotPath) && !overwrite) {
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
- fs17.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
6664
- fs17.renameSync(tmp, snapshotPath);
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 = path20.join(rootDir, ".ctxloom", "snapshots");
6668
- if (!fs17.existsSync(snapshotsDir)) return [];
6669
- return fs17.readdirSync(snapshotsDir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).sort();
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 fs18 from "fs";
6718
- import path21 from "path";
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 = path21.resolve(rootDir, ".ctxloom", "snapshots");
6726
- const snapshotPath = path21.resolve(snapshotsDir, `${name}.json`);
6727
- if (!snapshotPath.startsWith(snapshotsDir + path21.sep)) {
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 (!fs18.existsSync(snapshotPath)) {
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(fs18.readFileSync(snapshotPath, "utf-8"));
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 fs19 from "fs/promises";
7100
- import path22 from "path";
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 = path22.join(rootDir, ".ctxloom", "rules.yml");
7314
+ const filePath = path23.join(rootDir, ".ctxloom", "rules.yml");
7116
7315
  let raw;
7117
7316
  try {
7118
- raw = await fs19.readFile(filePath, "utf-8");
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 fs20 from "fs";
7484
- import path23 from "path";
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 = path23.join(this.projectRoot, ruleFile);
7707
+ const fullPath = path24.join(this.projectRoot, ruleFile);
7509
7708
  try {
7510
7709
  this.pathValidator.validate(fullPath);
7511
- if (fs20.existsSync(fullPath)) {
7512
- const stat = fs20.statSync(fullPath);
7710
+ if (fs21.existsSync(fullPath)) {
7711
+ const stat = fs21.statSync(fullPath);
7513
7712
  if (stat.isFile()) {
7514
- const content = fs20.readFileSync(fullPath, "utf-8");
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 = fs20.readdirSync(fullPath);
7720
+ const dirEntries = fs21.readdirSync(fullPath);
7522
7721
  for (const entry of dirEntries) {
7523
- const entryPath = path23.join(fullPath, entry);
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 = fs20.statSync(entryPath);
7728
+ const entryStat = fs21.statSync(entryPath);
7530
7729
  if (entryStat.isFile()) {
7531
- const content = fs20.readFileSync(entryPath, "utf-8");
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 fs21 from "fs/promises";
7575
- import path24 from "path";
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 fs21.writeFile(
7601
- path24.join(this.ctxloomDir, "authors-cache.json"),
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 = path24.join(this.ctxloomDir, "authors.yml");
7809
+ const file = path25.join(this.ctxloomDir, "authors.yml");
7611
7810
  try {
7612
- const raw = await fs21.readFile(file, "utf8");
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 = path24.join(this.ctxloomDir, "authors-cache.json");
7820
+ const file = path25.join(this.ctxloomDir, "authors-cache.json");
7622
7821
  try {
7623
- const raw = await fs21.readFile(file, "utf8");
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 fs22 from "fs/promises";
7649
- import path25 from "path";
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 fs22.readFile(codeownersPath, "utf8");
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 fs22.mkdir(path25.dirname(codeownersPath), { recursive: true });
7689
- await fs22.writeFile(codeownersPath, content, "utf8");
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 fs23 from "fs/promises";
7875
- import path26 from "path";
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 = path26.join(root, ".ctxloom", "review.yml");
8085
+ const file = path27.join(root, ".ctxloom", "review.yml");
7887
8086
  try {
7888
- const raw = await fs23.readFile(file, "utf8");
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 path27 from "path";
7904
- import fs24 from "fs";
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 = fs24.realpathSync(path27.resolve(projectRoot));
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 = path27.resolve(this.canonicalRoot, inputPath);
8118
+ const resolved = path28.resolve(this.canonicalRoot, inputPath);
7920
8119
  let canonical;
7921
8120
  try {
7922
- canonical = fs24.realpathSync(resolved);
8121
+ canonical = fs25.realpathSync(resolved);
7923
8122
  } catch {
7924
8123
  canonical = resolved;
7925
8124
  }
7926
- if (!canonical.startsWith(this.canonicalRoot + path27.sep) && canonical !== 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 path27.relative(this.canonicalRoot, absolutePath);
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 = fs24.statSync(absPath);
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 fs24.readFileSync(absPath, "utf-8");
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 path28 from "path";
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 path28.join(home, ".ctxloom", "license.json");
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(path28.dirname(this.filePath), { recursive: true });
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-EHFVPRTN.js.map
8744
+ //# sourceMappingURL=chunk-ZTC3QWIL.js.map