truecourse 0.4.3 → 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(`
@@ -4425,32 +4348,184 @@ var init_helpers = __esm({
4425
4348
  }
4426
4349
  });
4427
4350
 
4428
- // apps/server/dist/lib/logger.js
4351
+ // apps/server/dist/lib/atomic-write.js
4429
4352
  import fs4 from "node:fs";
4430
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";
4431
4506
  function rotateLog(filePath) {
4432
- if (!fs4.existsSync(filePath))
4507
+ if (!fs6.existsSync(filePath))
4433
4508
  return;
4434
- const stats = fs4.statSync(filePath);
4509
+ const stats = fs6.statSync(filePath);
4435
4510
  if (stats.size < MAX_LOG_SIZE)
4436
4511
  return;
4437
4512
  for (let i = MAX_LOG_FILES; i >= 1; i--) {
4438
4513
  const older = `${filePath}.${i}`;
4439
4514
  if (i === MAX_LOG_FILES) {
4440
- if (fs4.existsSync(older))
4441
- fs4.unlinkSync(older);
4515
+ if (fs6.existsSync(older))
4516
+ fs6.unlinkSync(older);
4442
4517
  } else {
4443
4518
  const newer = `${filePath}.${i + 1}`;
4444
- if (fs4.existsSync(older))
4445
- fs4.renameSync(older, newer);
4519
+ if (fs6.existsSync(older))
4520
+ fs6.renameSync(older, newer);
4446
4521
  }
4447
4522
  }
4448
- fs4.renameSync(filePath, `${filePath}.1`);
4523
+ fs6.renameSync(filePath, `${filePath}.1`);
4449
4524
  }
4450
4525
  function openSink(config2) {
4451
- fs4.mkdirSync(path4.dirname(config2.filePath), { recursive: true });
4526
+ fs6.mkdirSync(path6.dirname(config2.filePath), { recursive: true });
4452
4527
  rotateLog(config2.filePath);
4453
- const stream = fs4.createWriteStream(config2.filePath, { flags: "a" });
4528
+ const stream = fs6.createWriteStream(config2.filePath, { flags: "a" });
4454
4529
  stream.write(`
4455
4530
  --- ${(/* @__PURE__ */ new Date()).toISOString()} ---
4456
4531
  `);
@@ -5082,7 +5157,7 @@ var require_node = __commonJS({
5082
5157
  exports.inspectOpts = Object.keys(process.env).filter((key) => {
5083
5158
  return /^debug_/i.test(key);
5084
5159
  }).reduce((obj, key) => {
5085
- const prop = key.substring(6).toLowerCase().replace(/_([a-z])/g, (_2, k) => {
5160
+ const prop = key.substring(6).toLowerCase().replace(/_([a-z])/g, (_, k) => {
5086
5161
  return k.toUpperCase();
5087
5162
  });
5088
5163
  let val = process.env[key];
@@ -6558,7 +6633,7 @@ function deleteBranchTask(branch, forceDelete = false) {
6558
6633
  parser(stdOut, stdErr) {
6559
6634
  return parseBranchDeletions(stdOut, stdErr).branches[branch];
6560
6635
  },
6561
- onError({ exitCode, stdErr, stdOut }, error, _2, fail) {
6636
+ onError({ exitCode, stdErr, stdOut }, error, _, fail) {
6562
6637
  if (!hasBranchDeletionError(String(error), exitCode)) {
6563
6638
  return fail(error);
6564
6639
  }
@@ -9762,20 +9837,20 @@ var init_git = __esm({
9762
9837
  });
9763
9838
 
9764
9839
  // apps/server/dist/config/project-config.js
9765
- import fs5 from "node:fs";
9840
+ import fs7 from "node:fs";
9766
9841
  function readProjectConfig(repoDir) {
9767
9842
  const file = getRepoConfigPath(repoDir);
9768
- if (!fs5.existsSync(file))
9843
+ if (!fs7.existsSync(file))
9769
9844
  return { ...EMPTY };
9770
9845
  try {
9771
- return JSON.parse(fs5.readFileSync(file, "utf-8"));
9846
+ return JSON.parse(fs7.readFileSync(file, "utf-8"));
9772
9847
  } catch {
9773
9848
  return { ...EMPTY };
9774
9849
  }
9775
9850
  }
9776
9851
  function writeProjectConfig(repoDir, config2) {
9777
9852
  ensureRepoTruecourseDir(repoDir);
9778
- fs5.writeFileSync(getRepoConfigPath(repoDir), JSON.stringify(config2, null, 2), "utf-8");
9853
+ fs7.writeFileSync(getRepoConfigPath(repoDir), JSON.stringify(config2, null, 2), "utf-8");
9779
9854
  }
9780
9855
  function updateProjectConfig(repoDir, patch) {
9781
9856
  const current = readProjectConfig(repoDir);
@@ -9844,7 +9919,7 @@ var require_ignore = __commonJS({
9844
9919
  // (a ) -> (a)
9845
9920
  // (a \ ) -> (a )
9846
9921
  /((?:\\\\)*?)(\\?\s+)$/,
9847
- (_2, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY2)
9922
+ (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY2)
9848
9923
  ],
9849
9924
  // Replace (\ ) with ' '
9850
9925
  // (\ ) -> ' '
@@ -9852,7 +9927,7 @@ var require_ignore = __commonJS({
9852
9927
  // (\\\ ) -> '\\ '
9853
9928
  [
9854
9929
  /(\\+?)\s/g,
9855
- (_2, m1) => {
9930
+ (_, m1) => {
9856
9931
  const { length } = m1;
9857
9932
  return m1.slice(0, length - length % 2) + SPACE;
9858
9933
  }
@@ -9923,7 +9998,7 @@ var require_ignore = __commonJS({
9923
9998
  // Zero, one or several directories
9924
9999
  // should not use '*', or it will be replaced by the next replacer
9925
10000
  // Check if it is not the last `'/**'`
9926
- (_2, index, str2) => index + 6 < str2.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
10001
+ (_, index, str2) => index + 6 < str2.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
9927
10002
  ],
9928
10003
  // normal intermediate wildcards
9929
10004
  [
@@ -9935,7 +10010,7 @@ var require_ignore = __commonJS({
9935
10010
  /(^|[^\\]+)(\\\*)+(?=.+)/g,
9936
10011
  // '*.js' matches '.js'
9937
10012
  // '*.js' doesn't match 'abc'
9938
- (_2, p1, p2) => {
10013
+ (_, p1, p2) => {
9939
10014
  const unescaped = p2.replace(/\\\*/g, "[^\\/]*");
9940
10015
  return p1 + unescaped;
9941
10016
  }
@@ -9982,11 +10057,11 @@ var require_ignore = __commonJS({
9982
10057
  var MODE_CHECK_IGNORE = "checkRegex";
9983
10058
  var UNDERSCORE = "_";
9984
10059
  var TRAILING_WILD_CARD_REPLACERS = {
9985
- [MODE_IGNORE](_2, p1) {
10060
+ [MODE_IGNORE](_, p1) {
9986
10061
  const prefix = p1 ? `${p1}[^/]+` : "[^/]*";
9987
10062
  return `${prefix}(?=$|\\/$)`;
9988
10063
  },
9989
- [MODE_CHECK_IGNORE](_2, p1) {
10064
+ [MODE_CHECK_IGNORE](_, p1) {
9990
10065
  const prefix = p1 ? `${p1}[^/]*` : "[^/]*";
9991
10066
  return `${prefix}(?=$|\\/$)`;
9992
10067
  }
@@ -14772,7 +14847,7 @@ var init_ast = __esm({
14772
14847
  if (!isExtglobAST(this)) {
14773
14848
  const noEmpty = this.isStart() && this.isEnd() && !this.#parts.some((s) => typeof s !== "string");
14774
14849
  const src = this.#parts.map((p2) => {
14775
- 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);
14776
14851
  this.#hasMagic = this.#hasMagic || hasMagic;
14777
14852
  this.#uflag = this.#uflag || uflag;
14778
14853
  return re2;
@@ -14878,7 +14953,7 @@ var init_ast = __esm({
14878
14953
  if (typeof p2 === "string") {
14879
14954
  throw new Error("string type in extglob ast??");
14880
14955
  }
14881
- const [re2, _2, _hasMagic, uflag] = p2.toRegExpSource(dot);
14956
+ const [re2, _, _hasMagic, uflag] = p2.toRegExpSource(dot);
14882
14957
  this.#uflag = this.#uflag || uflag;
14883
14958
  return re2;
14884
14959
  }).filter((p2) => !(this.isStart() && this.isEnd()) || !!p2).join("|");
@@ -14951,7 +15026,7 @@ var init_escape = __esm({
14951
15026
  });
14952
15027
 
14953
15028
  // node_modules/.pnpm/minimatch@10.2.4/node_modules/minimatch/dist/esm/index.js
14954
- 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;
14955
15030
  var init_esm4 = __esm({
14956
15031
  "node_modules/.pnpm/minimatch@10.2.4/node_modules/minimatch/dist/esm/index.js"() {
14957
15032
  init_esm3();
@@ -15020,11 +15095,11 @@ var init_esm4 = __esm({
15020
15095
  return (f) => f.length === len && f !== "." && f !== "..";
15021
15096
  };
15022
15097
  defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
15023
- path5 = {
15098
+ path7 = {
15024
15099
  win32: { sep: "\\" },
15025
15100
  posix: { sep: "/" }
15026
15101
  };
15027
- sep = defaultPlatform === "win32" ? path5.win32.sep : path5.posix.sep;
15102
+ sep = defaultPlatform === "win32" ? path7.win32.sep : path7.posix.sep;
15028
15103
  minimatch.sep = sep;
15029
15104
  GLOBSTAR = Symbol("globstar **");
15030
15105
  minimatch.GLOBSTAR = GLOBSTAR;
@@ -15151,7 +15226,7 @@ var init_esm4 = __esm({
15151
15226
  }
15152
15227
  return false;
15153
15228
  }
15154
- debug(..._2) {
15229
+ debug(..._) {
15155
15230
  }
15156
15231
  make() {
15157
15232
  const pattern = this.pattern;
@@ -15173,7 +15248,7 @@ var init_esm4 = __esm({
15173
15248
  const rawGlobParts = this.globSet.map((s) => this.slashSplit(s));
15174
15249
  this.globParts = this.preprocess(rawGlobParts);
15175
15250
  this.debug(this.pattern, this.globParts);
15176
- let set2 = this.globParts.map((s, _2, __) => {
15251
+ let set2 = this.globParts.map((s, _, __) => {
15177
15252
  if (this.isWindows && this.windowsNoMagicRoot) {
15178
15253
  const isUNC = s[0] === "" && s[1] === "" && (s[2] === "?" || !globMagic.test(s[2])) && !globMagic.test(s[3]);
15179
15254
  const isDrive = /^[a-z]:/i.test(s[0]);
@@ -17070,7 +17145,7 @@ var init_database_detector = __esm({
17070
17145
  });
17071
17146
 
17072
17147
  // packages/analyzer/dist/module-extractor.js
17073
- import path6 from "path";
17148
+ import path8 from "path";
17074
17149
  function extractModulesAndMethods(analyses, layerDetails, fileDependencies) {
17075
17150
  const modules = [];
17076
17151
  const methods = [];
@@ -17217,7 +17292,7 @@ function deriveModuleName(analysis) {
17217
17292
  return localExports[0].name;
17218
17293
  }
17219
17294
  const baseName = fileBaseName(analysis.filePath);
17220
- const strippedName = stripExtension(path6.basename(analysis.filePath));
17295
+ const strippedName = stripExtension(path8.basename(analysis.filePath));
17221
17296
  if (INDEX_NAMES.has(strippedName)) {
17222
17297
  return baseName;
17223
17298
  }
@@ -17234,10 +17309,10 @@ function deriveModuleName(analysis) {
17234
17309
  return baseName;
17235
17310
  }
17236
17311
  function deriveNextjsRouteName(filePath) {
17237
- const base = path6.basename(filePath).replace(/\.(ts|tsx|js|jsx)$/, "");
17312
+ const base = path8.basename(filePath).replace(/\.(ts|tsx|js|jsx)$/, "");
17238
17313
  if (base !== "route" && base !== "page")
17239
17314
  return null;
17240
- const parts = filePath.split(path6.sep);
17315
+ const parts = filePath.split(path8.sep);
17241
17316
  const appIdx = parts.lastIndexOf("app");
17242
17317
  if (appIdx === -1)
17243
17318
  return null;
@@ -17255,9 +17330,9 @@ function stripExtension(filename) {
17255
17330
  return filename;
17256
17331
  }
17257
17332
  function fileBaseName(filePath) {
17258
- const name = stripExtension(path6.basename(filePath));
17333
+ const name = stripExtension(path8.basename(filePath));
17259
17334
  if (INDEX_NAMES.has(name)) {
17260
- return path6.basename(path6.dirname(filePath));
17335
+ return path8.basename(path8.dirname(filePath));
17261
17336
  }
17262
17337
  return name;
17263
17338
  }
@@ -44020,10 +44095,10 @@ var init_secret_rules = __esm({
44020
44095
  });
44021
44096
 
44022
44097
  // packages/analyzer/dist/rules/security/secret-scanner.js
44023
- import path7 from "node:path";
44098
+ import path9 from "node:path";
44024
44099
  function isSensitiveFile(filePath) {
44025
- const basename2 = path7.basename(filePath);
44026
- const ext2 = path7.extname(filePath);
44100
+ const basename2 = path9.basename(filePath);
44101
+ const ext2 = path9.extname(filePath);
44027
44102
  if (basename2.startsWith(".env")) {
44028
44103
  const envVariant = basename2;
44029
44104
  if (SENSITIVE_FILE_EXTENSIONS.has(envVariant)) {
@@ -86706,7 +86781,7 @@ var init_js_naming_convention = __esm({
86706
86781
  return null;
86707
86782
  }
86708
86783
  if (funcName.includes("_") && !funcName.startsWith("_")) {
86709
- 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())}.`);
86710
86785
  }
86711
86786
  return null;
86712
86787
  }
@@ -93624,7 +93699,7 @@ var init_dist6 = __esm({
93624
93699
  });
93625
93700
 
93626
93701
  // apps/server/dist/services/analyzer.service.js
93627
- import path8 from "node:path";
93702
+ import path10 from "node:path";
93628
93703
  function runDeterministicModuleChecks(result, enabledDeterministic) {
93629
93704
  if (!result.modules || !result.methods)
93630
93705
  return [];
@@ -93667,7 +93742,7 @@ async function runAnalysis(repoPath, _branch, onProgress, options) {
93667
93742
  const statusResult = await git.status();
93668
93743
  hasChanges = !statusResult.isClean();
93669
93744
  const gitRoot = (await git.revparse(["--show-toplevel"])).trim();
93670
- isSubdirectory = path8.resolve(repoPath) !== path8.resolve(gitRoot);
93745
+ isSubdirectory = path10.resolve(repoPath) !== path10.resolve(gitRoot);
93671
93746
  }
93672
93747
  if (hasChanges && !options?.skipStash && !isSubdirectory && git) {
93673
93748
  onProgress({ step: "stash", percent: 2, detail: "Stashing pending changes to analyze committed state..." });
@@ -94001,155 +94076,210 @@ var init_analysis_persistence_service = __esm({
94001
94076
  }
94002
94077
  });
94003
94078
 
94004
- // apps/server/dist/lib/atomic-write.js
94005
- import fs6 from "node:fs";
94006
- import path9 from "node:path";
94007
- function atomicWriteJson(targetPath, data) {
94008
- fs6.mkdirSync(path9.dirname(targetPath), { recursive: true });
94009
- const tmp = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
94010
- fs6.writeFileSync(tmp, JSON.stringify(data, null, 2));
94011
- fs6.renameSync(tmp, targetPath);
94012
- }
94013
- function lockPath(repoPath) {
94014
- return path9.join(repoPath, ".truecourse", LOCK_FILENAME);
94015
- }
94016
- function acquireAnalyzeLock(repoPath) {
94017
- const file = lockPath(repoPath);
94018
- fs6.mkdirSync(path9.dirname(file), { recursive: true });
94019
- try {
94020
- const fd = fs6.openSync(file, "wx");
94021
- fs6.writeSync(fd, `${process.pid}
94022
- ${(/* @__PURE__ */ new Date()).toISOString()}
94023
- `);
94024
- fs6.closeSync(fd);
94025
- } catch (err) {
94026
- if (err.code === "EEXIST") {
94027
- let owner = null;
94028
- try {
94029
- owner = parseInt(fs6.readFileSync(file, "utf-8").split("\n")[0], 10) || null;
94030
- } 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 });
94031
94092
  }
94032
- 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);
94033
94100
  }
94034
- throw err;
94035
94101
  }
94036
- }
94037
- function releaseAnalyzeLock(repoPath) {
94038
- try {
94039
- fs6.unlinkSync(lockPath(repoPath));
94040
- } catch (err) {
94041
- if (err.code !== "ENOENT")
94042
- 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}`);
94043
94106
  }
94044
- }
94045
- var LOCK_FILENAME, AnalyzeLockError;
94046
- var init_atomic_write = __esm({
94047
- "apps/server/dist/lib/atomic-write.js"() {
94048
- "use strict";
94049
- LOCK_FILENAME = ".analyze.lock";
94050
- AnalyzeLockError = class extends Error {
94051
- constructor(repoPath, ownerPid) {
94052
- const who = ownerPid != null ? ` (held by pid ${ownerPid})` : "";
94053
- super(`Another analyze is already running for ${repoPath}${who}. If you're sure no analyze is in progress, remove ${lockPath(repoPath)} and retry.`);
94054
- 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
+ }
94055
94130
  }
94056
- };
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
+ }
94057
94142
  }
94058
- });
94059
-
94060
- // apps/server/dist/lib/analysis-store.js
94061
- import fs7 from "node:fs";
94062
- import path10 from "node:path";
94063
- function storeDir(repoPath) {
94064
- return path10.join(repoPath, TRUECOURSE_DIR2);
94065
- }
94066
- function analysesDir(repoPath) {
94067
- return path10.join(storeDir(repoPath), ANALYSES_DIR);
94068
- }
94069
- function analysisFilePath(repoPath, filename) {
94070
- return path10.join(analysesDir(repoPath), filename);
94071
- }
94072
- function latestPath(repoPath) {
94073
- return path10.join(storeDir(repoPath), LATEST_FILE);
94074
- }
94075
- function historyPath(repoPath) {
94076
- return path10.join(storeDir(repoPath), HISTORY_FILE);
94077
- }
94078
- function diffPath(repoPath) {
94079
- return path10.join(storeDir(repoPath), DIFF_FILE);
94080
- }
94081
- function buildAnalysisFilename(analysisId, createdAt) {
94082
- const iso = createdAt.replace(/[:.]/g, "-").replace(/-\d{3}Z$/, "Z");
94083
- const shortId = analysisId.replace(/-/g, "").slice(0, 8);
94084
- 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;
94085
94183
  }
94086
- function readLatest(repoPath) {
94087
- const file = latestPath(repoPath);
94088
- let mtime;
94089
- try {
94090
- mtime = fs7.statSync(file).mtimeMs;
94091
- } catch (err) {
94092
- if (err.code === "ENOENT") {
94093
- latestCache.delete(repoPath);
94094
- 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);
94095
94198
  }
94096
- throw err;
94097
94199
  }
94098
- const cached = latestCache.get(repoPath);
94099
- if (cached && cached.mtime === mtime)
94100
- return cached.data;
94101
- const data = JSON.parse(fs7.readFileSync(file, "utf-8"));
94102
- latestCache.set(repoPath, { mtime, data });
94103
- return data;
94104
- }
94105
- function writeLatest(repoPath, latest) {
94106
- atomicWriteJson(latestPath(repoPath), latest);
94107
- latestCache.delete(repoPath);
94108
- }
94109
- function writeAnalysis(repoPath, snapshot) {
94110
- const filename = buildAnalysisFilename(snapshot.id, snapshot.createdAt);
94111
- atomicWriteJson(analysisFilePath(repoPath, filename), snapshot);
94112
- return { filename, snapshot };
94113
- }
94114
- function readHistory(repoPath) {
94115
- const file = historyPath(repoPath);
94116
- if (!fs7.existsSync(file))
94117
- return { analyses: [] };
94118
- return JSON.parse(fs7.readFileSync(file, "utf-8"));
94119
- }
94120
- function appendHistory(repoPath, entry) {
94121
- const history = readHistory(repoPath);
94122
- history.analyses.push(entry);
94123
- atomicWriteJson(historyPath(repoPath), history);
94124
- }
94125
- function readDiff(repoPath) {
94126
- const file = diffPath(repoPath);
94127
- if (!fs7.existsSync(file))
94128
- return null;
94129
- return JSON.parse(fs7.readFileSync(file, "utf-8"));
94130
- }
94131
- function writeDiff(repoPath, diff) {
94132
- 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;
94133
94235
  }
94134
- function deleteDiff(repoPath) {
94135
- try {
94136
- fs7.unlinkSync(diffPath(repoPath));
94137
- } catch (err) {
94138
- if (err.code !== "ENOENT")
94139
- 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
+ }
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
+ }
94140
94268
  }
94269
+ return fileToModuleName.get(routeFile.filePath) || handlerName;
94141
94270
  }
94142
- var TRUECOURSE_DIR2, ANALYSES_DIR, LATEST_FILE, HISTORY_FILE, DIFF_FILE, latestCache;
94143
- var init_analysis_store = __esm({
94144
- "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"() {
94145
94279
  "use strict";
94146
- init_atomic_write();
94147
- TRUECOURSE_DIR2 = ".truecourse";
94148
- ANALYSES_DIR = "analyses";
94149
- LATEST_FILE = "LATEST.json";
94150
- HISTORY_FILE = "history.json";
94151
- DIFF_FILE = "diff.json";
94152
- latestCache = /* @__PURE__ */ new Map();
94280
+ init_logger();
94281
+ init_dist6();
94282
+ init_analysis_store();
94153
94283
  }
94154
94284
  });
94155
94285
 
@@ -107145,7 +107275,7 @@ var require_sender = __commonJS({
107145
107275
  const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
107146
107276
  this._bufferedBytes += options[kByteLength];
107147
107277
  this._state = DEFLATING;
107148
- perMessageDeflate.compress(data, options.fin, (_2, buf) => {
107278
+ perMessageDeflate.compress(data, options.fin, (_, buf) => {
107149
107279
  if (this._socket.destroyed) {
107150
107280
  const err = new Error(
107151
107281
  "The socket was closed while data was being compressed"
@@ -115969,7 +116099,7 @@ var util, objectUtil, ZodParsedType, getParsedType;
115969
116099
  var init_util3 = __esm({
115970
116100
  "node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js"() {
115971
116101
  (function(util2) {
115972
- util2.assertEqual = (_2) => {
116102
+ util2.assertEqual = (_) => {
115973
116103
  };
115974
116104
  function assertIs(_arg) {
115975
116105
  }
@@ -116019,7 +116149,7 @@ var init_util3 = __esm({
116019
116149
  return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator);
116020
116150
  }
116021
116151
  util2.joinValues = joinValues;
116022
- util2.jsonStringifyReplacer = (_2, value) => {
116152
+ util2.jsonStringifyReplacer = (_, value) => {
116023
116153
  if (typeof value === "bigint") {
116024
116154
  return value.toString();
116025
116155
  }
@@ -120632,9 +120762,12 @@ var init_schemas = __esm({
120632
120762
  path: external_exports.string().min(1)
120633
120763
  });
120634
120764
  AnalyzeRepoSchema = external_exports.object({
120635
- branch: external_exports.string().optional(),
120636
- enabledCategories: external_exports.array(external_exports.string()).optional().default([]),
120637
- 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. */
120638
120771
  skipGit: external_exports.boolean().optional().default(false)
120639
120772
  });
120640
120773
  GenerateViolationsSchema = external_exports.object({
@@ -121328,7 +121461,7 @@ function parseStringDef(def, refs) {
121328
121461
  case "trim":
121329
121462
  break;
121330
121463
  default:
121331
- /* @__PURE__ */ ((_2) => {
121464
+ /* @__PURE__ */ ((_) => {
121332
121465
  })(check);
121333
121466
  }
121334
121467
  }
@@ -122176,7 +122309,7 @@ var init_selectParser = __esm({
122176
122309
  case ZodFirstPartyTypeKind.ZodSymbol:
122177
122310
  return void 0;
122178
122311
  default:
122179
- return /* @__PURE__ */ ((_2) => void 0)(typeName);
122312
+ return /* @__PURE__ */ ((_) => void 0)(typeName);
122180
122313
  }
122181
122314
  };
122182
122315
  }
@@ -125880,422 +126013,6 @@ var init_violation_pipeline_service = __esm({
125880
126013
  }
125881
126014
  });
125882
126015
 
125883
- // apps/server/dist/commands/diff-in-process.js
125884
- var diff_in_process_exports = {};
125885
- __export(diff_in_process_exports, {
125886
- diffInProcess: () => diffInProcess
125887
- });
125888
- import path14 from "node:path";
125889
- import { randomUUID as randomUUID7 } from "node:crypto";
125890
- async function diffInProcess(project, options = {}) {
125891
- const latest = readLatest(project.path);
125892
- if (!latest) {
125893
- throw new Error("Run a full analysis first before checking a diff.");
125894
- }
125895
- const projectConfig = readProjectConfig(project.path);
125896
- const enabledCategories = options.enabledCategoriesOverride ?? projectConfig.enabledCategories ?? void 0;
125897
- const enableLlmRules = options.enableLlmRulesOverride ?? projectConfig.enableLlmRules ?? false;
125898
- const git = await getGit(project.path);
125899
- const statusResult = await git.status();
125900
- const changedFiles = [];
125901
- for (const f of statusResult.not_added)
125902
- changedFiles.push({ path: f, status: "new" });
125903
- for (const f of statusResult.created)
125904
- changedFiles.push({ path: f, status: "new" });
125905
- for (const f of statusResult.modified)
125906
- changedFiles.push({ path: f, status: "modified" });
125907
- for (const f of statusResult.staged) {
125908
- if (!changedFiles.some((cf) => cf.path === f)) {
125909
- changedFiles.push({ path: f, status: "modified" });
125910
- }
125911
- }
125912
- for (const f of statusResult.deleted)
125913
- changedFiles.push({ path: f, status: "deleted" });
125914
- const commitHash = (await git.revparse(["HEAD"])).trim() || null;
125915
- options.tracker?.start("parse", "Analyzing working tree...");
125916
- const result = await runAnalysis(project.path, latest.analysis.branch ?? void 0, (progress) => {
125917
- options.tracker?.detail("parse", progress.detail ?? "Analyzing...");
125918
- options.onProgress?.({ detail: progress.detail });
125919
- }, { signal: options.signal, skipStash: true });
125920
- if (options.signal?.aborted)
125921
- throw new DOMException("Diff cancelled", "AbortError");
125922
- const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
125923
- const analysisId = randomUUID7();
125924
- const now = (/* @__PURE__ */ new Date()).toISOString();
125925
- options.tracker?.done("parse", `${result.services.length} services, ${result.fileAnalyses?.length ?? 0} files`);
125926
- const pipelineResult = await runViolationPipeline({
125927
- repoPath: project.path,
125928
- analysisId,
125929
- now,
125930
- result,
125931
- serviceIdMap,
125932
- moduleIdMap,
125933
- methodIdMap,
125934
- dbIdMap,
125935
- previousActiveViolations: latest.violations,
125936
- enabledCategories,
125937
- enableLlmRules,
125938
- tracker: options.tracker,
125939
- signal: options.signal
125940
- });
125941
- const diff = buildDiffSnapshot({
125942
- latest,
125943
- graph,
125944
- analysisId,
125945
- now,
125946
- branch: latest.analysis.branch,
125947
- commitHash,
125948
- changedFiles,
125949
- pipelineResult,
125950
- repoPath: project.path
125951
- });
125952
- writeDiff(project.path, diff);
125953
- log.info(`[Diff] Done \u2014 ${diff.summary.newCount} new, ${diff.summary.unchangedCount} unchanged, ${diff.summary.resolvedCount} resolved across ${diff.changedFiles.length} changed files`);
125954
- return { diff, isStale: false };
125955
- }
125956
- function buildDiffSnapshot(params) {
125957
- const { latest, graph, analysisId, branch, commitHash, changedFiles, pipelineResult, repoPath } = params;
125958
- const serviceById = new Map(graph.services.map((s) => [s.id, s.name]));
125959
- const moduleById = new Map(graph.modules.map((m) => [m.id, m.name]));
125960
- const methodById = new Map(graph.methods.map((m) => [m.id, m.name]));
125961
- const databaseById = new Map(graph.databases.map((d3) => [d3.id, d3.name]));
125962
- const denormalize = (v) => ({
125963
- ...v,
125964
- targetServiceName: v.targetServiceId ? serviceById.get(v.targetServiceId) ?? null : null,
125965
- targetModuleName: v.targetModuleId ? moduleById.get(v.targetModuleId) ?? null : null,
125966
- targetMethodName: v.targetMethodId ? methodById.get(v.targetMethodId) ?? null : null,
125967
- targetDatabaseName: v.targetDatabaseId ? databaseById.get(v.targetDatabaseId) ?? null : null
125968
- });
125969
- const newViolations = pipelineResult.added.map(denormalize);
125970
- const latestById = new Map(latest.violations.map((v) => [v.id, v]));
125971
- const resolvedViolations = pipelineResult.resolvedRefs.map((r) => latestById.get(r.id)).filter((v) => !!v);
125972
- const changedAbs = new Set(changedFiles.map((c2) => path14.resolve(repoPath, c2.path)));
125973
- const matchesChanged = (p2) => !!p2 && (changedAbs.has(p2) || changedAbs.has(path14.resolve(repoPath, p2)));
125974
- const affectedModules = graph.modules.filter((m) => matchesChanged(m.filePath));
125975
- const affectedModuleIdSet = new Set(affectedModules.map((m) => m.id));
125976
- const serviceNameById = new Map(graph.services.map((s) => [s.id, s.name]));
125977
- const layerKeyById = new Map(graph.layers.map((l) => [l.id, `${l.serviceName}::${l.layer}`]));
125978
- const affectedServices = /* @__PURE__ */ new Set();
125979
- const affectedLayers = /* @__PURE__ */ new Set();
125980
- const affectedModuleKeys = /* @__PURE__ */ new Set();
125981
- for (const mod of affectedModules) {
125982
- const svcName = serviceNameById.get(mod.serviceId);
125983
- if (svcName) {
125984
- affectedServices.add(svcName);
125985
- affectedModuleKeys.add(`${svcName}::${mod.name}`);
125986
- }
125987
- const layerKey = layerKeyById.get(mod.layerId);
125988
- if (layerKey)
125989
- affectedLayers.add(layerKey);
125990
- }
125991
- const moduleNameById = new Map(graph.modules.map((m) => [m.id, m.name]));
125992
- const affectedMethodKeys = [];
125993
- for (const method of graph.methods) {
125994
- if (!affectedModuleIdSet.has(method.moduleId))
125995
- continue;
125996
- const modName = moduleNameById.get(method.moduleId);
125997
- const mod = graph.modules.find((m) => m.id === method.moduleId);
125998
- const svcName = mod ? serviceNameById.get(mod.serviceId) : void 0;
125999
- if (svcName && modName)
126000
- affectedMethodKeys.push(`${svcName}::${modName}::${method.name}`);
126001
- }
126002
- return {
126003
- id: analysisId,
126004
- baseAnalysisId: latest.analysis.id,
126005
- createdAt: params.now,
126006
- branch,
126007
- commitHash,
126008
- graph,
126009
- changedFiles,
126010
- newViolations,
126011
- resolvedViolations,
126012
- affectedNodeIds: {
126013
- services: [...affectedServices],
126014
- layers: [...affectedLayers],
126015
- modules: [...affectedModuleKeys],
126016
- methods: affectedMethodKeys
126017
- },
126018
- summary: {
126019
- newCount: newViolations.length,
126020
- unchangedCount: pipelineResult.unchanged.length,
126021
- resolvedCount: resolvedViolations.length
126022
- }
126023
- };
126024
- }
126025
- var init_diff_in_process = __esm({
126026
- "apps/server/dist/commands/diff-in-process.js"() {
126027
- "use strict";
126028
- init_git();
126029
- init_logger();
126030
- init_project_config();
126031
- init_analyzer_service();
126032
- init_analysis_persistence_service();
126033
- init_violation_pipeline_service();
126034
- init_analysis_store();
126035
- }
126036
- });
126037
-
126038
- // node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
126039
- var import_index = __toESM(require_commander(), 1);
126040
- var {
126041
- program,
126042
- createCommand,
126043
- createArgument,
126044
- createOption,
126045
- CommanderError,
126046
- InvalidArgumentError,
126047
- InvalidOptionArgumentError,
126048
- // deprecated old name
126049
- Command,
126050
- Argument,
126051
- Option,
126052
- Help
126053
- } = import_index.default;
126054
-
126055
- // tools/cli/src/index.ts
126056
- init_dist4();
126057
-
126058
- // tools/cli/src/commands/add.ts
126059
- init_dist4();
126060
- init_paths();
126061
- init_registry();
126062
- init_helpers();
126063
- async function runAdd() {
126064
- const repoPath = resolveRepoDir(process.cwd()) ?? process.cwd();
126065
- mt("Adding repository to TrueCourse");
126066
- O2.step(repoPath);
126067
- ensureRepoTruecourseDir(repoPath);
126068
- const existing = getProjectByPath(repoPath);
126069
- const entry = registerProject(repoPath);
126070
- if (existing) {
126071
- O2.info(`Repository "${entry.name}" is already registered.`);
126072
- } else {
126073
- O2.success(`Repository "${entry.name}" added.`);
126074
- }
126075
- await promptInstallSkills(repoPath);
126076
- gt("Run `truecourse analyze` to generate analysis data.");
126077
- }
126078
-
126079
- // tools/cli/src/commands/analyze.ts
126080
- init_dist4();
126081
- import { execSync } from "node:child_process";
126082
- import path15 from "node:path";
126083
-
126084
- // apps/server/dist/commands/analyze-in-process.js
126085
- init_logger();
126086
- init_git();
126087
- init_project_config();
126088
- init_registry();
126089
- init_analyzer_service();
126090
- init_analysis_persistence_service();
126091
- import { randomUUID as randomUUID6 } from "node:crypto";
126092
-
126093
- // apps/server/dist/services/flow.service.js
126094
- init_logger();
126095
- init_dist6();
126096
- init_analysis_store();
126097
- import { randomUUID as randomUUID2 } from "node:crypto";
126098
- function detectFlows(result) {
126099
- const dbTypeMap = /* @__PURE__ */ new Map();
126100
- for (const db of result.databaseResult.databases) {
126101
- dbTypeMap.set(db.name, db.type);
126102
- }
126103
- const functionsByFile = /* @__PURE__ */ new Map();
126104
- if (result.fileAnalyses) {
126105
- for (const fa of result.fileAnalyses) {
126106
- const entries = [];
126107
- for (const fn of fa.functions) {
126108
- entries.push({ name: fn.name, startLine: fn.location.startLine, endLine: fn.location.endLine });
126109
- }
126110
- for (const cls of fa.classes) {
126111
- for (const method of cls.methods) {
126112
- entries.push({ name: method.name, startLine: method.location.startLine, endLine: method.location.endLine });
126113
- }
126114
- }
126115
- if (entries.length > 0)
126116
- functionsByFile.set(fa.filePath, entries);
126117
- }
126118
- }
126119
- const crossServiceCalls = [];
126120
- const fileToModule = /* @__PURE__ */ new Map();
126121
- for (const mod of result.modules) {
126122
- fileToModule.set(mod.filePath, `${mod.serviceName}::${mod.name}`);
126123
- }
126124
- const fileToLanguage = /* @__PURE__ */ new Map();
126125
- if (result.fileAnalyses) {
126126
- for (const fa of result.fileAnalyses) {
126127
- fileToLanguage.set(fa.filePath, fa.language);
126128
- }
126129
- }
126130
- for (const dep of result.dependencies) {
126131
- if (!dep.httpCalls || dep.httpCalls.length === 0)
126132
- continue;
126133
- for (const call of dep.httpCalls) {
126134
- const moduleKey = fileToModule.get(call.location.filePath);
126135
- if (!moduleKey)
126136
- continue;
126137
- const [sourceService, sourceModule] = moduleKey.split("::");
126138
- let sourceMethod;
126139
- const fileFunctions = functionsByFile.get(call.location.filePath);
126140
- if (fileFunctions) {
126141
- for (const fn of fileFunctions) {
126142
- if (call.location.startLine >= fn.startLine && call.location.startLine <= fn.endLine) {
126143
- sourceMethod = fn.name;
126144
- break;
126145
- }
126146
- }
126147
- }
126148
- const language = fileToLanguage.get(call.location.filePath);
126149
- const normalizedUrl = language ? normalizeUrl(call.url, language) : call.url;
126150
- crossServiceCalls.push({
126151
- sourceService,
126152
- sourceModule,
126153
- sourceMethod,
126154
- httpMethod: call.method,
126155
- url: normalizedUrl,
126156
- targetService: dep.target
126157
- });
126158
- }
126159
- }
126160
- const routeHandlers = buildRouteHandlerLookup(result);
126161
- const graph = new AnalysisGraph({
126162
- methods: result.methods,
126163
- methodDependencies: result.methodLevelDependencies,
126164
- modules: result.modules,
126165
- services: result.services.map((s) => ({ name: s.name, type: s.type })),
126166
- crossServiceCalls: crossServiceCalls.length > 0 ? crossServiceCalls : void 0,
126167
- databaseConnections: result.databaseResult.connections.map((c2) => ({
126168
- serviceName: c2.serviceName,
126169
- databaseName: c2.databaseName,
126170
- databaseType: dbTypeMap.get(c2.databaseName) || "unknown"
126171
- })),
126172
- routeHandlers: routeHandlers.size > 0 ? routeHandlers : void 0
126173
- });
126174
- const traced = traceFlows(graph);
126175
- const out = traced.map((flow) => ({
126176
- id: randomUUID2(),
126177
- name: flow.name,
126178
- description: null,
126179
- entryService: flow.entryService,
126180
- entryMethod: flow.entryMethod,
126181
- category: flow.category,
126182
- trigger: flow.trigger,
126183
- stepCount: flow.steps.length,
126184
- steps: flow.steps.map((step) => ({
126185
- stepOrder: step.stepOrder,
126186
- sourceService: step.sourceService,
126187
- sourceModule: step.sourceModule,
126188
- sourceMethod: step.sourceMethod,
126189
- targetService: step.targetService,
126190
- targetModule: step.targetModule,
126191
- targetMethod: step.targetMethod,
126192
- stepType: step.stepType,
126193
- dataDescription: null,
126194
- isAsync: step.isAsync,
126195
- isConditional: step.isConditional
126196
- }))
126197
- }));
126198
- log.info(`[Flows] Detected ${out.length} flows`);
126199
- return out;
126200
- }
126201
- function buildRouteHandlerLookup(result) {
126202
- const handlers = /* @__PURE__ */ new Map();
126203
- if (!result.fileAnalyses)
126204
- return handlers;
126205
- const fileToService = /* @__PURE__ */ new Map();
126206
- const fileToModuleName = /* @__PURE__ */ new Map();
126207
- for (const mod of result.modules) {
126208
- fileToService.set(mod.filePath, mod.serviceName);
126209
- fileToModuleName.set(mod.filePath, mod.name);
126210
- }
126211
- for (const ld of result.layerDetails) {
126212
- for (const fp of ld.filePaths) {
126213
- if (!fileToService.has(fp))
126214
- fileToService.set(fp, ld.serviceName);
126215
- }
126216
- }
126217
- const fileMountPrefix = /* @__PURE__ */ new Map();
126218
- for (const fa of result.fileAnalyses) {
126219
- if (!fa.routerMounts || fa.routerMounts.length === 0)
126220
- continue;
126221
- for (const mount of fa.routerMounts) {
126222
- for (const imp of fa.imports) {
126223
- const spec = imp.specifiers.find((s) => s.name === mount.routerName || s.alias === mount.routerName);
126224
- if (spec) {
126225
- for (const dep of result.moduleDependencies) {
126226
- if (dep.source === fa.filePath && dep.importedNames.includes(spec.name)) {
126227
- fileMountPrefix.set(dep.target, mount.path);
126228
- }
126229
- }
126230
- }
126231
- }
126232
- const hasLocal = fa.functions.some((f) => f.name === mount.routerName) || fa.exports.some((e) => e.name === mount.routerName);
126233
- if (hasLocal)
126234
- fileMountPrefix.set(fa.filePath, mount.path);
126235
- }
126236
- }
126237
- for (const fa of result.fileAnalyses) {
126238
- if (!fa.routeRegistrations || fa.routeRegistrations.length === 0)
126239
- continue;
126240
- const serviceName = fileToService.get(fa.filePath);
126241
- if (!serviceName)
126242
- continue;
126243
- const mountPrefix = fileMountPrefix.get(fa.filePath) || "";
126244
- for (const route of fa.routeRegistrations) {
126245
- const fullPath = composePath(mountPrefix, route.path);
126246
- const moduleName = resolveHandlerModule(route.handlerName, fa, result.fileAnalyses, fileToModuleName);
126247
- const key = `${serviceName}::${route.httpMethod}::${fullPath}`;
126248
- handlers.set(key, { handlerName: route.handlerName, moduleName });
126249
- }
126250
- }
126251
- return handlers;
126252
- }
126253
- function resolveHandlerModule(handlerName, routeFile, allFiles, fileToModuleName) {
126254
- for (const cls of routeFile.classes) {
126255
- for (const method of cls.methods) {
126256
- if (method.name === handlerName)
126257
- return fileToModuleName.get(routeFile.filePath) || handlerName;
126258
- }
126259
- }
126260
- for (const fn of routeFile.functions) {
126261
- if (fn.name === handlerName)
126262
- return fileToModuleName.get(routeFile.filePath) || handlerName;
126263
- }
126264
- for (const imp of routeFile.imports) {
126265
- const spec = imp.specifiers.find((s) => s.name === handlerName || s.alias === handlerName);
126266
- if (spec) {
126267
- for (const targetFile of allFiles) {
126268
- const hasExport = targetFile.exports.some((e) => e.name === (spec.alias || spec.name) || e.name === spec.name);
126269
- const hasFunction = targetFile.functions.some((f) => f.name === spec.name);
126270
- const hasClassMethod = targetFile.classes.some((c2) => c2.methods.some((m) => m.name === spec.name));
126271
- if (hasExport || hasFunction || hasClassMethod) {
126272
- return fileToModuleName.get(targetFile.filePath) || handlerName;
126273
- }
126274
- }
126275
- }
126276
- for (const s of imp.specifiers) {
126277
- for (const targetFile of allFiles) {
126278
- for (const cls of targetFile.classes) {
126279
- if (cls.name === s.name && cls.methods.some((m) => m.name === handlerName)) {
126280
- return fileToModuleName.get(targetFile.filePath) || cls.name;
126281
- }
126282
- }
126283
- }
126284
- }
126285
- }
126286
- return fileToModuleName.get(routeFile.filePath) || handlerName;
126287
- }
126288
- function composePath(prefix, routePath) {
126289
- const p2 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
126290
- const r = routePath.startsWith("/") ? routePath : `/${routePath}`;
126291
- const full = `${p2}${r}`;
126292
- return full.length > 1 && full.endsWith("/") ? full.slice(0, -1) : full || "/";
126293
- }
126294
-
126295
- // apps/server/dist/commands/analyze-in-process.js
126296
- init_violation_pipeline_service();
126297
- init_provider();
126298
-
126299
126016
  // apps/server/dist/services/usage.service.js
126300
126017
  function toUsageRecords(records) {
126301
126018
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -126312,11 +126029,15 @@ function toUsageRecords(records) {
126312
126029
  createdAt: now
126313
126030
  }));
126314
126031
  }
126032
+ var init_usage_service = __esm({
126033
+ "apps/server/dist/services/usage.service.js"() {
126034
+ "use strict";
126035
+ }
126036
+ });
126315
126037
 
126316
- // apps/server/dist/commands/analyze-in-process.js
126317
- init_analysis_store();
126318
- init_atomic_write();
126319
- 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) {
126320
126041
  try {
126321
126042
  acquireAnalyzeLock(project.path);
126322
126043
  } catch (err) {
@@ -126325,12 +126046,27 @@ async function analyzeInProcess(project, options = {}) {
126325
126046
  throw err;
126326
126047
  }
126327
126048
  try {
126328
- const start = Date.now();
126329
- const { skipGit, signal } = options;
126049
+ const { mode, signal } = options;
126050
+ const isDiff = mode === "diff";
126051
+ const skipGit = !isDiff && !!options.skipGit;
126330
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
+ }
126331
126057
  let branch = options.branch ?? null;
126332
126058
  let commitHash = options.commitHash ?? null;
126333
- 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)) {
126334
126070
  const git = await getGit(project.path);
126335
126071
  if (branch === null)
126336
126072
  branch = (await git.branch()).current || null;
@@ -126339,39 +126075,62 @@ async function analyzeInProcess(project, options = {}) {
126339
126075
  }
126340
126076
  const analysisId = randomUUID6();
126341
126077
  const now = (/* @__PURE__ */ new Date()).toISOString();
126342
- const filename = buildAnalysisFilename(analysisId, now);
126078
+ const start = Date.now();
126343
126079
  const effectiveCategories = options.enabledCategoriesOverride?.length ? options.enabledCategoriesOverride : projectConfig.enabledCategories ?? void 0;
126344
126080
  const effectiveLlmRules = projectConfig.enableLlmRules ?? options.enableLlmRulesOverride ?? true;
126345
- options.tracker?.start("parse", "Starting analysis...");
126081
+ options.tracker?.start("parse", isDiff ? "Analyzing working tree..." : "Starting analysis...");
126346
126082
  const result = await runAnalysis(project.path, branch ?? void 0, (progress) => {
126347
126083
  options.tracker?.detail("parse", progress.detail ?? "Analyzing...");
126348
126084
  options.onProgress?.({ detail: progress.detail });
126349
- }, { signal });
126350
- if (signal?.aborted)
126351
- throw new DOMException("Analysis cancelled", "AbortError");
126352
- const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
126353
- const previousLatest = readLatest(project.path);
126354
- const previousAnalysisId = previousLatest?.analysis.id ?? null;
126355
- const previousActiveViolations = previousLatest ? previousLatest.violations.filter((v) => branch == null || previousLatest.analysis.branch == null || previousLatest.analysis.branch === branch) : [];
126356
- try {
126357
- graph.flows = detectFlows(result);
126358
- } catch (flowError) {
126359
- log.error(`[Flows] Detection failed: ${flowError instanceof Error ? flowError.message : String(flowError)}`);
126360
- graph.flows = [];
126085
+ }, { signal, skipStash: isDiff });
126086
+ if (signal?.aborted) {
126087
+ throw new DOMException(isDiff ? "Diff cancelled" : "Analysis cancelled", "AbortError");
126361
126088
  }
126362
- touchProject(project.slug);
126089
+ const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
126090
+ let changedFiles = [];
126363
126091
  let changedFileSet;
126364
- if (previousLatest?.analysis.commitHash && !skipGit) {
126092
+ if (isDiff) {
126093
+ try {
126094
+ const git = await getGit(project.path);
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) {
126365
126113
  try {
126366
126114
  const git = await getGit(project.path);
126367
- const diffOutput = await git.diff([previousLatest.analysis.commitHash, "HEAD", "--name-only"]);
126115
+ const diffOutput = await git.diff([latestBaseline.analysis.commitHash, "HEAD", "--name-only"]);
126368
126116
  const files = diffOutput.trim().split("\n").filter(Boolean);
126369
126117
  if (files.length > 0)
126370
126118
  changedFileSet = new Set(files);
126371
126119
  } catch {
126372
126120
  }
126373
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
+ }
126374
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;
126375
126134
  const provider = options.provider ?? (effectiveLlmRules ? createLLMProvider() : void 0);
126376
126135
  if (provider) {
126377
126136
  provider.setAnalysisId(analysisId);
@@ -126379,32 +126138,28 @@ async function analyzeInProcess(project, options = {}) {
126379
126138
  if (signal)
126380
126139
  provider.setAbortSignal(signal);
126381
126140
  }
126382
- let pipelineResult;
126383
- try {
126384
- pipelineResult = await runViolationPipeline({
126385
- repoPath: project.path,
126386
- analysisId,
126387
- now,
126388
- result,
126389
- serviceIdMap,
126390
- moduleIdMap,
126391
- methodIdMap,
126392
- dbIdMap,
126393
- previousActiveViolations,
126394
- changedFileSet,
126395
- tracker: options.tracker,
126396
- enabledCategories: effectiveCategories,
126397
- enableLlmRules: effectiveLlmRules,
126398
- provider,
126399
- signal,
126400
- onLlmEstimate: options.onLlmEstimate ? async (estimate) => {
126401
- const proceed = await options.onLlmEstimate(estimate);
126402
- options.onLlmResolved?.(proceed);
126403
- return proceed;
126404
- } : void 0
126405
- });
126406
- } finally {
126407
- }
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
+ });
126408
126163
  if (pipelineResult.serviceDescriptions.length > 0) {
126409
126164
  for (const desc of pipelineResult.serviceDescriptions) {
126410
126165
  const svc = graph.services.find((s) => s.id === desc.id);
@@ -126416,56 +126171,105 @@ async function analyzeInProcess(project, options = {}) {
126416
126171
  enforceLocationInvariant(pipelineResult.added);
126417
126172
  enforceLocationInvariant(pipelineResult.unchanged);
126418
126173
  enforceLocationInvariant(pipelineResult.resolved);
126419
- const snapshot = {
126420
- id: analysisId,
126421
- 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,
126422
126179
  branch,
126423
126180
  commitHash,
126424
126181
  architecture: result.architecture,
126425
- status: "completed",
126426
126182
  metadata: result.metadata ?? null,
126427
126183
  graph,
126428
- violations: {
126429
- added: pipelineResult.added,
126430
- resolved: pipelineResult.resolvedRefs,
126431
- previousAnalysisId
126432
- },
126433
- usage
126434
- };
126435
- const latest = buildLatestSnapshot(snapshot, filename, pipelineResult.unchanged, pipelineResult.added);
126436
- const { bySeverity, total } = summarizeActiveViolations(latest.violations);
126437
- writeAnalysis(project.path, snapshot);
126438
- writeLatest(project.path, latest);
126439
- const historyEntry = buildHistoryEntry(snapshot, filename, pipelineResult);
126440
- appendHistory(project.path, historyEntry);
126441
- deleteDiff(project.path);
126442
- setLastAnalyzed(project.slug, now);
126443
- return {
126444
- analysisId,
126445
- filename,
126446
- serviceCount: result.services.length,
126447
- fileCount: result.fileAnalyses?.length ?? 0,
126448
- architecture: result.architecture,
126449
- durationMs: Date.now() - start,
126450
- violationsSummary: { total, bySeverity }
126184
+ changedFiles,
126185
+ pipelineResult,
126186
+ usage,
126187
+ latestBaseline,
126188
+ previousAnalysisId,
126189
+ analysisResult: result
126451
126190
  };
126452
126191
  } finally {
126453
126192
  releaseAnalyzeLock(project.path);
126454
126193
  }
126455
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
+ }
126456
126271
  function buildLatestSnapshot(snapshot, filename, unchanged, added) {
126457
- const { graph } = snapshot;
126458
- const serviceById = new Map(graph.services.map((s) => [s.id, s.name]));
126459
- const moduleById = new Map(graph.modules.map((m) => [m.id, m.name]));
126460
- const methodById = new Map(graph.methods.map((m) => [m.id, m.name]));
126461
- const databaseById = new Map(graph.databases.map((d3) => [d3.id, d3.name]));
126462
- const denormalize = (v) => ({
126463
- ...v,
126464
- targetServiceName: v.targetServiceId ? serviceById.get(v.targetServiceId) ?? null : null,
126465
- targetModuleName: v.targetModuleId ? moduleById.get(v.targetModuleId) ?? null : null,
126466
- targetMethodName: v.targetMethodId ? methodById.get(v.targetMethodId) ?? null : null,
126467
- targetDatabaseName: v.targetDatabaseId ? databaseById.get(v.targetDatabaseId) ?? null : null
126468
- });
126272
+ const denormalize = makeDenormalizer(snapshot.graph);
126469
126273
  return {
126470
126274
  head: filename,
126471
126275
  analysis: {
@@ -126477,7 +126281,7 @@ function buildLatestSnapshot(snapshot, filename, unchanged, added) {
126477
126281
  metadata: snapshot.metadata,
126478
126282
  status: "completed"
126479
126283
  },
126480
- graph,
126284
+ graph: snapshot.graph,
126481
126285
  violations: [...added.map(denormalize), ...unchanged.map(denormalize)]
126482
126286
  };
126483
126287
  }
@@ -126541,17 +126345,159 @@ function buildHistoryEntry(snapshot, filename, pipeline) {
126541
126345
  }
126542
126346
  };
126543
126347
  }
126544
- function enforceLocationInvariant(violations) {
126545
- for (const v of violations) {
126546
- const hasFile = v.filePath != null;
126547
- const hasRange = v.lineStart != null && v.lineEnd != null;
126548
- 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))
126549
126377
  continue;
126550
- log.warn(`[Violations] ${v.ruleKey}: partial location (filePath=${v.filePath}, lineStart=${v.lineStart}, lineEnd=${v.lineEnd}) \u2014 dropping to uphold the location invariant`);
126551
- v.filePath = null;
126552
- v.lineStart = null;
126553
- 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}`);
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();
126554
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);
126555
126501
  }
126556
126502
 
126557
126503
  // tools/cli/src/commands/analyze.ts
@@ -126562,10 +126508,24 @@ init_project_config();
126562
126508
  init_logger();
126563
126509
  init_helpers();
126564
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
+
126565
126525
  // tools/cli/src/telemetry.ts
126566
126526
  init_dist4();
126567
126527
  import fs9 from "node:fs";
126568
- import path13 from "node:path";
126528
+ import path14 from "node:path";
126569
126529
  import os4 from "node:os";
126570
126530
  import crypto from "node:crypto";
126571
126531
  var DEFAULT_CONFIG2 = {
@@ -126574,7 +126534,7 @@ var DEFAULT_CONFIG2 = {
126574
126534
  noticeShown: false
126575
126535
  };
126576
126536
  function getTelemetryConfigPath() {
126577
- return path13.join(os4.homedir(), ".truecourse", "telemetry.json");
126537
+ return path14.join(os4.homedir(), ".truecourse", "telemetry.json");
126578
126538
  }
126579
126539
  function readTelemetryConfig() {
126580
126540
  const configPath = getTelemetryConfigPath();
@@ -126599,7 +126559,7 @@ function readTelemetryConfig() {
126599
126559
  }
126600
126560
  function writeTelemetryConfig(partial) {
126601
126561
  const configPath = getTelemetryConfigPath();
126602
- const dir = path13.dirname(configPath);
126562
+ const dir = path14.dirname(configPath);
126603
126563
  fs9.mkdirSync(dir, { recursive: true });
126604
126564
  let current;
126605
126565
  try {
@@ -126734,16 +126694,9 @@ async function runAnalyze(_options = {}) {
126734
126694
  enableLlmRulesOverride: enableLlmRules,
126735
126695
  onLlmEstimate: async (estimate) => {
126736
126696
  stopSpinner();
126737
- const totalRules = estimate.uniqueRuleCount ?? estimate.tiers.reduce((s, t2) => s + t2.ruleCount, 0);
126738
- const totalFiles = estimate.uniqueFileCount ?? estimate.tiers.reduce((s, t2) => s + t2.fileCount, 0);
126739
- const tokens = estimate.totalEstimatedTokens;
126740
- const tokenStr = tokens >= 1e6 ? `~${(tokens / 1e6).toFixed(1)}M tokens` : `~${Math.round(tokens / 1e3)}k tokens`;
126741
- O2.step(`LLM will analyze ${totalFiles} files with ${totalRules} rules (${tokenStr})`);
126742
- const proceed = await ot2({ message: "Run LLM-powered rules?", initialValue: true });
126697
+ const proceed = await promptLlmEstimate(estimate);
126743
126698
  renderPhase = "post-llm";
126744
- if (q(proceed)) return false;
126745
- if (!proceed) O2.info("Skipping LLM rules.");
126746
- return !!proceed;
126699
+ return proceed;
126747
126700
  }
126748
126701
  });
126749
126702
  stopSpinner();
@@ -126774,19 +126727,32 @@ async function runAnalyzeDiff(_options = {}) {
126774
126727
  configureLogger({
126775
126728
  filePath: path15.join(project.path, ".truecourse/logs/analyze.log")
126776
126729
  });
126777
- const spinner = fe();
126778
- 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);
126779
126738
  const abortController = new AbortController();
126780
126739
  const onSigint = () => abortController.abort();
126781
126740
  process.on("SIGINT", onSigint);
126782
126741
  try {
126783
126742
  const { diff } = await diffInProcess2(project, {
126743
+ tracker,
126784
126744
  signal: abortController.signal,
126785
- onProgress: ({ detail }) => {
126786
- 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;
126787
126752
  }
126788
126753
  });
126789
- spinner.stop("Diff check complete");
126754
+ stopSpinner();
126755
+ O2.success("Diff check complete");
126790
126756
  renderDiffResultsSummary2({
126791
126757
  changedFiles: diff.changedFiles,
126792
126758
  newViolations: diff.newViolations,
@@ -126796,7 +126762,7 @@ async function runAnalyzeDiff(_options = {}) {
126796
126762
  });
126797
126763
  gt("Diff complete \u2014 view results with: truecourse dashboard");
126798
126764
  } catch (err) {
126799
- spinner.stop("Diff check failed");
126765
+ stopSpinner();
126800
126766
  if (err instanceof DOMException && err.name === "AbortError") {
126801
126767
  gt("Diff cancelled");
126802
126768
  process.exit(130);
@@ -130474,7 +130440,7 @@ async function runHooksRun() {
130474
130440
 
130475
130441
  // tools/cli/src/index.ts
130476
130442
  var program2 = new Command();
130477
- program2.name("truecourse").version("0.4.3").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130443
+ program2.name("truecourse").version("0.4.4").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130478
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) => {
130479
130445
  await runDashboard({ reconfigure: options.reconfigure });
130480
130446
  });