truecourse 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -3492,9 +3492,7 @@ var require_src = __commonJS({
3492
3492
 
3493
3493
  // node_modules/.pnpm/@clack+core@1.2.0/node_modules/@clack/core/dist/index.mjs
3494
3494
  import { stdout as S, stdin as $ } from "node:process";
3495
- import * as _ from "node:readline";
3496
3495
  import P from "node:readline";
3497
- import { ReadStream as D } from "node:tty";
3498
3496
  function d(r, t2, e) {
3499
3497
  if (!e.some((o) => !o.disabled)) return r;
3500
3498
  const s = r + t2, i = Math.max(e.length - 1, 0), n = s < 0 ? i : s > i ? 0 : s;
@@ -3520,27 +3518,6 @@ function w(r, t2) {
3520
3518
  const e = r;
3521
3519
  e.isTTY && e.setRawMode(t2);
3522
3520
  }
3523
- function z({ input: r = $, output: t2 = S, overwrite: e = true, hideCursor: s = true } = {}) {
3524
- const i = _.createInterface({ input: r, output: t2, prompt: "", tabSize: 1 });
3525
- _.emitKeypressEvents(r, i), r instanceof D && r.isTTY && r.setRawMode(true);
3526
- const n = (o, { name: a, sequence: h }) => {
3527
- const l = String(o);
3528
- if (V([l, a, h], "cancel")) {
3529
- s && t2.write(import_sisteransi.cursor.show), process.exit(0);
3530
- return;
3531
- }
3532
- if (!e) return;
3533
- const f = a === "return" ? 0 : -1, v = a === "return" ? -1 : 0;
3534
- _.moveCursor(t2, f, v, () => {
3535
- _.clearLine(t2, 1, () => {
3536
- r.once("keypress", n);
3537
- });
3538
- });
3539
- };
3540
- return s && t2.write(import_sisteransi.cursor.hide), r.once("keypress", n), () => {
3541
- r.off("keypress", n), s && t2.write(import_sisteransi.cursor.show), r instanceof D && r.isTTY && !Y && r.setRawMode(false), i.terminal = false, i.close();
3542
- };
3543
- }
3544
3521
  function R(r, t2, e, s = e) {
3545
3522
  const i = O(r ?? S);
3546
3523
  return wrapAnsi(t2, i - e.length, { hard: true, trim: false }).split(`
@@ -3734,7 +3711,7 @@ import P2 from "node:process";
3734
3711
  function Ze() {
3735
3712
  return P2.platform !== "win32" ? P2.env.TERM !== "linux" : !!P2.env.CI || !!P2.env.WT_SESSION || !!P2.env.TERMINUS_SUBLIME || P2.env.ConEmuTask === "{cmd::Cmder}" || P2.env.TERM_PROGRAM === "Terminus-Sublime" || P2.env.TERM_PROGRAM === "vscode" || P2.env.TERM === "xterm-256color" || P2.env.TERM === "alacritty" || P2.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
3736
3713
  }
3737
- var import_sisteransi2, ee, ae, w2, _e, oe, ue, F, le, d2, E2, Ie, Ee, z2, H2, te, U, J, xe, se, ce, Ge, $e, de, Oe, he, pe, me, ge, V2, ye, et2, Y2, ot2, O2, pt, mt, gt, Ct, fe, Ve, re, _t, je;
3714
+ var import_sisteransi2, ee, w2, _e, oe, ue, F, le, d2, E2, Ie, Ee, z2, H2, te, U, J, xe, se, ce, Ge, $e, de, Oe, he, pe, me, ge, V2, ye, et2, Y2, ot2, O2, pt, mt, gt, Ve, re, _t, je;
3738
3715
  var init_dist4 = __esm({
3739
3716
  "node_modules/.pnpm/@clack+prompts@1.2.0/node_modules/@clack/prompts/dist/index.mjs"() {
3740
3717
  init_dist3();
@@ -3743,7 +3720,6 @@ var init_dist4 = __esm({
3743
3720
  init_dist2();
3744
3721
  import_sisteransi2 = __toESM(require_src(), 1);
3745
3722
  ee = Ze();
3746
- ae = () => process.env.CI === "true";
3747
3723
  w2 = (e, i) => ee ? e : i;
3748
3724
  _e = w2("\u25C6", "*");
3749
3725
  oe = w2("\u25A0", "x");
@@ -3820,7 +3796,7 @@ var init_dist4 = __esm({
3820
3796
  }
3821
3797
  if (f > $2) {
3822
3798
  let b = 0, x = 0, G2 = f;
3823
- const M2 = e - v, R2 = (j2, D2) => et2(h, G2, j2, D2, $2);
3799
+ const M2 = e - v, R2 = (j2, D) => et2(h, G2, j2, D, $2);
3824
3800
  m ? ({ lineCount: G2, removals: b } = R2(0, M2), G2 > $2 && ({ lineCount: G2, removals: x } = R2(M2 + 1, h.length))) : ({ lineCount: G2, removals: x } = R2(M2 + 1, h.length), G2 > $2 && ({ lineCount: G2, removals: b } = R2(0, M2))), b > 0 && (m = true, h.splice(0, b)), x > 0 && (g = true, h.splice(h.length - x, x));
3825
3801
  }
3826
3802
  const C2 = [];
@@ -3899,59 +3875,6 @@ ${t("gray", E2)} ` : "";
3899
3875
 
3900
3876
  `);
3901
3877
  };
3902
- Ct = (e) => t("magenta", e);
3903
- fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, cancelMessage: r, errorMessage: u2, frames: n = ee ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: o = ee ? 80 : 120, signal: c2, ...a } = {}) => {
3904
- const l = ae();
3905
- let $2, y, p2 = false, m = false, g = "", S2, h = performance.now();
3906
- const f = O(s), v = a?.styleFrame ?? Ct, T = (_2) => {
3907
- const A2 = _2 > 1 ? u2 ?? u.messages.error : r ?? u.messages.cancel;
3908
- m = _2 === 1, p2 && (W(A2, _2), m && typeof i == "function" && i());
3909
- }, C2 = () => T(2), b = () => T(1), x = () => {
3910
- process.on("uncaughtExceptionMonitor", C2), process.on("unhandledRejection", C2), process.on("SIGINT", b), process.on("SIGTERM", b), process.on("exit", T), c2 && c2.addEventListener("abort", b);
3911
- }, G2 = () => {
3912
- process.removeListener("uncaughtExceptionMonitor", C2), process.removeListener("unhandledRejection", C2), process.removeListener("SIGINT", b), process.removeListener("SIGTERM", b), process.removeListener("exit", T), c2 && c2.removeEventListener("abort", b);
3913
- }, M2 = () => {
3914
- if (S2 === void 0) return;
3915
- l && s.write(`
3916
- `);
3917
- const _2 = wrapAnsi(S2, f, { hard: true, trim: false }).split(`
3918
- `);
3919
- _2.length > 1 && s.write(import_sisteransi2.cursor.up(_2.length - 1)), s.write(import_sisteransi2.cursor.to(0)), s.write(import_sisteransi2.erase.down());
3920
- }, R2 = (_2) => _2.replace(/\.+$/, ""), j2 = (_2) => {
3921
- const A2 = (performance.now() - _2) / 1e3, k = Math.floor(A2 / 60), L = Math.floor(A2 % 60);
3922
- return k > 0 ? `[${k}m ${L}s]` : `[${L}s]`;
3923
- }, D2 = a.withGuide ?? u.withGuide, ie = (_2 = "") => {
3924
- p2 = true, $2 = z({ output: s }), g = R2(_2), h = performance.now(), D2 && s.write(`${t("gray", d2)}
3925
- `);
3926
- let A2 = 0, k = 0;
3927
- x(), y = setInterval(() => {
3928
- if (l && g === S2) return;
3929
- M2(), S2 = g;
3930
- const L = v(n[A2]);
3931
- let Z;
3932
- if (l) Z = `${L} ${g}...`;
3933
- else if (e === "timer") Z = `${L} ${g} ${j2(h)}`;
3934
- else {
3935
- const Be = ".".repeat(Math.floor(k)).slice(0, 3);
3936
- Z = `${L} ${g}${Be}`;
3937
- }
3938
- const Ne = wrapAnsi(Z, f, { hard: true, trim: false });
3939
- s.write(Ne), A2 = A2 + 1 < n.length ? A2 + 1 : 0, k = k < 4 ? k + 0.125 : 0;
3940
- }, o);
3941
- }, W = (_2 = "", A2 = 0, k = false) => {
3942
- if (!p2) return;
3943
- p2 = false, clearInterval(y), M2();
3944
- const L = A2 === 0 ? t("green", F) : A2 === 1 ? t("red", oe) : t("red", ue);
3945
- g = _2 ?? g, k || (e === "timer" ? s.write(`${L} ${g} ${j2(h)}
3946
- `) : s.write(`${L} ${g}
3947
- `)), G2(), $2();
3948
- };
3949
- return { start: ie, stop: (_2 = "") => W(_2, 0), message: (_2 = "") => {
3950
- g = R2(_2 ?? g);
3951
- }, cancel: (_2 = "") => W(_2, 1), error: (_2 = "") => W(_2, 2), clear: () => W("", 0, true), get isCancelled() {
3952
- return m;
3953
- } };
3954
- };
3955
3878
  Ve = { light: w2("\u2500", "-"), heavy: w2("\u2501", "="), block: w2("\u2588", "#") };
3956
3879
  re = (e, i) => e.includes(`
3957
3880
  `) ? e.split(`
@@ -4148,7 +4071,6 @@ __export(helpers_exports, {
4148
4071
  renderDiffResultsSummary: () => renderDiffResultsSummary,
4149
4072
  renderViolations: () => renderViolations,
4150
4073
  renderViolationsSummary: () => renderViolationsSummary,
4151
- requireDashboard: () => requireDashboard,
4152
4074
  requireRegisteredRepo: () => requireRegisteredRepo,
4153
4075
  severityColor: () => severityColor,
4154
4076
  severityIcon: () => severityIcon,
@@ -4185,19 +4107,6 @@ function getServerUrl() {
4185
4107
  const port = process.env.PORT || DEFAULT_PORT;
4186
4108
  return `http://localhost:${port}`;
4187
4109
  }
4188
- async function requireDashboard() {
4189
- const url = getServerUrl();
4190
- try {
4191
- const res = await fetch(`${url}/api/health`);
4192
- if (!res.ok) throw new Error();
4193
- } catch {
4194
- O2.error(
4195
- `TrueCourse dashboard is not running at ${url}.
4196
- Start it first with: truecourse dashboard`
4197
- );
4198
- process.exit(1);
4199
- }
4200
- }
4201
4110
  function requireRegisteredRepo() {
4202
4111
  const repoDir = resolveRepoDir(process.cwd());
4203
4112
  if (!repoDir) {
@@ -4439,32 +4348,184 @@ var init_helpers = __esm({
4439
4348
  }
4440
4349
  });
4441
4350
 
4442
- // apps/server/dist/lib/logger.js
4351
+ // apps/server/dist/lib/atomic-write.js
4443
4352
  import fs4 from "node:fs";
4444
4353
  import path4 from "node:path";
4354
+ function atomicWriteJson(targetPath, data) {
4355
+ fs4.mkdirSync(path4.dirname(targetPath), { recursive: true });
4356
+ const tmp = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
4357
+ fs4.writeFileSync(tmp, JSON.stringify(data, null, 2));
4358
+ fs4.renameSync(tmp, targetPath);
4359
+ }
4360
+ function lockPath(repoPath) {
4361
+ return path4.join(repoPath, ".truecourse", LOCK_FILENAME);
4362
+ }
4363
+ function acquireAnalyzeLock(repoPath) {
4364
+ const file = lockPath(repoPath);
4365
+ fs4.mkdirSync(path4.dirname(file), { recursive: true });
4366
+ try {
4367
+ const fd = fs4.openSync(file, "wx");
4368
+ fs4.writeSync(fd, `${process.pid}
4369
+ ${(/* @__PURE__ */ new Date()).toISOString()}
4370
+ `);
4371
+ fs4.closeSync(fd);
4372
+ } catch (err) {
4373
+ if (err.code === "EEXIST") {
4374
+ let owner = null;
4375
+ try {
4376
+ owner = parseInt(fs4.readFileSync(file, "utf-8").split("\n")[0], 10) || null;
4377
+ } catch {
4378
+ }
4379
+ throw new AnalyzeLockError(repoPath, owner);
4380
+ }
4381
+ throw err;
4382
+ }
4383
+ }
4384
+ function releaseAnalyzeLock(repoPath) {
4385
+ try {
4386
+ fs4.unlinkSync(lockPath(repoPath));
4387
+ } catch (err) {
4388
+ if (err.code !== "ENOENT")
4389
+ throw err;
4390
+ }
4391
+ }
4392
+ var LOCK_FILENAME, AnalyzeLockError;
4393
+ var init_atomic_write = __esm({
4394
+ "apps/server/dist/lib/atomic-write.js"() {
4395
+ "use strict";
4396
+ LOCK_FILENAME = ".analyze.lock";
4397
+ AnalyzeLockError = class extends Error {
4398
+ constructor(repoPath, ownerPid) {
4399
+ const who = ownerPid != null ? ` (held by pid ${ownerPid})` : "";
4400
+ super(`Another analyze is already running for ${repoPath}${who}. If you're sure no analyze is in progress, remove ${lockPath(repoPath)} and retry.`);
4401
+ this.name = "AnalyzeLockError";
4402
+ }
4403
+ };
4404
+ }
4405
+ });
4406
+
4407
+ // apps/server/dist/lib/analysis-store.js
4408
+ import fs5 from "node:fs";
4409
+ import path5 from "node:path";
4410
+ function storeDir(repoPath) {
4411
+ return path5.join(repoPath, TRUECOURSE_DIR2);
4412
+ }
4413
+ function analysesDir(repoPath) {
4414
+ return path5.join(storeDir(repoPath), ANALYSES_DIR);
4415
+ }
4416
+ function analysisFilePath(repoPath, filename) {
4417
+ return path5.join(analysesDir(repoPath), filename);
4418
+ }
4419
+ function latestPath(repoPath) {
4420
+ return path5.join(storeDir(repoPath), LATEST_FILE);
4421
+ }
4422
+ function historyPath(repoPath) {
4423
+ return path5.join(storeDir(repoPath), HISTORY_FILE);
4424
+ }
4425
+ function diffPath(repoPath) {
4426
+ return path5.join(storeDir(repoPath), DIFF_FILE);
4427
+ }
4428
+ function buildAnalysisFilename(analysisId, createdAt) {
4429
+ const iso = createdAt.replace(/[:.]/g, "-").replace(/-\d{3}Z$/, "Z");
4430
+ const shortId = analysisId.replace(/-/g, "").slice(0, 8);
4431
+ return `${iso}_${shortId}.json`;
4432
+ }
4433
+ function readLatest(repoPath) {
4434
+ const file = latestPath(repoPath);
4435
+ let mtime;
4436
+ try {
4437
+ mtime = fs5.statSync(file).mtimeMs;
4438
+ } catch (err) {
4439
+ if (err.code === "ENOENT") {
4440
+ latestCache.delete(repoPath);
4441
+ return null;
4442
+ }
4443
+ throw err;
4444
+ }
4445
+ const cached = latestCache.get(repoPath);
4446
+ if (cached && cached.mtime === mtime)
4447
+ return cached.data;
4448
+ const data = JSON.parse(fs5.readFileSync(file, "utf-8"));
4449
+ latestCache.set(repoPath, { mtime, data });
4450
+ return data;
4451
+ }
4452
+ function writeLatest(repoPath, latest) {
4453
+ atomicWriteJson(latestPath(repoPath), latest);
4454
+ latestCache.delete(repoPath);
4455
+ }
4456
+ function writeAnalysis(repoPath, snapshot) {
4457
+ const filename = buildAnalysisFilename(snapshot.id, snapshot.createdAt);
4458
+ atomicWriteJson(analysisFilePath(repoPath, filename), snapshot);
4459
+ return { filename, snapshot };
4460
+ }
4461
+ function readHistory(repoPath) {
4462
+ const file = historyPath(repoPath);
4463
+ if (!fs5.existsSync(file))
4464
+ return { analyses: [] };
4465
+ return JSON.parse(fs5.readFileSync(file, "utf-8"));
4466
+ }
4467
+ function appendHistory(repoPath, entry) {
4468
+ const history = readHistory(repoPath);
4469
+ history.analyses.push(entry);
4470
+ atomicWriteJson(historyPath(repoPath), history);
4471
+ }
4472
+ function readDiff(repoPath) {
4473
+ const file = diffPath(repoPath);
4474
+ if (!fs5.existsSync(file))
4475
+ return null;
4476
+ return JSON.parse(fs5.readFileSync(file, "utf-8"));
4477
+ }
4478
+ function writeDiff(repoPath, diff) {
4479
+ atomicWriteJson(diffPath(repoPath), diff);
4480
+ }
4481
+ function deleteDiff(repoPath) {
4482
+ try {
4483
+ fs5.unlinkSync(diffPath(repoPath));
4484
+ } catch (err) {
4485
+ if (err.code !== "ENOENT")
4486
+ throw err;
4487
+ }
4488
+ }
4489
+ var TRUECOURSE_DIR2, ANALYSES_DIR, LATEST_FILE, HISTORY_FILE, DIFF_FILE, latestCache;
4490
+ var init_analysis_store = __esm({
4491
+ "apps/server/dist/lib/analysis-store.js"() {
4492
+ "use strict";
4493
+ init_atomic_write();
4494
+ TRUECOURSE_DIR2 = ".truecourse";
4495
+ ANALYSES_DIR = "analyses";
4496
+ LATEST_FILE = "LATEST.json";
4497
+ HISTORY_FILE = "history.json";
4498
+ DIFF_FILE = "diff.json";
4499
+ latestCache = /* @__PURE__ */ new Map();
4500
+ }
4501
+ });
4502
+
4503
+ // apps/server/dist/lib/logger.js
4504
+ import fs6 from "node:fs";
4505
+ import path6 from "node:path";
4445
4506
  function rotateLog(filePath) {
4446
- if (!fs4.existsSync(filePath))
4507
+ if (!fs6.existsSync(filePath))
4447
4508
  return;
4448
- const stats = fs4.statSync(filePath);
4509
+ const stats = fs6.statSync(filePath);
4449
4510
  if (stats.size < MAX_LOG_SIZE)
4450
4511
  return;
4451
4512
  for (let i = MAX_LOG_FILES; i >= 1; i--) {
4452
4513
  const older = `${filePath}.${i}`;
4453
4514
  if (i === MAX_LOG_FILES) {
4454
- if (fs4.existsSync(older))
4455
- fs4.unlinkSync(older);
4515
+ if (fs6.existsSync(older))
4516
+ fs6.unlinkSync(older);
4456
4517
  } else {
4457
4518
  const newer = `${filePath}.${i + 1}`;
4458
- if (fs4.existsSync(older))
4459
- fs4.renameSync(older, newer);
4519
+ if (fs6.existsSync(older))
4520
+ fs6.renameSync(older, newer);
4460
4521
  }
4461
4522
  }
4462
- fs4.renameSync(filePath, `${filePath}.1`);
4523
+ fs6.renameSync(filePath, `${filePath}.1`);
4463
4524
  }
4464
4525
  function openSink(config2) {
4465
- fs4.mkdirSync(path4.dirname(config2.filePath), { recursive: true });
4526
+ fs6.mkdirSync(path6.dirname(config2.filePath), { recursive: true });
4466
4527
  rotateLog(config2.filePath);
4467
- const stream = fs4.createWriteStream(config2.filePath, { flags: "a" });
4528
+ const stream = fs6.createWriteStream(config2.filePath, { flags: "a" });
4468
4529
  stream.write(`
4469
4530
  --- ${(/* @__PURE__ */ new Date()).toISOString()} ---
4470
4531
  `);
@@ -5096,7 +5157,7 @@ var require_node = __commonJS({
5096
5157
  exports.inspectOpts = Object.keys(process.env).filter((key) => {
5097
5158
  return /^debug_/i.test(key);
5098
5159
  }).reduce((obj, key) => {
5099
- const prop = key.substring(6).toLowerCase().replace(/_([a-z])/g, (_2, k) => {
5160
+ const prop = key.substring(6).toLowerCase().replace(/_([a-z])/g, (_, k) => {
5100
5161
  return k.toUpperCase();
5101
5162
  });
5102
5163
  let val = process.env[key];
@@ -6572,7 +6633,7 @@ function deleteBranchTask(branch, forceDelete = false) {
6572
6633
  parser(stdOut, stdErr) {
6573
6634
  return parseBranchDeletions(stdOut, stdErr).branches[branch];
6574
6635
  },
6575
- onError({ exitCode, stdErr, stdOut }, error, _2, fail) {
6636
+ onError({ exitCode, stdErr, stdOut }, error, _, fail) {
6576
6637
  if (!hasBranchDeletionError(String(error), exitCode)) {
6577
6638
  return fail(error);
6578
6639
  }
@@ -9776,17 +9837,27 @@ var init_git = __esm({
9776
9837
  });
9777
9838
 
9778
9839
  // apps/server/dist/config/project-config.js
9779
- import fs5 from "node:fs";
9840
+ import fs7 from "node:fs";
9780
9841
  function readProjectConfig(repoDir) {
9781
9842
  const file = getRepoConfigPath(repoDir);
9782
- if (!fs5.existsSync(file))
9843
+ if (!fs7.existsSync(file))
9783
9844
  return { ...EMPTY };
9784
9845
  try {
9785
- return JSON.parse(fs5.readFileSync(file, "utf-8"));
9846
+ return JSON.parse(fs7.readFileSync(file, "utf-8"));
9786
9847
  } catch {
9787
9848
  return { ...EMPTY };
9788
9849
  }
9789
9850
  }
9851
+ function writeProjectConfig(repoDir, config2) {
9852
+ ensureRepoTruecourseDir(repoDir);
9853
+ fs7.writeFileSync(getRepoConfigPath(repoDir), JSON.stringify(config2, null, 2), "utf-8");
9854
+ }
9855
+ function updateProjectConfig(repoDir, patch) {
9856
+ const current = readProjectConfig(repoDir);
9857
+ const next = { ...current, ...patch };
9858
+ writeProjectConfig(repoDir, next);
9859
+ return next;
9860
+ }
9790
9861
  var EMPTY;
9791
9862
  var init_project_config = __esm({
9792
9863
  "apps/server/dist/config/project-config.js"() {
@@ -9848,7 +9919,7 @@ var require_ignore = __commonJS({
9848
9919
  // (a ) -> (a)
9849
9920
  // (a \ ) -> (a )
9850
9921
  /((?:\\\\)*?)(\\?\s+)$/,
9851
- (_2, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY2)
9922
+ (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY2)
9852
9923
  ],
9853
9924
  // Replace (\ ) with ' '
9854
9925
  // (\ ) -> ' '
@@ -9856,7 +9927,7 @@ var require_ignore = __commonJS({
9856
9927
  // (\\\ ) -> '\\ '
9857
9928
  [
9858
9929
  /(\\+?)\s/g,
9859
- (_2, m1) => {
9930
+ (_, m1) => {
9860
9931
  const { length } = m1;
9861
9932
  return m1.slice(0, length - length % 2) + SPACE;
9862
9933
  }
@@ -9927,7 +9998,7 @@ var require_ignore = __commonJS({
9927
9998
  // Zero, one or several directories
9928
9999
  // should not use '*', or it will be replaced by the next replacer
9929
10000
  // Check if it is not the last `'/**'`
9930
- (_2, index, str2) => index + 6 < str2.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
10001
+ (_, index, str2) => index + 6 < str2.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
9931
10002
  ],
9932
10003
  // normal intermediate wildcards
9933
10004
  [
@@ -9939,7 +10010,7 @@ var require_ignore = __commonJS({
9939
10010
  /(^|[^\\]+)(\\\*)+(?=.+)/g,
9940
10011
  // '*.js' matches '.js'
9941
10012
  // '*.js' doesn't match 'abc'
9942
- (_2, p1, p2) => {
10013
+ (_, p1, p2) => {
9943
10014
  const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
9944
10015
  return p1 + unescaped;
9945
10016
  }
@@ -9986,11 +10057,11 @@ var require_ignore = __commonJS({
9986
10057
  var MODE_CHECK_IGNORE = "checkRegex";
9987
10058
  var UNDERSCORE = "_";
9988
10059
  var TRAILING_WILD_CARD_REPLACERS = {
9989
- [MODE_IGNORE](_2, p1) {
10060
+ [MODE_IGNORE](_, p1) {
9990
10061
  const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
9991
10062
  return `${prefix}(?=$|\\/$)`;
9992
10063
  },
9993
- [MODE_CHECK_IGNORE](_2, p1) {
10064
+ [MODE_CHECK_IGNORE](_, p1) {
9994
10065
  const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
9995
10066
  return `${prefix}(?=$|\\/$)`;
9996
10067
  }
@@ -14776,7 +14847,7 @@ var init_ast = __esm({
14776
14847
  if (!isExtglobAST(this)) {
14777
14848
  const noEmpty = this.isStart() && this.isEnd() && !this.#parts.some((s) => typeof s !== "string");
14778
14849
  const src = this.#parts.map((p2) => {
14779
- const [re2, _2, hasMagic, uflag] = typeof p2 === "string" ? _a2.#parseGlob(p2, this.#hasMagic, noEmpty) : p2.toRegExpSource(allowDot);
14850
+ const [re2, _, hasMagic, uflag] = typeof p2 === "string" ? _a2.#parseGlob(p2, this.#hasMagic, noEmpty) : p2.toRegExpSource(allowDot);
14780
14851
  this.#hasMagic = this.#hasMagic || hasMagic;
14781
14852
  this.#uflag = this.#uflag || uflag;
14782
14853
  return re2;
@@ -14882,7 +14953,7 @@ var init_ast = __esm({
14882
14953
  if (typeof p2 === "string") {
14883
14954
  throw new Error("string type in extglob ast??");
14884
14955
  }
14885
- const [re2, _2, _hasMagic, uflag] = p2.toRegExpSource(dot);
14956
+ const [re2, _, _hasMagic, uflag] = p2.toRegExpSource(dot);
14886
14957
  this.#uflag = this.#uflag || uflag;
14887
14958
  return re2;
14888
14959
  }).filter((p2) => !(this.isStart() && this.isEnd()) || !!p2).join("|");
@@ -14955,7 +15026,7 @@ var init_escape = __esm({
14955
15026
  });
14956
15027
 
14957
15028
  // node_modules/.pnpm/minimatch@10.2.4/node_modules/minimatch/dist/esm/index.js
14958
- var minimatch, starDotExtRE, starDotExtTest, starDotExtTestDot, starDotExtTestNocase, starDotExtTestNocaseDot, starDotStarRE, starDotStarTest, starDotStarTestDot, dotStarRE, dotStarTest, starRE, starTest, starTestDot, qmarksRE, qmarksTestNocase, qmarksTestNocaseDot, qmarksTestDot, qmarksTest, qmarksTestNoExt, qmarksTestNoExtDot, defaultPlatform, path5, sep, GLOBSTAR, qmark2, star2, twoStarDot, twoStarNoDot, filter, ext, defaults, braceExpand, makeRe, match, globMagic, regExpEscape2, Minimatch;
15029
+ var minimatch, starDotExtRE, starDotExtTest, starDotExtTestDot, starDotExtTestNocase, starDotExtTestNocaseDot, starDotStarRE, starDotStarTest, starDotStarTestDot, dotStarRE, dotStarTest, starRE, starTest, starTestDot, qmarksRE, qmarksTestNocase, qmarksTestNocaseDot, qmarksTestDot, qmarksTest, qmarksTestNoExt, qmarksTestNoExtDot, defaultPlatform, path7, sep, GLOBSTAR, qmark2, star2, twoStarDot, twoStarNoDot, filter, ext, defaults, braceExpand, makeRe, match, globMagic, regExpEscape2, Minimatch;
14959
15030
  var init_esm4 = __esm({
14960
15031
  "node_modules/.pnpm/minimatch@10.2.4/node_modules/minimatch/dist/esm/index.js"() {
14961
15032
  init_esm3();
@@ -15024,11 +15095,11 @@ var init_esm4 = __esm({
15024
15095
  return (f) => f.length === len && f !== "." && f !== "..";
15025
15096
  };
15026
15097
  defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
15027
- path5 = {
15098
+ path7 = {
15028
15099
  win32: { sep: "\\" },
15029
15100
  posix: { sep: "/" }
15030
15101
  };
15031
- sep = defaultPlatform === "win32" ? path5.win32.sep : path5.posix.sep;
15102
+ sep = defaultPlatform === "win32" ? path7.win32.sep : path7.posix.sep;
15032
15103
  minimatch.sep = sep;
15033
15104
  GLOBSTAR = Symbol("globstar **");
15034
15105
  minimatch.GLOBSTAR = GLOBSTAR;
@@ -15155,7 +15226,7 @@ var init_esm4 = __esm({
15155
15226
  }
15156
15227
  return false;
15157
15228
  }
15158
- debug(..._2) {
15229
+ debug(..._) {
15159
15230
  }
15160
15231
  make() {
15161
15232
  const pattern = this.pattern;
@@ -15177,7 +15248,7 @@ var init_esm4 = __esm({
15177
15248
  const rawGlobParts = this.globSet.map((s) => this.slashSplit(s));
15178
15249
  this.globParts = this.preprocess(rawGlobParts);
15179
15250
  this.debug(this.pattern, this.globParts);
15180
- let set2 = this.globParts.map((s, _2, __) => {
15251
+ let set2 = this.globParts.map((s, _, __) => {
15181
15252
  if (this.isWindows && this.windowsNoMagicRoot) {
15182
15253
  const isUNC = s[0] === "" && s[1] === "" && (s[2] === "?" || !globMagic.test(s[2])) && !globMagic.test(s[3]);
15183
15254
  const isDrive = /^[a-z]:/i.test(s[0]);
@@ -17074,7 +17145,7 @@ var init_database_detector = __esm({
17074
17145
  });
17075
17146
 
17076
17147
  // packages/analyzer/dist/module-extractor.js
17077
- import path6 from "path";
17148
+ import path8 from "path";
17078
17149
  function extractModulesAndMethods(analyses, layerDetails, fileDependencies) {
17079
17150
  const modules = [];
17080
17151
  const methods = [];
@@ -17221,7 +17292,7 @@ function deriveModuleName(analysis) {
17221
17292
  return localExports[0].name;
17222
17293
  }
17223
17294
  const baseName = fileBaseName(analysis.filePath);
17224
- const strippedName = stripExtension(path6.basename(analysis.filePath));
17295
+ const strippedName = stripExtension(path8.basename(analysis.filePath));
17225
17296
  if (INDEX_NAMES.has(strippedName)) {
17226
17297
  return baseName;
17227
17298
  }
@@ -17238,10 +17309,10 @@ function deriveModuleName(analysis) {
17238
17309
  return baseName;
17239
17310
  }
17240
17311
  function deriveNextjsRouteName(filePath) {
17241
- const base = path6.basename(filePath).replace(/\.(ts|tsx|js|jsx)$/, "");
17312
+ const base = path8.basename(filePath).replace(/\.(ts|tsx|js|jsx)$/, "");
17242
17313
  if (base !== "route" && base !== "page")
17243
17314
  return null;
17244
- const parts = filePath.split(path6.sep);
17315
+ const parts = filePath.split(path8.sep);
17245
17316
  const appIdx = parts.lastIndexOf("app");
17246
17317
  if (appIdx === -1)
17247
17318
  return null;
@@ -17259,9 +17330,9 @@ function stripExtension(filename) {
17259
17330
  return filename;
17260
17331
  }
17261
17332
  function fileBaseName(filePath) {
17262
- const name = stripExtension(path6.basename(filePath));
17333
+ const name = stripExtension(path8.basename(filePath));
17263
17334
  if (INDEX_NAMES.has(name)) {
17264
- return path6.basename(path6.dirname(filePath));
17335
+ return path8.basename(path8.dirname(filePath));
17265
17336
  }
17266
17337
  return name;
17267
17338
  }
@@ -44024,10 +44095,10 @@ var init_secret_rules = __esm({
44024
44095
  });
44025
44096
 
44026
44097
  // packages/analyzer/dist/rules/security/secret-scanner.js
44027
- import path7 from "node:path";
44098
+ import path9 from "node:path";
44028
44099
  function isSensitiveFile(filePath) {
44029
- const basename2 = path7.basename(filePath);
44030
- const ext2 = path7.extname(filePath);
44100
+ const basename2 = path9.basename(filePath);
44101
+ const ext2 = path9.extname(filePath);
44031
44102
  if (basename2.startsWith(".env")) {
44032
44103
  const envVariant = basename2;
44033
44104
  if (SENSITIVE_FILE_EXTENSIONS.has(envVariant)) {
@@ -86710,7 +86781,7 @@ var init_js_naming_convention = __esm({
86710
86781
  return null;
86711
86782
  }
86712
86783
  if (funcName.includes("_") && !funcName.startsWith("_")) {
86713
- return makeViolation(this.ruleKey, node, filePath, "low", "Function uses snake_case naming", `Function '${funcName}' uses snake_case. JavaScript convention is camelCase.`, sourceCode, `Rename to camelCase: ${funcName.replace(/_([a-z])/g, (_2, c2) => c2.toUpperCase())}.`);
86784
+ return makeViolation(this.ruleKey, node, filePath, "low", "Function uses snake_case naming", `Function '${funcName}' uses snake_case. JavaScript convention is camelCase.`, sourceCode, `Rename to camelCase: ${funcName.replace(/_([a-z])/g, (_, c2) => c2.toUpperCase())}.`);
86714
86785
  }
86715
86786
  return null;
86716
86787
  }
@@ -93628,7 +93699,7 @@ var init_dist6 = __esm({
93628
93699
  });
93629
93700
 
93630
93701
  // apps/server/dist/services/analyzer.service.js
93631
- import path8 from "node:path";
93702
+ import path10 from "node:path";
93632
93703
  function runDeterministicModuleChecks(result, enabledDeterministic) {
93633
93704
  if (!result.modules || !result.methods)
93634
93705
  return [];
@@ -93671,7 +93742,7 @@ async function runAnalysis(repoPath, _branch, onProgress, options) {
93671
93742
  const statusResult = await git.status();
93672
93743
  hasChanges = !statusResult.isClean();
93673
93744
  const gitRoot = (await git.revparse(["--show-toplevel"])).trim();
93674
- isSubdirectory = path8.resolve(repoPath) !== path8.resolve(gitRoot);
93745
+ isSubdirectory = path10.resolve(repoPath) !== path10.resolve(gitRoot);
93675
93746
  }
93676
93747
  if (hasChanges && !options?.skipStash && !isSubdirectory && git) {
93677
93748
  onProgress({ step: "stash", percent: 2, detail: "Stashing pending changes to analyze committed state..." });
@@ -94005,149 +94076,210 @@ var init_analysis_persistence_service = __esm({
94005
94076
  }
94006
94077
  });
94007
94078
 
94008
- // apps/server/dist/lib/atomic-write.js
94009
- import fs6 from "node:fs";
94010
- import path9 from "node:path";
94011
- function atomicWriteJson(targetPath, data) {
94012
- fs6.mkdirSync(path9.dirname(targetPath), { recursive: true });
94013
- const tmp = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
94014
- fs6.writeFileSync(tmp, JSON.stringify(data, null, 2));
94015
- fs6.renameSync(tmp, targetPath);
94016
- }
94017
- function lockPath(repoPath) {
94018
- return path9.join(repoPath, ".truecourse", LOCK_FILENAME);
94019
- }
94020
- function acquireAnalyzeLock(repoPath) {
94021
- const file = lockPath(repoPath);
94022
- fs6.mkdirSync(path9.dirname(file), { recursive: true });
94023
- try {
94024
- const fd = fs6.openSync(file, "wx");
94025
- fs6.writeSync(fd, `${process.pid}
94026
- ${(/* @__PURE__ */ new Date()).toISOString()}
94027
- `);
94028
- fs6.closeSync(fd);
94029
- } catch (err) {
94030
- if (err.code === "EEXIST") {
94031
- let owner = null;
94032
- try {
94033
- owner = parseInt(fs6.readFileSync(file, "utf-8").split("\n")[0], 10) || null;
94034
- } catch {
94079
+ // apps/server/dist/services/flow.service.js
94080
+ import { randomUUID as randomUUID2 } from "node:crypto";
94081
+ function detectFlows(result) {
94082
+ const dbTypeMap = /* @__PURE__ */ new Map();
94083
+ for (const db of result.databaseResult.databases) {
94084
+ dbTypeMap.set(db.name, db.type);
94085
+ }
94086
+ const functionsByFile = /* @__PURE__ */ new Map();
94087
+ if (result.fileAnalyses) {
94088
+ for (const fa of result.fileAnalyses) {
94089
+ const entries = [];
94090
+ for (const fn of fa.functions) {
94091
+ entries.push({ name: fn.name, startLine: fn.location.startLine, endLine: fn.location.endLine });
94035
94092
  }
94036
- throw new AnalyzeLockError(repoPath, owner);
94093
+ for (const cls of fa.classes) {
94094
+ for (const method of cls.methods) {
94095
+ entries.push({ name: method.name, startLine: method.location.startLine, endLine: method.location.endLine });
94096
+ }
94097
+ }
94098
+ if (entries.length > 0)
94099
+ functionsByFile.set(fa.filePath, entries);
94037
94100
  }
94038
- throw err;
94039
94101
  }
94040
- }
94041
- function releaseAnalyzeLock(repoPath) {
94042
- try {
94043
- fs6.unlinkSync(lockPath(repoPath));
94044
- } catch (err) {
94045
- if (err.code !== "ENOENT")
94046
- throw err;
94102
+ const crossServiceCalls = [];
94103
+ const fileToModule = /* @__PURE__ */ new Map();
94104
+ for (const mod of result.modules) {
94105
+ fileToModule.set(mod.filePath, `${mod.serviceName}::${mod.name}`);
94047
94106
  }
94048
- }
94049
- var LOCK_FILENAME, AnalyzeLockError;
94050
- var init_atomic_write = __esm({
94051
- "apps/server/dist/lib/atomic-write.js"() {
94052
- "use strict";
94053
- LOCK_FILENAME = ".analyze.lock";
94054
- AnalyzeLockError = class extends Error {
94055
- constructor(repoPath, ownerPid) {
94056
- const who = ownerPid != null ? ` (held by pid ${ownerPid})` : "";
94057
- super(`Another analyze is already running for ${repoPath}${who}. If you're sure no analyze is in progress, remove ${lockPath(repoPath)} and retry.`);
94058
- this.name = "AnalyzeLockError";
94107
+ const fileToLanguage = /* @__PURE__ */ new Map();
94108
+ if (result.fileAnalyses) {
94109
+ for (const fa of result.fileAnalyses) {
94110
+ fileToLanguage.set(fa.filePath, fa.language);
94111
+ }
94112
+ }
94113
+ for (const dep of result.dependencies) {
94114
+ if (!dep.httpCalls || dep.httpCalls.length === 0)
94115
+ continue;
94116
+ for (const call of dep.httpCalls) {
94117
+ const moduleKey = fileToModule.get(call.location.filePath);
94118
+ if (!moduleKey)
94119
+ continue;
94120
+ const [sourceService, sourceModule] = moduleKey.split("::");
94121
+ let sourceMethod;
94122
+ const fileFunctions = functionsByFile.get(call.location.filePath);
94123
+ if (fileFunctions) {
94124
+ for (const fn of fileFunctions) {
94125
+ if (call.location.startLine >= fn.startLine && call.location.startLine <= fn.endLine) {
94126
+ sourceMethod = fn.name;
94127
+ break;
94128
+ }
94129
+ }
94059
94130
  }
94060
- };
94131
+ const language = fileToLanguage.get(call.location.filePath);
94132
+ const normalizedUrl = language ? normalizeUrl(call.url, language) : call.url;
94133
+ crossServiceCalls.push({
94134
+ sourceService,
94135
+ sourceModule,
94136
+ sourceMethod,
94137
+ httpMethod: call.method,
94138
+ url: normalizedUrl,
94139
+ targetService: dep.target
94140
+ });
94141
+ }
94061
94142
  }
94062
- });
94063
-
94064
- // apps/server/dist/lib/analysis-store.js
94065
- import fs7 from "node:fs";
94066
- import path10 from "node:path";
94067
- function storeDir(repoPath) {
94068
- return path10.join(repoPath, TRUECOURSE_DIR2);
94069
- }
94070
- function analysesDir(repoPath) {
94071
- return path10.join(storeDir(repoPath), ANALYSES_DIR);
94072
- }
94073
- function analysisFilePath(repoPath, filename) {
94074
- return path10.join(analysesDir(repoPath), filename);
94075
- }
94076
- function latestPath(repoPath) {
94077
- return path10.join(storeDir(repoPath), LATEST_FILE);
94078
- }
94079
- function historyPath(repoPath) {
94080
- return path10.join(storeDir(repoPath), HISTORY_FILE);
94081
- }
94082
- function diffPath(repoPath) {
94083
- return path10.join(storeDir(repoPath), DIFF_FILE);
94084
- }
94085
- function buildAnalysisFilename(analysisId, createdAt) {
94086
- const iso = createdAt.replace(/[:.]/g, "-").replace(/-\d{3}Z$/, "Z");
94087
- const shortId = analysisId.replace(/-/g, "").slice(0, 8);
94088
- return `${iso}_${shortId}.json`;
94143
+ const routeHandlers = buildRouteHandlerLookup(result);
94144
+ const graph = new AnalysisGraph({
94145
+ methods: result.methods,
94146
+ methodDependencies: result.methodLevelDependencies,
94147
+ modules: result.modules,
94148
+ services: result.services.map((s) => ({ name: s.name, type: s.type })),
94149
+ crossServiceCalls: crossServiceCalls.length > 0 ? crossServiceCalls : void 0,
94150
+ databaseConnections: result.databaseResult.connections.map((c2) => ({
94151
+ serviceName: c2.serviceName,
94152
+ databaseName: c2.databaseName,
94153
+ databaseType: dbTypeMap.get(c2.databaseName) || "unknown"
94154
+ })),
94155
+ routeHandlers: routeHandlers.size > 0 ? routeHandlers : void 0
94156
+ });
94157
+ const traced = traceFlows(graph);
94158
+ const out = traced.map((flow) => ({
94159
+ id: randomUUID2(),
94160
+ name: flow.name,
94161
+ description: null,
94162
+ entryService: flow.entryService,
94163
+ entryMethod: flow.entryMethod,
94164
+ category: flow.category,
94165
+ trigger: flow.trigger,
94166
+ stepCount: flow.steps.length,
94167
+ steps: flow.steps.map((step) => ({
94168
+ stepOrder: step.stepOrder,
94169
+ sourceService: step.sourceService,
94170
+ sourceModule: step.sourceModule,
94171
+ sourceMethod: step.sourceMethod,
94172
+ targetService: step.targetService,
94173
+ targetModule: step.targetModule,
94174
+ targetMethod: step.targetMethod,
94175
+ stepType: step.stepType,
94176
+ dataDescription: null,
94177
+ isAsync: step.isAsync,
94178
+ isConditional: step.isConditional
94179
+ }))
94180
+ }));
94181
+ log.info(`[Flows] Detected ${out.length} flows`);
94182
+ return out;
94089
94183
  }
94090
- function readLatest(repoPath) {
94091
- const file = latestPath(repoPath);
94092
- let mtime;
94093
- try {
94094
- mtime = fs7.statSync(file).mtimeMs;
94095
- } catch (err) {
94096
- if (err.code === "ENOENT") {
94097
- latestCache.delete(repoPath);
94098
- return null;
94184
+ function buildRouteHandlerLookup(result) {
94185
+ const handlers = /* @__PURE__ */ new Map();
94186
+ if (!result.fileAnalyses)
94187
+ return handlers;
94188
+ const fileToService = /* @__PURE__ */ new Map();
94189
+ const fileToModuleName = /* @__PURE__ */ new Map();
94190
+ for (const mod of result.modules) {
94191
+ fileToService.set(mod.filePath, mod.serviceName);
94192
+ fileToModuleName.set(mod.filePath, mod.name);
94193
+ }
94194
+ for (const ld of result.layerDetails) {
94195
+ for (const fp of ld.filePaths) {
94196
+ if (!fileToService.has(fp))
94197
+ fileToService.set(fp, ld.serviceName);
94099
94198
  }
94100
- throw err;
94101
94199
  }
94102
- const cached = latestCache.get(repoPath);
94103
- if (cached && cached.mtime === mtime)
94104
- return cached.data;
94105
- const data = JSON.parse(fs7.readFileSync(file, "utf-8"));
94106
- latestCache.set(repoPath, { mtime, data });
94107
- return data;
94108
- }
94109
- function writeLatest(repoPath, latest) {
94110
- atomicWriteJson(latestPath(repoPath), latest);
94111
- latestCache.delete(repoPath);
94112
- }
94113
- function writeAnalysis(repoPath, snapshot) {
94114
- const filename = buildAnalysisFilename(snapshot.id, snapshot.createdAt);
94115
- atomicWriteJson(analysisFilePath(repoPath, filename), snapshot);
94116
- return { filename, snapshot };
94117
- }
94118
- function readHistory(repoPath) {
94119
- const file = historyPath(repoPath);
94120
- if (!fs7.existsSync(file))
94121
- return { analyses: [] };
94122
- return JSON.parse(fs7.readFileSync(file, "utf-8"));
94123
- }
94124
- function appendHistory(repoPath, entry) {
94125
- const history = readHistory(repoPath);
94126
- history.analyses.push(entry);
94127
- atomicWriteJson(historyPath(repoPath), history);
94128
- }
94129
- function writeDiff(repoPath, diff) {
94130
- atomicWriteJson(diffPath(repoPath), diff);
94200
+ const fileMountPrefix = /* @__PURE__ */ new Map();
94201
+ for (const fa of result.fileAnalyses) {
94202
+ if (!fa.routerMounts || fa.routerMounts.length === 0)
94203
+ continue;
94204
+ for (const mount of fa.routerMounts) {
94205
+ for (const imp of fa.imports) {
94206
+ const spec = imp.specifiers.find((s) => s.name === mount.routerName || s.alias === mount.routerName);
94207
+ if (spec) {
94208
+ for (const dep of result.moduleDependencies) {
94209
+ if (dep.source === fa.filePath && dep.importedNames.includes(spec.name)) {
94210
+ fileMountPrefix.set(dep.target, mount.path);
94211
+ }
94212
+ }
94213
+ }
94214
+ }
94215
+ const hasLocal = fa.functions.some((f) => f.name === mount.routerName) || fa.exports.some((e) => e.name === mount.routerName);
94216
+ if (hasLocal)
94217
+ fileMountPrefix.set(fa.filePath, mount.path);
94218
+ }
94219
+ }
94220
+ for (const fa of result.fileAnalyses) {
94221
+ if (!fa.routeRegistrations || fa.routeRegistrations.length === 0)
94222
+ continue;
94223
+ const serviceName = fileToService.get(fa.filePath);
94224
+ if (!serviceName)
94225
+ continue;
94226
+ const mountPrefix = fileMountPrefix.get(fa.filePath) || "";
94227
+ for (const route of fa.routeRegistrations) {
94228
+ const fullPath = composePath(mountPrefix, route.path);
94229
+ const moduleName = resolveHandlerModule(route.handlerName, fa, result.fileAnalyses, fileToModuleName);
94230
+ const key = `${serviceName}::${route.httpMethod}::${fullPath}`;
94231
+ handlers.set(key, { handlerName: route.handlerName, moduleName });
94232
+ }
94233
+ }
94234
+ return handlers;
94131
94235
  }
94132
- function deleteDiff(repoPath) {
94133
- try {
94134
- fs7.unlinkSync(diffPath(repoPath));
94135
- } catch (err) {
94136
- if (err.code !== "ENOENT")
94137
- throw err;
94236
+ function resolveHandlerModule(handlerName, routeFile, allFiles, fileToModuleName) {
94237
+ for (const cls of routeFile.classes) {
94238
+ for (const method of cls.methods) {
94239
+ if (method.name === handlerName)
94240
+ return fileToModuleName.get(routeFile.filePath) || handlerName;
94241
+ }
94138
94242
  }
94243
+ for (const fn of routeFile.functions) {
94244
+ if (fn.name === handlerName)
94245
+ return fileToModuleName.get(routeFile.filePath) || handlerName;
94246
+ }
94247
+ for (const imp of routeFile.imports) {
94248
+ const spec = imp.specifiers.find((s) => s.name === handlerName || s.alias === handlerName);
94249
+ if (spec) {
94250
+ for (const targetFile of allFiles) {
94251
+ const hasExport = targetFile.exports.some((e) => e.name === (spec.alias || spec.name) || e.name === spec.name);
94252
+ const hasFunction = targetFile.functions.some((f) => f.name === spec.name);
94253
+ const hasClassMethod = targetFile.classes.some((c2) => c2.methods.some((m) => m.name === spec.name));
94254
+ if (hasExport || hasFunction || hasClassMethod) {
94255
+ return fileToModuleName.get(targetFile.filePath) || handlerName;
94256
+ }
94257
+ }
94258
+ }
94259
+ for (const s of imp.specifiers) {
94260
+ for (const targetFile of allFiles) {
94261
+ for (const cls of targetFile.classes) {
94262
+ if (cls.name === s.name && cls.methods.some((m) => m.name === handlerName)) {
94263
+ return fileToModuleName.get(targetFile.filePath) || cls.name;
94264
+ }
94265
+ }
94266
+ }
94267
+ }
94268
+ }
94269
+ return fileToModuleName.get(routeFile.filePath) || handlerName;
94139
94270
  }
94140
- var TRUECOURSE_DIR2, ANALYSES_DIR, LATEST_FILE, HISTORY_FILE, DIFF_FILE, latestCache;
94141
- var init_analysis_store = __esm({
94142
- "apps/server/dist/lib/analysis-store.js"() {
94271
+ function composePath(prefix, routePath) {
94272
+ const p2 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
94273
+ const r = routePath.startsWith("/") ? routePath : `/${routePath}`;
94274
+ const full = `${p2}${r}`;
94275
+ return full.length > 1 && full.endsWith("/") ? full.slice(0, -1) : full || "/";
94276
+ }
94277
+ var init_flow_service = __esm({
94278
+ "apps/server/dist/services/flow.service.js"() {
94143
94279
  "use strict";
94144
- init_atomic_write();
94145
- TRUECOURSE_DIR2 = ".truecourse";
94146
- ANALYSES_DIR = "analyses";
94147
- LATEST_FILE = "LATEST.json";
94148
- HISTORY_FILE = "history.json";
94149
- DIFF_FILE = "diff.json";
94150
- latestCache = /* @__PURE__ */ new Map();
94280
+ init_logger();
94281
+ init_dist6();
94282
+ init_analysis_store();
94151
94283
  }
94152
94284
  });
94153
94285
 
@@ -107143,7 +107275,7 @@ var require_sender = __commonJS({
107143
107275
  const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
107144
107276
  this._bufferedBytes += options[kByteLength];
107145
107277
  this._state = DEFLATING;
107146
- perMessageDeflate.compress(data, options.fin, (_2, buf) => {
107278
+ perMessageDeflate.compress(data, options.fin, (_, buf) => {
107147
107279
  if (this._socket.destroyed) {
107148
107280
  const err = new Error(
107149
107281
  "The socket was closed while data was being compressed"
@@ -115967,7 +116099,7 @@ var util, objectUtil, ZodParsedType, getParsedType;
115967
116099
  var init_util3 = __esm({
115968
116100
  "node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js"() {
115969
116101
  (function(util2) {
115970
- util2.assertEqual = (_2) => {
116102
+ util2.assertEqual = (_) => {
115971
116103
  };
115972
116104
  function assertIs(_arg) {
115973
116105
  }
@@ -116017,7 +116149,7 @@ var init_util3 = __esm({
116017
116149
  return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
116018
116150
  }
116019
116151
  util2.joinValues = joinValues;
116020
- util2.jsonStringifyReplacer = (_2, value) => {
116152
+ util2.jsonStringifyReplacer = (_, value) => {
116021
116153
  if (typeof value === "bigint") {
116022
116154
  return value.toString();
116023
116155
  }
@@ -120630,9 +120762,12 @@ var init_schemas = __esm({
120630
120762
  path: external_exports.string().min(1)
120631
120763
  });
120632
120764
  AnalyzeRepoSchema = external_exports.object({
120633
- branch: external_exports.string().optional(),
120634
- enabledCategories: external_exports.array(external_exports.string()).optional().default([]),
120635
- enableLlmRules: external_exports.boolean().optional().default(true),
120765
+ /** Which mode to run — full analyze (HEAD committed state) or diff (working tree
120766
+ * vs LATEST). Required; no silent default. */
120767
+ mode: external_exports.enum(["full", "diff"]),
120768
+ /** Skip git ops (branch detection, commit hash read, pre-parse stash). Useful
120769
+ * for non-git dirs or test environments. No per-repo-config equivalent —
120770
+ * only way to opt out for a single run. */
120636
120771
  skipGit: external_exports.boolean().optional().default(false)
120637
120772
  });
120638
120773
  GenerateViolationsSchema = external_exports.object({
@@ -120642,80 +120777,6 @@ var init_schemas = __esm({
120642
120777
  });
120643
120778
 
120644
120779
  // packages/shared/dist/index.js
120645
- var dist_exports3 = {};
120646
- __export(dist_exports3, {
120647
- AnalysisRuleSchema: () => AnalysisRuleSchema,
120648
- AnalyzeRepoSchema: () => AnalyzeRepoSchema,
120649
- ArchitectureSchema: () => ArchitectureSchema,
120650
- ArchitectureSummarySchema: () => ArchitectureSummarySchema,
120651
- ArchitectureSummaryServiceSchema: () => ArchitectureSummaryServiceSchema,
120652
- BreakdownResponseSchema: () => BreakdownResponseSchema,
120653
- CODE_DOMAINS: () => CODE_DOMAINS,
120654
- CallExpressionSchema: () => CallExpressionSchema,
120655
- ClassDefinitionSchema: () => ClassDefinitionSchema,
120656
- ClassPropertySchema: () => ClassPropertySchema,
120657
- ColumnInfoSchema: () => ColumnInfoSchema,
120658
- ContextRequirementSchema: () => ContextRequirementSchema,
120659
- ContextTierSchema: () => ContextTierSchema,
120660
- CreateRepoSchema: () => CreateRepoSchema,
120661
- DEFAULT_DOMAINS: () => DEFAULT_DOMAINS,
120662
- DOMAIN_ORDER: () => DOMAIN_ORDER,
120663
- DatabaseConnectionInfoSchema: () => DatabaseConnectionInfoSchema,
120664
- DatabaseDetectionResultSchema: () => DatabaseDetectionResultSchema,
120665
- DatabaseInfoSchema: () => DatabaseInfoSchema,
120666
- DatabaseTypeSchema: () => DatabaseTypeSchema,
120667
- EntityFieldSchema: () => EntityFieldSchema,
120668
- EntityRelationshipSchema: () => EntityRelationshipSchema,
120669
- EntitySchema: () => EntitySchema,
120670
- ExportStatementSchema: () => ExportStatementSchema,
120671
- FileAnalysisSchema: () => FileAnalysisSchema,
120672
- FileFilterSchema: () => FileFilterSchema,
120673
- FlowSchema: () => FlowSchema,
120674
- FlowStepSchema: () => FlowStepSchema,
120675
- FlowStepTypeSchema: () => FlowStepTypeSchema,
120676
- FlowTriggerSchema: () => FlowTriggerSchema,
120677
- FunctionDefinitionSchema: () => FunctionDefinitionSchema,
120678
- FunctionFilterSchema: () => FunctionFilterSchema,
120679
- GenerateViolationsSchema: () => GenerateViolationsSchema,
120680
- HttpCallSchema: () => HttpCallSchema,
120681
- ImportSpecifierSchema: () => ImportSpecifierSchema,
120682
- ImportStatementSchema: () => ImportStatementSchema,
120683
- IndexInfoSchema: () => IndexInfoSchema,
120684
- LayerDependencyInfoSchema: () => LayerDependencyInfoSchema,
120685
- LayerDetailSchema: () => LayerDetailSchema,
120686
- LayerDetectionResultSchema: () => LayerDetectionResultSchema,
120687
- LayerSchema: () => LayerSchema,
120688
- MethodInfoSchema: () => MethodInfoSchema,
120689
- MethodLevelDependencySchema: () => MethodLevelDependencySchema,
120690
- ModuleDependencySchema: () => ModuleDependencySchema,
120691
- ModuleInfoSchema: () => ModuleInfoSchema,
120692
- ModuleKindSchema: () => ModuleKindSchema,
120693
- ModuleLevelDependencySchema: () => ModuleLevelDependencySchema,
120694
- ParameterSchema: () => ParameterSchema,
120695
- RelationInfoSchema: () => RelationInfoSchema,
120696
- ResolutionResponseSchema: () => ResolutionResponseSchema,
120697
- RouteRegistrationSchema: () => RouteRegistrationSchema,
120698
- RouterMountSchema: () => RouterMountSchema,
120699
- RuleCategorySchema: () => RuleCategorySchema,
120700
- RuleDomainSchema: () => RuleDomainSchema,
120701
- RuleSeveritySchema: () => RuleSeveritySchema,
120702
- RuleTypeSchema: () => RuleTypeSchema,
120703
- ServiceDependencyDetailSchema: () => ServiceDependencyDetailSchema,
120704
- ServiceDependencyInfoSchema: () => ServiceDependencyInfoSchema,
120705
- ServiceInfoSchema: () => ServiceInfoSchema,
120706
- ServiceTypeSchema: () => ServiceTypeSchema,
120707
- SourceLocationSchema: () => SourceLocationSchema,
120708
- SupportedLanguageSchema: () => SupportedLanguageSchema,
120709
- TableInfoSchema: () => TableInfoSchema,
120710
- TopOffenderSchema: () => TopOffenderSchema,
120711
- TopOffendersResponseSchema: () => TopOffendersResponseSchema,
120712
- TrendDataPointSchema: () => TrendDataPointSchema,
120713
- TrendResponseSchema: () => TrendResponseSchema,
120714
- ViolationSchema: () => ViolationSchema,
120715
- ViolationSeveritySchema: () => ViolationSeveritySchema,
120716
- ViolationStatusSchema: () => ViolationStatusSchema,
120717
- ViolationTypeSchema: () => ViolationTypeSchema
120718
- });
120719
120780
  var init_dist7 = __esm({
120720
120781
  "packages/shared/dist/index.js"() {
120721
120782
  "use strict";
@@ -121400,7 +121461,7 @@ function parseStringDef(def, refs) {
121400
121461
  case "trim":
121401
121462
  break;
121402
121463
  default:
121403
- /* @__PURE__ */ ((_2) => {
121464
+ /* @__PURE__ */ ((_) => {
121404
121465
  })(check);
121405
121466
  }
121406
121467
  }
@@ -122248,7 +122309,7 @@ var init_selectParser = __esm({
122248
122309
  case ZodFirstPartyTypeKind.ZodSymbol:
122249
122310
  return void 0;
122250
122311
  default:
122251
- return /* @__PURE__ */ ((_2) => void 0)(typeName);
122312
+ return /* @__PURE__ */ ((_) => void 0)(typeName);
122252
122313
  }
122253
122314
  };
122254
122315
  }
@@ -125952,422 +126013,6 @@ var init_violation_pipeline_service = __esm({
125952
126013
  }
125953
126014
  });
125954
126015
 
125955
- // apps/server/dist/commands/diff-in-process.js
125956
- var diff_in_process_exports = {};
125957
- __export(diff_in_process_exports, {
125958
- diffInProcess: () => diffInProcess
125959
- });
125960
- import path14 from "node:path";
125961
- import { randomUUID as randomUUID7 } from "node:crypto";
125962
- async function diffInProcess(project, options = {}) {
125963
- const latest = readLatest(project.path);
125964
- if (!latest) {
125965
- throw new Error("Run a full analysis first before checking a diff.");
125966
- }
125967
- const projectConfig = readProjectConfig(project.path);
125968
- const enabledCategories = options.enabledCategoriesOverride ?? projectConfig.enabledCategories ?? void 0;
125969
- const enableLlmRules = options.enableLlmRulesOverride ?? projectConfig.enableLlmRules ?? false;
125970
- const git = await getGit(project.path);
125971
- const statusResult = await git.status();
125972
- const changedFiles = [];
125973
- for (const f of statusResult.not_added)
125974
- changedFiles.push({ path: f, status: "new" });
125975
- for (const f of statusResult.created)
125976
- changedFiles.push({ path: f, status: "new" });
125977
- for (const f of statusResult.modified)
125978
- changedFiles.push({ path: f, status: "modified" });
125979
- for (const f of statusResult.staged) {
125980
- if (!changedFiles.some((cf) => cf.path === f)) {
125981
- changedFiles.push({ path: f, status: "modified" });
125982
- }
125983
- }
125984
- for (const f of statusResult.deleted)
125985
- changedFiles.push({ path: f, status: "deleted" });
125986
- const commitHash = (await git.revparse(["HEAD"])).trim() || null;
125987
- options.tracker?.start("parse", "Analyzing working tree...");
125988
- const result = await runAnalysis(project.path, latest.analysis.branch ?? void 0, (progress) => {
125989
- options.tracker?.detail("parse", progress.detail ?? "Analyzing...");
125990
- options.onProgress?.({ detail: progress.detail });
125991
- }, { signal: options.signal, skipStash: true });
125992
- if (options.signal?.aborted)
125993
- throw new DOMException("Diff cancelled", "AbortError");
125994
- const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
125995
- const analysisId = randomUUID7();
125996
- const now = (/* @__PURE__ */ new Date()).toISOString();
125997
- options.tracker?.done("parse", `${result.services.length} services, ${result.fileAnalyses?.length ?? 0} files`);
125998
- const pipelineResult = await runViolationPipeline({
125999
- repoPath: project.path,
126000
- analysisId,
126001
- now,
126002
- result,
126003
- serviceIdMap,
126004
- moduleIdMap,
126005
- methodIdMap,
126006
- dbIdMap,
126007
- previousActiveViolations: latest.violations,
126008
- enabledCategories,
126009
- enableLlmRules,
126010
- tracker: options.tracker,
126011
- signal: options.signal
126012
- });
126013
- const diff = buildDiffSnapshot({
126014
- latest,
126015
- graph,
126016
- analysisId,
126017
- now,
126018
- branch: latest.analysis.branch,
126019
- commitHash,
126020
- changedFiles,
126021
- pipelineResult,
126022
- repoPath: project.path
126023
- });
126024
- writeDiff(project.path, diff);
126025
- log.info(`[Diff] Done \u2014 ${diff.summary.newCount} new, ${diff.summary.unchangedCount} unchanged, ${diff.summary.resolvedCount} resolved across ${diff.changedFiles.length} changed files`);
126026
- return { diff, isStale: false };
126027
- }
126028
- function buildDiffSnapshot(params) {
126029
- const { latest, graph, analysisId, branch, commitHash, changedFiles, pipelineResult, repoPath } = params;
126030
- const serviceById = new Map(graph.services.map((s) => [s.id, s.name]));
126031
- const moduleById = new Map(graph.modules.map((m) => [m.id, m.name]));
126032
- const methodById = new Map(graph.methods.map((m) => [m.id, m.name]));
126033
- const databaseById = new Map(graph.databases.map((d3) => [d3.id, d3.name]));
126034
- const denormalize = (v) => ({
126035
- ...v,
126036
- targetServiceName: v.targetServiceId ? serviceById.get(v.targetServiceId) ?? null : null,
126037
- targetModuleName: v.targetModuleId ? moduleById.get(v.targetModuleId) ?? null : null,
126038
- targetMethodName: v.targetMethodId ? methodById.get(v.targetMethodId) ?? null : null,
126039
- targetDatabaseName: v.targetDatabaseId ? databaseById.get(v.targetDatabaseId) ?? null : null
126040
- });
126041
- const newViolations = pipelineResult.added.map(denormalize);
126042
- const latestById = new Map(latest.violations.map((v) => [v.id, v]));
126043
- const resolvedViolations = pipelineResult.resolvedRefs.map((r) => latestById.get(r.id)).filter((v) => !!v);
126044
- const changedAbs = new Set(changedFiles.map((c2) => path14.resolve(repoPath, c2.path)));
126045
- const matchesChanged = (p2) => !!p2 && (changedAbs.has(p2) || changedAbs.has(path14.resolve(repoPath, p2)));
126046
- const affectedModules = graph.modules.filter((m) => matchesChanged(m.filePath));
126047
- const affectedModuleIdSet = new Set(affectedModules.map((m) => m.id));
126048
- const serviceNameById = new Map(graph.services.map((s) => [s.id, s.name]));
126049
- const layerKeyById = new Map(graph.layers.map((l) => [l.id, `${l.serviceName}::${l.layer}`]));
126050
- const affectedServices = /* @__PURE__ */ new Set();
126051
- const affectedLayers = /* @__PURE__ */ new Set();
126052
- const affectedModuleKeys = /* @__PURE__ */ new Set();
126053
- for (const mod of affectedModules) {
126054
- const svcName = serviceNameById.get(mod.serviceId);
126055
- if (svcName) {
126056
- affectedServices.add(svcName);
126057
- affectedModuleKeys.add(`${svcName}::${mod.name}`);
126058
- }
126059
- const layerKey = layerKeyById.get(mod.layerId);
126060
- if (layerKey)
126061
- affectedLayers.add(layerKey);
126062
- }
126063
- const moduleNameById = new Map(graph.modules.map((m) => [m.id, m.name]));
126064
- const affectedMethodKeys = [];
126065
- for (const method of graph.methods) {
126066
- if (!affectedModuleIdSet.has(method.moduleId))
126067
- continue;
126068
- const modName = moduleNameById.get(method.moduleId);
126069
- const mod = graph.modules.find((m) => m.id === method.moduleId);
126070
- const svcName = mod ? serviceNameById.get(mod.serviceId) : void 0;
126071
- if (svcName && modName)
126072
- affectedMethodKeys.push(`${svcName}::${modName}::${method.name}`);
126073
- }
126074
- return {
126075
- id: analysisId,
126076
- baseAnalysisId: latest.analysis.id,
126077
- createdAt: params.now,
126078
- branch,
126079
- commitHash,
126080
- graph,
126081
- changedFiles,
126082
- newViolations,
126083
- resolvedViolations,
126084
- affectedNodeIds: {
126085
- services: [...affectedServices],
126086
- layers: [...affectedLayers],
126087
- modules: [...affectedModuleKeys],
126088
- methods: affectedMethodKeys
126089
- },
126090
- summary: {
126091
- newCount: newViolations.length,
126092
- unchangedCount: pipelineResult.unchanged.length,
126093
- resolvedCount: resolvedViolations.length
126094
- }
126095
- };
126096
- }
126097
- var init_diff_in_process = __esm({
126098
- "apps/server/dist/commands/diff-in-process.js"() {
126099
- "use strict";
126100
- init_git();
126101
- init_logger();
126102
- init_project_config();
126103
- init_analyzer_service();
126104
- init_analysis_persistence_service();
126105
- init_violation_pipeline_service();
126106
- init_analysis_store();
126107
- }
126108
- });
126109
-
126110
- // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
126111
- var import_index = __toESM(require_commander(), 1);
126112
- var {
126113
- program,
126114
- createCommand,
126115
- createArgument,
126116
- createOption,
126117
- CommanderError,
126118
- InvalidArgumentError,
126119
- InvalidOptionArgumentError,
126120
- // deprecated old name
126121
- Command,
126122
- Argument,
126123
- Option,
126124
- Help
126125
- } = import_index.default;
126126
-
126127
- // tools/cli/src/index.ts
126128
- init_dist4();
126129
-
126130
- // tools/cli/src/commands/add.ts
126131
- init_dist4();
126132
- init_paths();
126133
- init_registry();
126134
- init_helpers();
126135
- async function runAdd() {
126136
- const repoPath = resolveRepoDir(process.cwd()) ?? process.cwd();
126137
- mt("Adding repository to TrueCourse");
126138
- O2.step(repoPath);
126139
- ensureRepoTruecourseDir(repoPath);
126140
- const existing = getProjectByPath(repoPath);
126141
- const entry = registerProject(repoPath);
126142
- if (existing) {
126143
- O2.info(`Repository "${entry.name}" is already registered.`);
126144
- } else {
126145
- O2.success(`Repository "${entry.name}" added.`);
126146
- }
126147
- await promptInstallSkills(repoPath);
126148
- gt("Run `truecourse analyze` to generate analysis data.");
126149
- }
126150
-
126151
- // tools/cli/src/commands/analyze.ts
126152
- init_dist4();
126153
- import { execSync } from "node:child_process";
126154
- import path15 from "node:path";
126155
-
126156
- // apps/server/dist/commands/analyze-in-process.js
126157
- init_logger();
126158
- init_git();
126159
- init_project_config();
126160
- init_registry();
126161
- init_analyzer_service();
126162
- init_analysis_persistence_service();
126163
- import { randomUUID as randomUUID6 } from "node:crypto";
126164
-
126165
- // apps/server/dist/services/flow.service.js
126166
- init_logger();
126167
- init_dist6();
126168
- init_analysis_store();
126169
- import { randomUUID as randomUUID2 } from "node:crypto";
126170
- function detectFlows(result) {
126171
- const dbTypeMap = /* @__PURE__ */ new Map();
126172
- for (const db of result.databaseResult.databases) {
126173
- dbTypeMap.set(db.name, db.type);
126174
- }
126175
- const functionsByFile = /* @__PURE__ */ new Map();
126176
- if (result.fileAnalyses) {
126177
- for (const fa of result.fileAnalyses) {
126178
- const entries = [];
126179
- for (const fn of fa.functions) {
126180
- entries.push({ name: fn.name, startLine: fn.location.startLine, endLine: fn.location.endLine });
126181
- }
126182
- for (const cls of fa.classes) {
126183
- for (const method of cls.methods) {
126184
- entries.push({ name: method.name, startLine: method.location.startLine, endLine: method.location.endLine });
126185
- }
126186
- }
126187
- if (entries.length > 0)
126188
- functionsByFile.set(fa.filePath, entries);
126189
- }
126190
- }
126191
- const crossServiceCalls = [];
126192
- const fileToModule = /* @__PURE__ */ new Map();
126193
- for (const mod of result.modules) {
126194
- fileToModule.set(mod.filePath, `${mod.serviceName}::${mod.name}`);
126195
- }
126196
- const fileToLanguage = /* @__PURE__ */ new Map();
126197
- if (result.fileAnalyses) {
126198
- for (const fa of result.fileAnalyses) {
126199
- fileToLanguage.set(fa.filePath, fa.language);
126200
- }
126201
- }
126202
- for (const dep of result.dependencies) {
126203
- if (!dep.httpCalls || dep.httpCalls.length === 0)
126204
- continue;
126205
- for (const call of dep.httpCalls) {
126206
- const moduleKey = fileToModule.get(call.location.filePath);
126207
- if (!moduleKey)
126208
- continue;
126209
- const [sourceService, sourceModule] = moduleKey.split("::");
126210
- let sourceMethod;
126211
- const fileFunctions = functionsByFile.get(call.location.filePath);
126212
- if (fileFunctions) {
126213
- for (const fn of fileFunctions) {
126214
- if (call.location.startLine >= fn.startLine && call.location.startLine <= fn.endLine) {
126215
- sourceMethod = fn.name;
126216
- break;
126217
- }
126218
- }
126219
- }
126220
- const language = fileToLanguage.get(call.location.filePath);
126221
- const normalizedUrl = language ? normalizeUrl(call.url, language) : call.url;
126222
- crossServiceCalls.push({
126223
- sourceService,
126224
- sourceModule,
126225
- sourceMethod,
126226
- httpMethod: call.method,
126227
- url: normalizedUrl,
126228
- targetService: dep.target
126229
- });
126230
- }
126231
- }
126232
- const routeHandlers = buildRouteHandlerLookup(result);
126233
- const graph = new AnalysisGraph({
126234
- methods: result.methods,
126235
- methodDependencies: result.methodLevelDependencies,
126236
- modules: result.modules,
126237
- services: result.services.map((s) => ({ name: s.name, type: s.type })),
126238
- crossServiceCalls: crossServiceCalls.length > 0 ? crossServiceCalls : void 0,
126239
- databaseConnections: result.databaseResult.connections.map((c2) => ({
126240
- serviceName: c2.serviceName,
126241
- databaseName: c2.databaseName,
126242
- databaseType: dbTypeMap.get(c2.databaseName) || "unknown"
126243
- })),
126244
- routeHandlers: routeHandlers.size > 0 ? routeHandlers : void 0
126245
- });
126246
- const traced = traceFlows(graph);
126247
- const out = traced.map((flow) => ({
126248
- id: randomUUID2(),
126249
- name: flow.name,
126250
- description: null,
126251
- entryService: flow.entryService,
126252
- entryMethod: flow.entryMethod,
126253
- category: flow.category,
126254
- trigger: flow.trigger,
126255
- stepCount: flow.steps.length,
126256
- steps: flow.steps.map((step) => ({
126257
- stepOrder: step.stepOrder,
126258
- sourceService: step.sourceService,
126259
- sourceModule: step.sourceModule,
126260
- sourceMethod: step.sourceMethod,
126261
- targetService: step.targetService,
126262
- targetModule: step.targetModule,
126263
- targetMethod: step.targetMethod,
126264
- stepType: step.stepType,
126265
- dataDescription: null,
126266
- isAsync: step.isAsync,
126267
- isConditional: step.isConditional
126268
- }))
126269
- }));
126270
- log.info(`[Flows] Detected ${out.length} flows`);
126271
- return out;
126272
- }
126273
- function buildRouteHandlerLookup(result) {
126274
- const handlers = /* @__PURE__ */ new Map();
126275
- if (!result.fileAnalyses)
126276
- return handlers;
126277
- const fileToService = /* @__PURE__ */ new Map();
126278
- const fileToModuleName = /* @__PURE__ */ new Map();
126279
- for (const mod of result.modules) {
126280
- fileToService.set(mod.filePath, mod.serviceName);
126281
- fileToModuleName.set(mod.filePath, mod.name);
126282
- }
126283
- for (const ld of result.layerDetails) {
126284
- for (const fp of ld.filePaths) {
126285
- if (!fileToService.has(fp))
126286
- fileToService.set(fp, ld.serviceName);
126287
- }
126288
- }
126289
- const fileMountPrefix = /* @__PURE__ */ new Map();
126290
- for (const fa of result.fileAnalyses) {
126291
- if (!fa.routerMounts || fa.routerMounts.length === 0)
126292
- continue;
126293
- for (const mount of fa.routerMounts) {
126294
- for (const imp of fa.imports) {
126295
- const spec = imp.specifiers.find((s) => s.name === mount.routerName || s.alias === mount.routerName);
126296
- if (spec) {
126297
- for (const dep of result.moduleDependencies) {
126298
- if (dep.source === fa.filePath && dep.importedNames.includes(spec.name)) {
126299
- fileMountPrefix.set(dep.target, mount.path);
126300
- }
126301
- }
126302
- }
126303
- }
126304
- const hasLocal = fa.functions.some((f) => f.name === mount.routerName) || fa.exports.some((e) => e.name === mount.routerName);
126305
- if (hasLocal)
126306
- fileMountPrefix.set(fa.filePath, mount.path);
126307
- }
126308
- }
126309
- for (const fa of result.fileAnalyses) {
126310
- if (!fa.routeRegistrations || fa.routeRegistrations.length === 0)
126311
- continue;
126312
- const serviceName = fileToService.get(fa.filePath);
126313
- if (!serviceName)
126314
- continue;
126315
- const mountPrefix = fileMountPrefix.get(fa.filePath) || "";
126316
- for (const route of fa.routeRegistrations) {
126317
- const fullPath = composePath(mountPrefix, route.path);
126318
- const moduleName = resolveHandlerModule(route.handlerName, fa, result.fileAnalyses, fileToModuleName);
126319
- const key = `${serviceName}::${route.httpMethod}::${fullPath}`;
126320
- handlers.set(key, { handlerName: route.handlerName, moduleName });
126321
- }
126322
- }
126323
- return handlers;
126324
- }
126325
- function resolveHandlerModule(handlerName, routeFile, allFiles, fileToModuleName) {
126326
- for (const cls of routeFile.classes) {
126327
- for (const method of cls.methods) {
126328
- if (method.name === handlerName)
126329
- return fileToModuleName.get(routeFile.filePath) || handlerName;
126330
- }
126331
- }
126332
- for (const fn of routeFile.functions) {
126333
- if (fn.name === handlerName)
126334
- return fileToModuleName.get(routeFile.filePath) || handlerName;
126335
- }
126336
- for (const imp of routeFile.imports) {
126337
- const spec = imp.specifiers.find((s) => s.name === handlerName || s.alias === handlerName);
126338
- if (spec) {
126339
- for (const targetFile of allFiles) {
126340
- const hasExport = targetFile.exports.some((e) => e.name === (spec.alias || spec.name) || e.name === spec.name);
126341
- const hasFunction = targetFile.functions.some((f) => f.name === spec.name);
126342
- const hasClassMethod = targetFile.classes.some((c2) => c2.methods.some((m) => m.name === spec.name));
126343
- if (hasExport || hasFunction || hasClassMethod) {
126344
- return fileToModuleName.get(targetFile.filePath) || handlerName;
126345
- }
126346
- }
126347
- }
126348
- for (const s of imp.specifiers) {
126349
- for (const targetFile of allFiles) {
126350
- for (const cls of targetFile.classes) {
126351
- if (cls.name === s.name && cls.methods.some((m) => m.name === handlerName)) {
126352
- return fileToModuleName.get(targetFile.filePath) || cls.name;
126353
- }
126354
- }
126355
- }
126356
- }
126357
- }
126358
- return fileToModuleName.get(routeFile.filePath) || handlerName;
126359
- }
126360
- function composePath(prefix, routePath) {
126361
- const p2 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
126362
- const r = routePath.startsWith("/") ? routePath : `/${routePath}`;
126363
- const full = `${p2}${r}`;
126364
- return full.length > 1 && full.endsWith("/") ? full.slice(0, -1) : full || "/";
126365
- }
126366
-
126367
- // apps/server/dist/commands/analyze-in-process.js
126368
- init_violation_pipeline_service();
126369
- init_provider();
126370
-
126371
126016
  // apps/server/dist/services/usage.service.js
126372
126017
  function toUsageRecords(records) {
126373
126018
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -126384,11 +126029,15 @@ function toUsageRecords(records) {
126384
126029
  createdAt: now
126385
126030
  }));
126386
126031
  }
126032
+ var init_usage_service = __esm({
126033
+ "apps/server/dist/services/usage.service.js"() {
126034
+ "use strict";
126035
+ }
126036
+ });
126387
126037
 
126388
- // apps/server/dist/commands/analyze-in-process.js
126389
- init_analysis_store();
126390
- init_atomic_write();
126391
- async function analyzeInProcess(project, options = {}) {
126038
+ // apps/server/dist/commands/analyze-core.js
126039
+ import { randomUUID as randomUUID6 } from "node:crypto";
126040
+ async function analyzeCore(project, options) {
126392
126041
  try {
126393
126042
  acquireAnalyzeLock(project.path);
126394
126043
  } catch (err) {
@@ -126397,12 +126046,27 @@ async function analyzeInProcess(project, options = {}) {
126397
126046
  throw err;
126398
126047
  }
126399
126048
  try {
126400
- const start = Date.now();
126401
- const { skipGit, signal } = options;
126049
+ const { mode, signal } = options;
126050
+ const isDiff = mode === "diff";
126051
+ const skipGit = !isDiff && !!options.skipGit;
126402
126052
  const projectConfig = readProjectConfig(project.path);
126053
+ const latestBaseline = readLatest(project.path);
126054
+ if (isDiff && !latestBaseline) {
126055
+ throw new Error("Run a full analysis first before checking a diff.");
126056
+ }
126403
126057
  let branch = options.branch ?? null;
126404
126058
  let commitHash = options.commitHash ?? null;
126405
- if (!skipGit && (branch === null || commitHash === null)) {
126059
+ if (isDiff) {
126060
+ branch = latestBaseline.analysis.branch ?? branch;
126061
+ if (commitHash === null) {
126062
+ try {
126063
+ const git = await getGit(project.path);
126064
+ commitHash = (await git.revparse(["HEAD"])).trim() || null;
126065
+ } catch {
126066
+ commitHash = null;
126067
+ }
126068
+ }
126069
+ } else if (!skipGit && (branch === null || commitHash === null)) {
126406
126070
  const git = await getGit(project.path);
126407
126071
  if (branch === null)
126408
126072
  branch = (await git.branch()).current || null;
@@ -126411,39 +126075,62 @@ async function analyzeInProcess(project, options = {}) {
126411
126075
  }
126412
126076
  const analysisId = randomUUID6();
126413
126077
  const now = (/* @__PURE__ */ new Date()).toISOString();
126414
- const filename = buildAnalysisFilename(analysisId, now);
126078
+ const start = Date.now();
126415
126079
  const effectiveCategories = options.enabledCategoriesOverride?.length ? options.enabledCategoriesOverride : projectConfig.enabledCategories ?? void 0;
126416
126080
  const effectiveLlmRules = projectConfig.enableLlmRules ?? options.enableLlmRulesOverride ?? true;
126417
- options.tracker?.start("parse", "Starting analysis...");
126081
+ options.tracker?.start("parse", isDiff ? "Analyzing working tree..." : "Starting analysis...");
126418
126082
  const result = await runAnalysis(project.path, branch ?? void 0, (progress) => {
126419
126083
  options.tracker?.detail("parse", progress.detail ?? "Analyzing...");
126420
126084
  options.onProgress?.({ detail: progress.detail });
126421
- }, { signal });
126422
- if (signal?.aborted)
126423
- throw new DOMException("Analysis cancelled", "AbortError");
126424
- const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
126425
- const previousLatest = readLatest(project.path);
126426
- const previousAnalysisId = previousLatest?.analysis.id ?? null;
126427
- const previousActiveViolations = previousLatest ? previousLatest.violations.filter((v) => branch == null || previousLatest.analysis.branch == null || previousLatest.analysis.branch === branch) : [];
126428
- try {
126429
- graph.flows = detectFlows(result);
126430
- } catch (flowError) {
126431
- log.error(`[Flows] Detection failed: ${flowError instanceof Error ? flowError.message : String(flowError)}`);
126432
- graph.flows = [];
126085
+ }, { signal, skipStash: isDiff });
126086
+ if (signal?.aborted) {
126087
+ throw new DOMException(isDiff ? "Diff cancelled" : "Analysis cancelled", "AbortError");
126433
126088
  }
126434
- touchProject(project.slug);
126089
+ const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
126090
+ let changedFiles = [];
126435
126091
  let changedFileSet;
126436
- if (previousLatest?.analysis.commitHash && !skipGit) {
126092
+ if (isDiff) {
126437
126093
  try {
126438
126094
  const git = await getGit(project.path);
126439
- const diffOutput = await git.diff([previousLatest.analysis.commitHash, "HEAD", "--name-only"]);
126095
+ const statusResult = await git.status();
126096
+ for (const f of statusResult.not_added)
126097
+ changedFiles.push({ path: f, status: "new" });
126098
+ for (const f of statusResult.created)
126099
+ changedFiles.push({ path: f, status: "new" });
126100
+ for (const f of statusResult.modified)
126101
+ changedFiles.push({ path: f, status: "modified" });
126102
+ for (const f of statusResult.staged) {
126103
+ if (!changedFiles.some((cf) => cf.path === f)) {
126104
+ changedFiles.push({ path: f, status: "modified" });
126105
+ }
126106
+ }
126107
+ for (const f of statusResult.deleted)
126108
+ changedFiles.push({ path: f, status: "deleted" });
126109
+ } catch (err) {
126110
+ log.warn(`[Diff] git status failed: ${err instanceof Error ? err.message : String(err)}`);
126111
+ }
126112
+ } else if (latestBaseline?.analysis.commitHash && !skipGit) {
126113
+ try {
126114
+ const git = await getGit(project.path);
126115
+ const diffOutput = await git.diff([latestBaseline.analysis.commitHash, "HEAD", "--name-only"]);
126440
126116
  const files = diffOutput.trim().split("\n").filter(Boolean);
126441
126117
  if (files.length > 0)
126442
126118
  changedFileSet = new Set(files);
126443
126119
  } catch {
126444
126120
  }
126445
126121
  }
126122
+ if (!isDiff) {
126123
+ try {
126124
+ graph.flows = detectFlows(result);
126125
+ } catch (flowError) {
126126
+ log.error(`[Flows] Detection failed: ${flowError instanceof Error ? flowError.message : String(flowError)}`);
126127
+ graph.flows = [];
126128
+ }
126129
+ touchProject(project.slug);
126130
+ }
126446
126131
  options.tracker?.done("parse", `${result.services.length} services, ${result.fileAnalyses?.length ?? 0} files`);
126132
+ const previousActiveViolations = latestBaseline ? latestBaseline.violations.filter((v) => branch == null || latestBaseline.analysis.branch == null || latestBaseline.analysis.branch === branch) : [];
126133
+ const previousAnalysisId = latestBaseline?.analysis.id ?? null;
126447
126134
  const provider = options.provider ?? (effectiveLlmRules ? createLLMProvider() : void 0);
126448
126135
  if (provider) {
126449
126136
  provider.setAnalysisId(analysisId);
@@ -126451,32 +126138,28 @@ async function analyzeInProcess(project, options = {}) {
126451
126138
  if (signal)
126452
126139
  provider.setAbortSignal(signal);
126453
126140
  }
126454
- let pipelineResult;
126455
- try {
126456
- pipelineResult = await runViolationPipeline({
126457
- repoPath: project.path,
126458
- analysisId,
126459
- now,
126460
- result,
126461
- serviceIdMap,
126462
- moduleIdMap,
126463
- methodIdMap,
126464
- dbIdMap,
126465
- previousActiveViolations,
126466
- changedFileSet,
126467
- tracker: options.tracker,
126468
- enabledCategories: effectiveCategories,
126469
- enableLlmRules: effectiveLlmRules,
126470
- provider,
126471
- signal,
126472
- onLlmEstimate: options.onLlmEstimate ? async (estimate) => {
126473
- const proceed = await options.onLlmEstimate(estimate);
126474
- options.onLlmResolved?.(proceed);
126475
- return proceed;
126476
- } : void 0
126477
- });
126478
- } finally {
126479
- }
126141
+ const pipelineResult = await runViolationPipeline({
126142
+ repoPath: project.path,
126143
+ analysisId,
126144
+ now,
126145
+ result,
126146
+ serviceIdMap,
126147
+ moduleIdMap,
126148
+ methodIdMap,
126149
+ dbIdMap,
126150
+ previousActiveViolations,
126151
+ changedFileSet,
126152
+ tracker: options.tracker,
126153
+ enabledCategories: effectiveCategories,
126154
+ enableLlmRules: effectiveLlmRules,
126155
+ provider,
126156
+ signal,
126157
+ onLlmEstimate: options.onLlmEstimate ? async (estimate) => {
126158
+ const proceed = await options.onLlmEstimate(estimate);
126159
+ options.onLlmResolved?.(proceed);
126160
+ return proceed;
126161
+ } : void 0
126162
+ });
126480
126163
  if (pipelineResult.serviceDescriptions.length > 0) {
126481
126164
  for (const desc of pipelineResult.serviceDescriptions) {
126482
126165
  const svc = graph.services.find((s) => s.id === desc.id);
@@ -126488,56 +126171,105 @@ async function analyzeInProcess(project, options = {}) {
126488
126171
  enforceLocationInvariant(pipelineResult.added);
126489
126172
  enforceLocationInvariant(pipelineResult.unchanged);
126490
126173
  enforceLocationInvariant(pipelineResult.resolved);
126491
- const snapshot = {
126492
- id: analysisId,
126493
- createdAt: now,
126174
+ log.info(`[${isDiff ? "Diff" : "Analysis"}] core complete in ${Date.now() - start}ms \u2014 ${pipelineResult.added.length} added, ${pipelineResult.unchanged.length} unchanged, ${pipelineResult.resolvedRefs.length} resolved`);
126175
+ return {
126176
+ mode,
126177
+ analysisId,
126178
+ now,
126494
126179
  branch,
126495
126180
  commitHash,
126496
126181
  architecture: result.architecture,
126497
- status: "completed",
126498
126182
  metadata: result.metadata ?? null,
126499
126183
  graph,
126500
- violations: {
126501
- added: pipelineResult.added,
126502
- resolved: pipelineResult.resolvedRefs,
126503
- previousAnalysisId
126504
- },
126505
- usage
126506
- };
126507
- const latest = buildLatestSnapshot(snapshot, filename, pipelineResult.unchanged, pipelineResult.added);
126508
- const { bySeverity, total } = summarizeActiveViolations(latest.violations);
126509
- writeAnalysis(project.path, snapshot);
126510
- writeLatest(project.path, latest);
126511
- const historyEntry = buildHistoryEntry(snapshot, filename, pipelineResult);
126512
- appendHistory(project.path, historyEntry);
126513
- deleteDiff(project.path);
126514
- setLastAnalyzed(project.slug, now);
126515
- return {
126516
- analysisId,
126517
- filename,
126518
- serviceCount: result.services.length,
126519
- fileCount: result.fileAnalyses?.length ?? 0,
126520
- architecture: result.architecture,
126521
- durationMs: Date.now() - start,
126522
- violationsSummary: { total, bySeverity }
126184
+ changedFiles,
126185
+ pipelineResult,
126186
+ usage,
126187
+ latestBaseline,
126188
+ previousAnalysisId,
126189
+ analysisResult: result
126523
126190
  };
126524
126191
  } finally {
126525
126192
  releaseAnalyzeLock(project.path);
126526
126193
  }
126527
126194
  }
126195
+ function enforceLocationInvariant(violations) {
126196
+ for (const v of violations) {
126197
+ const hasFile = v.filePath != null;
126198
+ const hasRange = v.lineStart != null && v.lineEnd != null;
126199
+ if (hasFile === hasRange)
126200
+ continue;
126201
+ log.warn(`[Violations] ${v.ruleKey}: partial location (filePath=${v.filePath}, lineStart=${v.lineStart}, lineEnd=${v.lineEnd}) \u2014 dropping to uphold the location invariant`);
126202
+ v.filePath = null;
126203
+ v.lineStart = null;
126204
+ v.lineEnd = null;
126205
+ }
126206
+ }
126207
+ var init_analyze_core = __esm({
126208
+ "apps/server/dist/commands/analyze-core.js"() {
126209
+ "use strict";
126210
+ init_logger();
126211
+ init_git();
126212
+ init_project_config();
126213
+ init_registry();
126214
+ init_analyzer_service();
126215
+ init_analysis_persistence_service();
126216
+ init_flow_service();
126217
+ init_violation_pipeline_service();
126218
+ init_provider();
126219
+ init_usage_service();
126220
+ init_analysis_store();
126221
+ init_atomic_write();
126222
+ }
126223
+ });
126224
+
126225
+ // apps/server/dist/commands/analyze-persist.js
126226
+ import path13 from "node:path";
126227
+ function persistFullAnalysis(project, core2, startedAt) {
126228
+ const filename = buildAnalysisFilename(core2.analysisId, core2.now);
126229
+ const snapshot = {
126230
+ id: core2.analysisId,
126231
+ createdAt: core2.now,
126232
+ branch: core2.branch,
126233
+ commitHash: core2.commitHash,
126234
+ architecture: core2.architecture,
126235
+ status: "completed",
126236
+ metadata: core2.metadata,
126237
+ graph: core2.graph,
126238
+ violations: {
126239
+ added: core2.pipelineResult.added,
126240
+ resolved: core2.pipelineResult.resolvedRefs,
126241
+ previousAnalysisId: core2.previousAnalysisId
126242
+ },
126243
+ usage: core2.usage
126244
+ };
126245
+ const latest = buildLatestSnapshot(snapshot, filename, core2.pipelineResult.unchanged, core2.pipelineResult.added);
126246
+ const { bySeverity, total } = summarizeActiveViolations(latest.violations);
126247
+ writeAnalysis(project.path, snapshot);
126248
+ writeLatest(project.path, latest);
126249
+ appendHistory(project.path, buildHistoryEntry(snapshot, filename, core2.pipelineResult));
126250
+ deleteDiff(project.path);
126251
+ setLastAnalyzed(project.slug, core2.now);
126252
+ return {
126253
+ analysisId: core2.analysisId,
126254
+ filename,
126255
+ serviceCount: core2.graph.services.length,
126256
+ fileCount: core2.analysisResult.fileAnalyses?.length ?? 0,
126257
+ architecture: core2.architecture,
126258
+ durationMs: Date.now() - startedAt,
126259
+ violationsSummary: { total, bySeverity }
126260
+ };
126261
+ }
126262
+ function persistDiffAnalysis(project, core2) {
126263
+ if (!core2.latestBaseline) {
126264
+ throw new Error("Diff persist requires a latestBaseline \u2014 analyzeCore should have enforced this.");
126265
+ }
126266
+ const diff = buildDiffSnapshot(project.path, core2, core2.latestBaseline);
126267
+ writeDiff(project.path, diff);
126268
+ log.info(`[Diff] Done \u2014 ${diff.summary.newCount} new, ${diff.summary.unchangedCount} unchanged, ${diff.summary.resolvedCount} resolved across ${diff.changedFiles.length} changed files`);
126269
+ return { diff, isStale: false };
126270
+ }
126528
126271
  function buildLatestSnapshot(snapshot, filename, unchanged, added) {
126529
- const { graph } = snapshot;
126530
- const serviceById = new Map(graph.services.map((s) => [s.id, s.name]));
126531
- const moduleById = new Map(graph.modules.map((m) => [m.id, m.name]));
126532
- const methodById = new Map(graph.methods.map((m) => [m.id, m.name]));
126533
- const databaseById = new Map(graph.databases.map((d3) => [d3.id, d3.name]));
126534
- const denormalize = (v) => ({
126535
- ...v,
126536
- targetServiceName: v.targetServiceId ? serviceById.get(v.targetServiceId) ?? null : null,
126537
- targetModuleName: v.targetModuleId ? moduleById.get(v.targetModuleId) ?? null : null,
126538
- targetMethodName: v.targetMethodId ? methodById.get(v.targetMethodId) ?? null : null,
126539
- targetDatabaseName: v.targetDatabaseId ? databaseById.get(v.targetDatabaseId) ?? null : null
126540
- });
126272
+ const denormalize = makeDenormalizer(snapshot.graph);
126541
126273
  return {
126542
126274
  head: filename,
126543
126275
  analysis: {
@@ -126549,7 +126281,7 @@ function buildLatestSnapshot(snapshot, filename, unchanged, added) {
126549
126281
  metadata: snapshot.metadata,
126550
126282
  status: "completed"
126551
126283
  },
126552
- graph,
126284
+ graph: snapshot.graph,
126553
126285
  violations: [...added.map(denormalize), ...unchanged.map(denormalize)]
126554
126286
  };
126555
126287
  }
@@ -126613,17 +126345,159 @@ function buildHistoryEntry(snapshot, filename, pipeline) {
126613
126345
  }
126614
126346
  };
126615
126347
  }
126616
- function enforceLocationInvariant(violations) {
126617
- for (const v of violations) {
126618
- const hasFile = v.filePath != null;
126619
- const hasRange = v.lineStart != null && v.lineEnd != null;
126620
- if (hasFile === hasRange)
126348
+ function buildDiffSnapshot(repoPath, core2, baseline) {
126349
+ const { graph, changedFiles, pipelineResult } = core2;
126350
+ const denormalize = makeDenormalizer(graph);
126351
+ const newViolations = pipelineResult.added.map(denormalize);
126352
+ const latestById = new Map(baseline.violations.map((v) => [v.id, v]));
126353
+ const resolvedViolations = pipelineResult.resolvedRefs.map((r) => latestById.get(r.id)).filter((v) => !!v);
126354
+ const changedAbs = new Set(changedFiles.map((c2) => path13.resolve(repoPath, c2.path)));
126355
+ const matchesChanged = (p2) => !!p2 && (changedAbs.has(p2) || changedAbs.has(path13.resolve(repoPath, p2)));
126356
+ const affectedModules = graph.modules.filter((m) => matchesChanged(m.filePath));
126357
+ const affectedModuleIdSet = new Set(affectedModules.map((m) => m.id));
126358
+ const serviceNameById = new Map(graph.services.map((s) => [s.id, s.name]));
126359
+ const layerKeyById = new Map(graph.layers.map((l) => [l.id, `${l.serviceName}::${l.layer}`]));
126360
+ const affectedServices = /* @__PURE__ */ new Set();
126361
+ const affectedLayers = /* @__PURE__ */ new Set();
126362
+ const affectedModuleKeys = /* @__PURE__ */ new Set();
126363
+ for (const mod of affectedModules) {
126364
+ const svcName = serviceNameById.get(mod.serviceId);
126365
+ if (svcName) {
126366
+ affectedServices.add(svcName);
126367
+ affectedModuleKeys.add(`${svcName}::${mod.name}`);
126368
+ }
126369
+ const layerKey = layerKeyById.get(mod.layerId);
126370
+ if (layerKey)
126371
+ affectedLayers.add(layerKey);
126372
+ }
126373
+ const moduleNameById = new Map(graph.modules.map((m) => [m.id, m.name]));
126374
+ const affectedMethodKeys = [];
126375
+ for (const method of graph.methods) {
126376
+ if (!affectedModuleIdSet.has(method.moduleId))
126621
126377
  continue;
126622
- log.warn(`[Violations] ${v.ruleKey}: partial location (filePath=${v.filePath}, lineStart=${v.lineStart}, lineEnd=${v.lineEnd}) \u2014 dropping to uphold the location invariant`);
126623
- v.filePath = null;
126624
- v.lineStart = null;
126625
- v.lineEnd = null;
126378
+ const modName = moduleNameById.get(method.moduleId);
126379
+ const mod = graph.modules.find((m) => m.id === method.moduleId);
126380
+ const svcName = mod ? serviceNameById.get(mod.serviceId) : void 0;
126381
+ if (svcName && modName)
126382
+ affectedMethodKeys.push(`${svcName}::${modName}::${method.name}`);
126626
126383
  }
126384
+ return {
126385
+ id: core2.analysisId,
126386
+ baseAnalysisId: baseline.analysis.id,
126387
+ createdAt: core2.now,
126388
+ branch: core2.branch,
126389
+ commitHash: core2.commitHash,
126390
+ graph,
126391
+ changedFiles,
126392
+ newViolations,
126393
+ resolvedViolations,
126394
+ affectedNodeIds: {
126395
+ services: [...affectedServices],
126396
+ layers: [...affectedLayers],
126397
+ modules: [...affectedModuleKeys],
126398
+ methods: affectedMethodKeys
126399
+ },
126400
+ summary: {
126401
+ newCount: newViolations.length,
126402
+ unchangedCount: pipelineResult.unchanged.length,
126403
+ resolvedCount: resolvedViolations.length
126404
+ },
126405
+ usage: core2.usage
126406
+ };
126407
+ }
126408
+ function makeDenormalizer(graph) {
126409
+ const serviceById = new Map(graph.services.map((s) => [s.id, s.name]));
126410
+ const moduleById = new Map(graph.modules.map((m) => [m.id, m.name]));
126411
+ const methodById = new Map(graph.methods.map((m) => [m.id, m.name]));
126412
+ const databaseById = new Map(graph.databases.map((d3) => [d3.id, d3.name]));
126413
+ return (v) => ({
126414
+ ...v,
126415
+ targetServiceName: v.targetServiceId ? serviceById.get(v.targetServiceId) ?? null : null,
126416
+ targetModuleName: v.targetModuleId ? moduleById.get(v.targetModuleId) ?? null : null,
126417
+ targetMethodName: v.targetMethodId ? methodById.get(v.targetMethodId) ?? null : null,
126418
+ targetDatabaseName: v.targetDatabaseId ? databaseById.get(v.targetDatabaseId) ?? null : null
126419
+ });
126420
+ }
126421
+ var init_analyze_persist = __esm({
126422
+ "apps/server/dist/commands/analyze-persist.js"() {
126423
+ "use strict";
126424
+ init_logger();
126425
+ init_registry();
126426
+ init_analysis_store();
126427
+ }
126428
+ });
126429
+
126430
+ // apps/server/dist/commands/diff-in-process.js
126431
+ var diff_in_process_exports = {};
126432
+ __export(diff_in_process_exports, {
126433
+ diffInProcess: () => diffInProcess
126434
+ });
126435
+ async function diffInProcess(project, options = {}) {
126436
+ const core2 = await analyzeCore(project, { ...options, mode: "diff" });
126437
+ return persistDiffAnalysis(project, core2);
126438
+ }
126439
+ var init_diff_in_process = __esm({
126440
+ "apps/server/dist/commands/diff-in-process.js"() {
126441
+ "use strict";
126442
+ init_analyze_core();
126443
+ init_analyze_persist();
126444
+ }
126445
+ });
126446
+
126447
+ // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
126448
+ var import_index = __toESM(require_commander(), 1);
126449
+ var {
126450
+ program,
126451
+ createCommand,
126452
+ createArgument,
126453
+ createOption,
126454
+ CommanderError,
126455
+ InvalidArgumentError,
126456
+ InvalidOptionArgumentError,
126457
+ // deprecated old name
126458
+ Command,
126459
+ Argument,
126460
+ Option,
126461
+ Help
126462
+ } = import_index.default;
126463
+
126464
+ // tools/cli/src/index.ts
126465
+ init_dist4();
126466
+
126467
+ // tools/cli/src/commands/add.ts
126468
+ init_dist4();
126469
+ init_paths();
126470
+ init_registry();
126471
+ init_helpers();
126472
+ async function runAdd() {
126473
+ const repoPath = resolveRepoDir(process.cwd()) ?? process.cwd();
126474
+ mt("Adding repository to TrueCourse");
126475
+ O2.step(repoPath);
126476
+ ensureRepoTruecourseDir(repoPath);
126477
+ const existing = getProjectByPath(repoPath);
126478
+ const entry = registerProject(repoPath);
126479
+ if (existing) {
126480
+ O2.info(`Repository "${entry.name}" is already registered.`);
126481
+ } else {
126482
+ O2.success(`Repository "${entry.name}" added.`);
126483
+ }
126484
+ await promptInstallSkills(repoPath);
126485
+ gt("Run `truecourse analyze` to generate analysis data.");
126486
+ }
126487
+
126488
+ // tools/cli/src/commands/analyze.ts
126489
+ init_dist4();
126490
+ import { execSync } from "node:child_process";
126491
+ import path15 from "node:path";
126492
+
126493
+ // apps/server/dist/commands/analyze-in-process.js
126494
+ init_analysis_store();
126495
+ init_analyze_core();
126496
+ init_analyze_persist();
126497
+ async function analyzeInProcess(project, options = {}) {
126498
+ const startedAt = Date.now();
126499
+ const core2 = await analyzeCore(project, { ...options, mode: "full" });
126500
+ return persistFullAnalysis(project, core2, startedAt);
126627
126501
  }
126628
126502
 
126629
126503
  // tools/cli/src/commands/analyze.ts
@@ -126634,10 +126508,24 @@ init_project_config();
126634
126508
  init_logger();
126635
126509
  init_helpers();
126636
126510
 
126511
+ // tools/cli/src/commands/llm-prompt.ts
126512
+ init_dist4();
126513
+ async function promptLlmEstimate(estimate) {
126514
+ const totalRules = estimate.uniqueRuleCount ?? estimate.tiers.reduce((s, t2) => s + t2.ruleCount, 0);
126515
+ const totalFiles = estimate.uniqueFileCount ?? estimate.tiers.reduce((s, t2) => s + t2.fileCount, 0);
126516
+ const tokens = estimate.totalEstimatedTokens;
126517
+ const tokenStr = tokens >= 1e6 ? `~${(tokens / 1e6).toFixed(1)}M tokens` : `~${Math.round(tokens / 1e3)}k tokens`;
126518
+ O2.step(`LLM will analyze ${totalFiles} files with ${totalRules} rules (${tokenStr})`);
126519
+ const proceed = await ot2({ message: "Run LLM-powered rules?", initialValue: true });
126520
+ if (q(proceed)) return false;
126521
+ if (!proceed) O2.info("Skipping LLM rules.");
126522
+ return !!proceed;
126523
+ }
126524
+
126637
126525
  // tools/cli/src/telemetry.ts
126638
126526
  init_dist4();
126639
126527
  import fs9 from "node:fs";
126640
- import path13 from "node:path";
126528
+ import path14 from "node:path";
126641
126529
  import os4 from "node:os";
126642
126530
  import crypto from "node:crypto";
126643
126531
  var DEFAULT_CONFIG2 = {
@@ -126646,7 +126534,7 @@ var DEFAULT_CONFIG2 = {
126646
126534
  noticeShown: false
126647
126535
  };
126648
126536
  function getTelemetryConfigPath() {
126649
- return path13.join(os4.homedir(), ".truecourse", "telemetry.json");
126537
+ return path14.join(os4.homedir(), ".truecourse", "telemetry.json");
126650
126538
  }
126651
126539
  function readTelemetryConfig() {
126652
126540
  const configPath = getTelemetryConfigPath();
@@ -126671,7 +126559,7 @@ function readTelemetryConfig() {
126671
126559
  }
126672
126560
  function writeTelemetryConfig(partial) {
126673
126561
  const configPath = getTelemetryConfigPath();
126674
- const dir = path13.dirname(configPath);
126562
+ const dir = path14.dirname(configPath);
126675
126563
  fs9.mkdirSync(dir, { recursive: true });
126676
126564
  let current;
126677
126565
  try {
@@ -126806,16 +126694,9 @@ async function runAnalyze(_options = {}) {
126806
126694
  enableLlmRulesOverride: enableLlmRules,
126807
126695
  onLlmEstimate: async (estimate) => {
126808
126696
  stopSpinner();
126809
- const totalRules = estimate.uniqueRuleCount ?? estimate.tiers.reduce((s, t2) => s + t2.ruleCount, 0);
126810
- const totalFiles = estimate.uniqueFileCount ?? estimate.tiers.reduce((s, t2) => s + t2.fileCount, 0);
126811
- const tokens = estimate.totalEstimatedTokens;
126812
- const tokenStr = tokens >= 1e6 ? `~${(tokens / 1e6).toFixed(1)}M tokens` : `~${Math.round(tokens / 1e3)}k tokens`;
126813
- O2.step(`LLM will analyze ${totalFiles} files with ${totalRules} rules (${tokenStr})`);
126814
- const proceed = await ot2({ message: "Run LLM-powered rules?", initialValue: true });
126697
+ const proceed = await promptLlmEstimate(estimate);
126815
126698
  renderPhase = "post-llm";
126816
- if (q(proceed)) return false;
126817
- if (!proceed) O2.info("Skipping LLM rules.");
126818
- return !!proceed;
126699
+ return proceed;
126819
126700
  }
126820
126701
  });
126821
126702
  stopSpinner();
@@ -126846,19 +126727,32 @@ async function runAnalyzeDiff(_options = {}) {
126846
126727
  configureLogger({
126847
126728
  filePath: path15.join(project.path, ".truecourse/logs/analyze.log")
126848
126729
  });
126849
- const spinner = fe();
126850
- spinner.start("Checking changes...");
126730
+ const config2 = readProjectConfig(project.path);
126731
+ const enabledCategories = config2.enabledCategories ?? void 0;
126732
+ const enableLlmRules = config2.enableLlmRules ?? true;
126733
+ renderPhase = enableLlmRules ? "pre-llm" : "all";
126734
+ const stepDefs = buildAnalysisSteps(enabledCategories, enableLlmRules);
126735
+ const tracker = new StepTracker((payload) => {
126736
+ if (payload.steps) renderSteps(payload.steps);
126737
+ }, stepDefs);
126851
126738
  const abortController = new AbortController();
126852
126739
  const onSigint = () => abortController.abort();
126853
126740
  process.on("SIGINT", onSigint);
126854
126741
  try {
126855
126742
  const { diff } = await diffInProcess2(project, {
126743
+ tracker,
126856
126744
  signal: abortController.signal,
126857
- onProgress: ({ detail }) => {
126858
- if (detail) spinner.message(detail);
126745
+ enabledCategoriesOverride: enabledCategories,
126746
+ enableLlmRulesOverride: enableLlmRules,
126747
+ onLlmEstimate: async (estimate) => {
126748
+ stopSpinner();
126749
+ const proceed = await promptLlmEstimate(estimate);
126750
+ renderPhase = "post-llm";
126751
+ return proceed;
126859
126752
  }
126860
126753
  });
126861
- spinner.stop("Diff check complete");
126754
+ stopSpinner();
126755
+ O2.success("Diff check complete");
126862
126756
  renderDiffResultsSummary2({
126863
126757
  changedFiles: diff.changedFiles,
126864
126758
  newViolations: diff.newViolations,
@@ -126868,7 +126762,7 @@ async function runAnalyzeDiff(_options = {}) {
126868
126762
  });
126869
126763
  gt("Diff complete \u2014 view results with: truecourse dashboard");
126870
126764
  } catch (err) {
126871
- spinner.stop("Diff check failed");
126765
+ stopSpinner();
126872
126766
  if (err instanceof DOMException && err.name === "AbortError") {
126873
126767
  gt("Diff cancelled");
126874
126768
  process.exit(130);
@@ -127313,7 +127207,7 @@ function targetUrlFor(baseUrl) {
127313
127207
  const repoDir = resolveRepoDir(process.cwd());
127314
127208
  if (!repoDir) return baseUrl;
127315
127209
  const entry = getProjectByPath(repoDir) ?? registerProject(repoDir);
127316
- return `${baseUrl}/projects/${entry.slug}`;
127210
+ return `${baseUrl}/repos/${entry.slug}`;
127317
127211
  }
127318
127212
  async function promptRunMode() {
127319
127213
  const choice = await _t({
@@ -127388,7 +127282,7 @@ async function runServiceMode(serverEntry) {
127388
127282
  O2.success(`Dashboard open at ${target}`);
127389
127283
  O2.info("Stop the dashboard with: truecourse dashboard stop");
127390
127284
  }
127391
- async function runDashboard() {
127285
+ async function runDashboard(options = {}) {
127392
127286
  mt("Opening TrueCourse dashboard");
127393
127287
  const serverEntry = resolveServerEntry();
127394
127288
  if (!serverEntry) {
@@ -127398,8 +127292,9 @@ async function runDashboard() {
127398
127292
  process.exit(1);
127399
127293
  }
127400
127294
  const configured = fs14.existsSync(getConfigPath());
127401
- const runMode = configured ? readConfig().runMode : await promptRunMode();
127402
- if (!configured) writeConfig({ runMode });
127295
+ const shouldPrompt = !configured || options.reconfigure;
127296
+ const runMode = shouldPrompt ? await promptRunMode() : readConfig().runMode;
127297
+ if (shouldPrompt) writeConfig({ runMode });
127403
127298
  if (runMode === "service") {
127404
127299
  try {
127405
127300
  await runServiceMode(serverEntry);
@@ -127497,55 +127392,163 @@ async function runDashboardUninstall() {
127497
127392
 
127498
127393
  // tools/cli/src/commands/list.ts
127499
127394
  init_dist4();
127395
+
127396
+ // apps/server/dist/services/violation-query.service.js
127397
+ init_analysis_store();
127398
+ var SEVERITY_ORDER = {
127399
+ critical: 0,
127400
+ high: 1,
127401
+ medium: 2,
127402
+ low: 3,
127403
+ info: 4
127404
+ };
127405
+ function listViolations(repoPath, options = {}) {
127406
+ const latest = readLatest(repoPath);
127407
+ if (!latest)
127408
+ return { violations: [], total: 0 };
127409
+ if (options.analysisId && latest.analysis.id !== options.analysisId) {
127410
+ return { violations: [], total: 0 };
127411
+ }
127412
+ const statusMode = options.status ?? "active";
127413
+ let filtered;
127414
+ if (statusMode === "resolved") {
127415
+ filtered = latest.violations.filter((v) => v.status === "resolved");
127416
+ } else if (statusMode === "all") {
127417
+ filtered = latest.violations;
127418
+ } else {
127419
+ const active = ["new", "unchanged"];
127420
+ filtered = latest.violations.filter((v) => active.includes(v.status));
127421
+ }
127422
+ if (options.filePath) {
127423
+ const absPath = options.filePath.startsWith("/") ? options.filePath : `${repoPath}/${options.filePath}`;
127424
+ filtered = filtered.filter((v) => v.type === "code" && (v.filePath === absPath || v.filePath === options.filePath));
127425
+ }
127426
+ filtered.sort((a, b) => {
127427
+ const sa = SEVERITY_ORDER[a.severity] ?? 5;
127428
+ const sb = SEVERITY_ORDER[b.severity] ?? 5;
127429
+ if (sa !== sb)
127430
+ return sa - sb;
127431
+ return b.createdAt.localeCompare(a.createdAt);
127432
+ });
127433
+ const total = filtered.length;
127434
+ const limit = options.limit ?? 0;
127435
+ const offset = options.offset ?? 0;
127436
+ const paged = limit > 0 ? filtered.slice(offset, offset + limit) : filtered;
127437
+ return { violations: paged, total };
127438
+ }
127439
+ function getDiffResult(repoPath) {
127440
+ const diff = readDiff(repoPath);
127441
+ if (!diff)
127442
+ return null;
127443
+ const latest = readLatest(repoPath);
127444
+ const isStale = latest ? latest.analysis.id !== diff.baseAnalysisId : false;
127445
+ return { diff, isStale };
127446
+ }
127447
+
127448
+ // tools/cli/src/commands/list.ts
127500
127449
  init_helpers();
127501
127450
  async function runList({ limit = 20, offset = 0 } = {}) {
127502
127451
  mt("Violations");
127503
- await requireDashboard();
127504
127452
  const repo = requireRegisteredRepo();
127505
- const serverUrl = getServerUrl();
127506
- const showAll = !isFinite(limit);
127507
- const params = new URLSearchParams();
127508
- if (!showAll) {
127509
- params.set("limit", String(limit));
127510
- params.set("offset", String(offset));
127511
- }
127512
- const url = `${serverUrl}/api/repos/${repo.id}/violations${params.toString() ? `?${params}` : ""}`;
127513
- const res = await fetch(url);
127514
- if (!res.ok) {
127515
- O2.error(`Failed to fetch violations: ${res.status}`);
127516
- process.exit(1);
127517
- }
127518
- const body = await res.json();
127519
- if (Array.isArray(body)) {
127520
- renderViolations(body, { total: body.length });
127521
- } else {
127522
- const { violations, total } = body;
127523
- renderViolations(violations, { total, offset });
127453
+ const { violations, total } = listViolations(repo.path, {
127454
+ limit: isFinite(limit) ? limit : 0,
127455
+ offset
127456
+ });
127457
+ if (total === 0 && violations.length === 0) {
127458
+ O2.info("No violations. Run `truecourse analyze` if you haven't yet.");
127459
+ return;
127524
127460
  }
127461
+ renderViolations(violations, { total, offset });
127525
127462
  }
127526
127463
  async function runListDiff() {
127527
127464
  mt("Diff check results");
127528
- await requireDashboard();
127529
127465
  const repo = requireRegisteredRepo();
127530
- const serverUrl = getServerUrl();
127531
- const res = await fetch(`${serverUrl}/api/repos/${repo.id}/diff-check`);
127532
- if (!res.ok) {
127533
- O2.error(`Failed to fetch diff check results: ${res.status}`);
127534
- process.exit(1);
127535
- }
127536
- const result = await res.json();
127466
+ const result = getDiffResult(repo.path);
127537
127467
  if (!result) {
127538
127468
  O2.info("No diff check found. Run `truecourse analyze --diff` first.");
127539
127469
  return;
127540
127470
  }
127541
- if (result.isStale) {
127471
+ const { diff, isStale } = result;
127472
+ if (isStale) {
127542
127473
  O2.warn("Results may be stale \u2014 baseline analysis has changed.");
127543
127474
  }
127544
- renderDiffResults(result);
127475
+ const rendered = {
127476
+ isStale,
127477
+ changedFiles: diff.changedFiles,
127478
+ newViolations: diff.newViolations,
127479
+ resolvedViolations: diff.resolvedViolations,
127480
+ summary: { newCount: diff.summary.newCount, resolvedCount: diff.summary.resolvedCount }
127481
+ };
127482
+ renderDiffResults(rendered);
127545
127483
  }
127546
127484
 
127547
- // tools/cli/src/index.ts
127485
+ // tools/cli/src/commands/rules.ts
127486
+ init_dist4();
127487
+ init_dist7();
127488
+ init_project_config();
127548
127489
  init_helpers();
127490
+ var ALL_CATEGORIES = [...DOMAIN_ORDER];
127491
+ async function runRulesCategories(options) {
127492
+ const repo = requireRegisteredRepo();
127493
+ if (options.reset) {
127494
+ updateProjectConfig(repo.path, { enabledCategories: null });
127495
+ O2.success("Reset to global default categories.");
127496
+ return;
127497
+ }
127498
+ if (options.enable || options.disable) {
127499
+ const cat = options.enable ?? options.disable;
127500
+ if (!ALL_CATEGORIES.includes(cat)) {
127501
+ O2.error(`Invalid category: ${cat}. Valid: ${ALL_CATEGORIES.join(", ")}`);
127502
+ process.exit(1);
127503
+ }
127504
+ const config3 = readProjectConfig(repo.path);
127505
+ const hasOverride = config3.enabledCategories != null;
127506
+ const current = new Set(hasOverride ? config3.enabledCategories : ALL_CATEGORIES);
127507
+ if (options.enable) current.add(cat);
127508
+ else current.delete(cat);
127509
+ updateProjectConfig(repo.path, { enabledCategories: [...current] });
127510
+ O2.success(`${options.enable ? "Enabled" : "Disabled"} ${cat} rules for ${repo.name}.`);
127511
+ return;
127512
+ }
127513
+ const config2 = readProjectConfig(repo.path);
127514
+ const isOverride = config2.enabledCategories != null;
127515
+ const enabled = new Set(isOverride ? config2.enabledCategories : ALL_CATEGORIES);
127516
+ const status = (cat) => enabled.has(cat) ? "\x1B[32menabled\x1B[0m" : "\x1B[31mdisabled\x1B[0m";
127517
+ O2.info(
127518
+ `Rule categories for ${repo.name}${isOverride ? " (per-repo override)" : " (global default)"}:`
127519
+ );
127520
+ for (const cat of ALL_CATEGORIES) {
127521
+ console.log(` ${cat.padEnd(14)} ${status(cat)}`);
127522
+ }
127523
+ console.log("");
127524
+ if (!isOverride) {
127525
+ O2.info("Override with: truecourse rules categories --enable/--disable <name>");
127526
+ }
127527
+ }
127528
+ async function runRulesLlm(options) {
127529
+ const repo = requireRegisteredRepo();
127530
+ if (options.reset) {
127531
+ updateProjectConfig(repo.path, { enableLlmRules: null });
127532
+ O2.success("Reset LLM rules to global default.");
127533
+ return;
127534
+ }
127535
+ if (options.enable || options.disable) {
127536
+ const enabled = !!options.enable;
127537
+ updateProjectConfig(repo.path, { enableLlmRules: enabled });
127538
+ O2.success(`LLM rules ${enabled ? "enabled" : "disabled"} for ${repo.name}.`);
127539
+ return;
127540
+ }
127541
+ const config2 = readProjectConfig(repo.path);
127542
+ const isOverride = config2.enableLlmRules != null;
127543
+ const effective = isOverride ? config2.enableLlmRules : true;
127544
+ const status = effective ? "\x1B[32menabled\x1B[0m" : "\x1B[31mdisabled\x1B[0m";
127545
+ O2.info(
127546
+ `LLM rules for ${repo.name}${isOverride ? " (per-repo override)" : " (global default)"}: ${status}`
127547
+ );
127548
+ if (!isOverride) {
127549
+ O2.info("Override with: truecourse rules llm --enable/--disable");
127550
+ }
127551
+ }
127549
127552
 
127550
127553
  // tools/cli/src/commands/hooks.ts
127551
127554
  import { execSync as execSync5 } from "node:child_process";
@@ -130437,9 +130440,9 @@ async function runHooksRun() {
130437
130440
 
130438
130441
  // tools/cli/src/index.ts
130439
130442
  var program2 = new Command();
130440
- program2.name("truecourse").version("0.4.2").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130441
- var dashboardCmd = program2.command("dashboard").description("Start the TrueCourse dashboard and open it in your browser").action(async () => {
130442
- await runDashboard();
130443
+ program2.name("truecourse").version("0.4.4").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130444
+ var dashboardCmd = program2.command("dashboard").description("Start the TrueCourse dashboard and open it in your browser").option("--reconfigure", "Re-prompt for console vs background service mode").action(async (options) => {
130445
+ await runDashboard({ reconfigure: options.reconfigure });
130443
130446
  });
130444
130447
  dashboardCmd.command("stop").description("Stop the dashboard").action(async () => {
130445
130448
  await runDashboardStop();
@@ -130475,94 +130478,10 @@ program2.command("list").description("List violations from the latest analysis")
130475
130478
  });
130476
130479
  var rulesCmd = program2.command("rules").description("Manage analysis rules");
130477
130480
  rulesCmd.command("categories").description("View or override rule categories for this repository").option("--enable <category>", "Enable a category").option("--disable <category>", "Disable a category").option("--reset", "Reset to global default").action(async (options) => {
130478
- await requireDashboard();
130479
- const repo = requireRegisteredRepo();
130480
- const serverUrl = getServerUrl();
130481
- const { DOMAIN_ORDER: DOMAIN_ORDER2 } = await Promise.resolve().then(() => (init_dist7(), dist_exports3));
130482
- const allCategories = [...DOMAIN_ORDER2];
130483
- if (options.reset) {
130484
- await fetch(`${serverUrl}/api/repos/${repo.id}/categories`, {
130485
- method: "PUT",
130486
- headers: { "Content-Type": "application/json" },
130487
- body: JSON.stringify({ enabledCategories: null })
130488
- });
130489
- O2.success("Reset to global default categories.");
130490
- return;
130491
- }
130492
- if (options.enable || options.disable) {
130493
- const cat = options.enable || options.disable;
130494
- if (!allCategories.includes(cat)) {
130495
- O2.error(`Invalid category: ${cat}. Valid: ${allCategories.join(", ")}`);
130496
- process.exit(1);
130497
- }
130498
- const repoRes2 = await fetch(`${serverUrl}/api/repos/${repo.id}`);
130499
- const repoData2 = await repoRes2.json();
130500
- const hasOverride = repoData2.enabledCategories !== null && repoData2.enabledCategories !== void 0;
130501
- const current = new Set(
130502
- hasOverride ? repoData2.enabledCategories : allCategories
130503
- );
130504
- if (options.enable) current.add(cat);
130505
- else current.delete(cat);
130506
- await fetch(`${serverUrl}/api/repos/${repo.id}/categories`, {
130507
- method: "PUT",
130508
- headers: { "Content-Type": "application/json" },
130509
- body: JSON.stringify({ enabledCategories: [...current] })
130510
- });
130511
- O2.success(`${options.enable ? "Enabled" : "Disabled"} ${cat} rules for ${repo.name}.`);
130512
- return;
130513
- }
130514
- const repoRes = await fetch(`${serverUrl}/api/repos/${repo.id}`);
130515
- const repoData = await repoRes.json();
130516
- const isOverride = repoData.enabledCategories !== null && repoData.enabledCategories !== void 0;
130517
- const enabled = new Set(
130518
- isOverride ? repoData.enabledCategories : allCategories
130519
- );
130520
- const status = (cat) => enabled.has(cat) ? "\x1B[32menabled\x1B[0m" : "\x1B[31mdisabled\x1B[0m";
130521
- O2.info(
130522
- `Rule categories for ${repo.name}${isOverride ? " (per-repo override)" : " (global default)"}:`
130523
- );
130524
- for (const cat of allCategories) {
130525
- console.log(` ${cat.padEnd(14)} ${status(cat)}`);
130526
- }
130527
- console.log("");
130528
- if (!isOverride) {
130529
- O2.info("Override with: truecourse rules categories --enable/--disable <name>");
130530
- }
130481
+ await runRulesCategories(options);
130531
130482
  });
130532
130483
  rulesCmd.command("llm").description("Enable or disable LLM-powered rules for this repository").option("--enable", "Enable LLM rules").option("--disable", "Disable LLM rules").option("--reset", "Reset to global default").action(async (options) => {
130533
- await requireDashboard();
130534
- const repo = requireRegisteredRepo();
130535
- const serverUrl = getServerUrl();
130536
- if (options.reset) {
130537
- await fetch(`${serverUrl}/api/repos/${repo.id}/llm`, {
130538
- method: "PUT",
130539
- headers: { "Content-Type": "application/json" },
130540
- body: JSON.stringify({ enableLlmRules: null })
130541
- });
130542
- O2.success("Reset LLM rules to global default.");
130543
- return;
130544
- }
130545
- if (options.enable || options.disable) {
130546
- const enabled = !!options.enable;
130547
- await fetch(`${serverUrl}/api/repos/${repo.id}/llm`, {
130548
- method: "PUT",
130549
- headers: { "Content-Type": "application/json" },
130550
- body: JSON.stringify({ enableLlmRules: enabled })
130551
- });
130552
- O2.success(`LLM rules ${enabled ? "enabled" : "disabled"} for ${repo.name}.`);
130553
- return;
130554
- }
130555
- const repoRes = await fetch(`${serverUrl}/api/repos/${repo.id}`);
130556
- const repoData = await repoRes.json();
130557
- const isOverride = repoData.enableLlmRules !== null && repoData.enableLlmRules !== void 0;
130558
- const effective = isOverride ? repoData.enableLlmRules : true;
130559
- const status = effective ? "\x1B[32menabled\x1B[0m" : "\x1B[31mdisabled\x1B[0m";
130560
- O2.info(
130561
- `LLM rules for ${repo.name}${isOverride ? " (per-repo override)" : " (global default)"}: ${status}`
130562
- );
130563
- if (!isOverride) {
130564
- O2.info("Override with: truecourse rules llm --enable/--disable");
130565
- }
130484
+ await runRulesLlm(options);
130566
130485
  });
130567
130486
  var telemetryCmd = program2.command("telemetry").description("Manage anonymous usage telemetry");
130568
130487
  telemetryCmd.command("enable").description("Enable anonymous usage telemetry").action(() => {