truecourse 0.5.6 → 0.5.7

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/server.mjs CHANGED
@@ -40794,7 +40794,72 @@ var init_language_config = __esm({
40794
40794
 
40795
40795
  // packages/analyzer/dist/file-discovery.js
40796
40796
  import { existsSync as existsSync2, readFileSync, readdirSync, statSync as statSync2 } from "fs";
40797
+ import { execFileSync } from "child_process";
40797
40798
  import { join, relative, resolve } from "path";
40799
+ function isInsideGitWorkTree(dir) {
40800
+ try {
40801
+ const out = execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], {
40802
+ stdio: ["ignore", "pipe", "ignore"]
40803
+ });
40804
+ return out.toString("utf8").trim() === "true";
40805
+ } catch {
40806
+ return false;
40807
+ }
40808
+ }
40809
+ function buildPostGitFilter(dir) {
40810
+ const ig = (0, import_ignore.default)();
40811
+ const tcPath = join(dir, ".truecourseignore");
40812
+ if (existsSync2(tcPath))
40813
+ ig.add(readFileSync(tcPath, "utf8"));
40814
+ ig.add(".git");
40815
+ for (const p of getAllTestPatterns())
40816
+ ig.add(p);
40817
+ for (const p of getAllIgnorePatterns())
40818
+ ig.add(p);
40819
+ return ig;
40820
+ }
40821
+ function discoverFilesViaGit(dir) {
40822
+ if (!isInsideGitWorkTree(dir))
40823
+ return null;
40824
+ let stdout;
40825
+ try {
40826
+ stdout = execFileSync("git", ["-C", dir, "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "."], { maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
40827
+ } catch {
40828
+ return null;
40829
+ }
40830
+ const relPaths = stdout.toString("utf8").split("\0").filter(Boolean);
40831
+ relPaths.sort(compareDepthFirst);
40832
+ const ig = buildPostGitFilter(dir);
40833
+ const out = [];
40834
+ for (const rel of relPaths) {
40835
+ if (ig.ignores(rel))
40836
+ continue;
40837
+ const abs = join(dir, rel);
40838
+ let isFile = false;
40839
+ try {
40840
+ isFile = statSync2(abs).isFile();
40841
+ } catch {
40842
+ continue;
40843
+ }
40844
+ if (!isFile)
40845
+ continue;
40846
+ if (detectLanguage(abs))
40847
+ out.push(abs);
40848
+ }
40849
+ return out;
40850
+ }
40851
+ function compareDepthFirst(a, b) {
40852
+ const ap = a.split("/");
40853
+ const bp = b.split("/");
40854
+ const len = Math.min(ap.length, bp.length);
40855
+ for (let i = 0; i < len; i++) {
40856
+ const x = ap[i];
40857
+ const y = bp[i];
40858
+ if (x !== y)
40859
+ return x < y ? -1 : 1;
40860
+ }
40861
+ return ap.length - bp.length;
40862
+ }
40798
40863
  function findAllGitignores(startDir) {
40799
40864
  const gitignores = [];
40800
40865
  let currentDir = resolve(startDir);
@@ -40804,25 +40869,46 @@ function findAllGitignores(startDir) {
40804
40869
  gitignores.unshift({ path: gitignorePath, dir: currentDir });
40805
40870
  }
40806
40871
  const parentDir = resolve(currentDir, "..");
40807
- if (parentDir === currentDir) {
40872
+ if (parentDir === currentDir)
40808
40873
  break;
40809
- }
40810
40874
  currentDir = parentDir;
40811
40875
  }
40812
40876
  return gitignores;
40813
40877
  }
40878
+ function reanchorTruecourseignore(content, prefix) {
40879
+ if (prefix === "" || prefix === ".")
40880
+ return content;
40881
+ return content.split("\n").map((line) => {
40882
+ const raw = line;
40883
+ const trimmed2 = raw.trim();
40884
+ if (trimmed2 === "" || trimmed2.startsWith("#"))
40885
+ return raw;
40886
+ const negate = trimmed2.startsWith("!");
40887
+ const body = negate ? trimmed2.slice(1) : trimmed2;
40888
+ if (body.startsWith("**/"))
40889
+ return raw;
40890
+ const hasLeadingSlash = body.startsWith("/");
40891
+ const withoutTrailing = body.endsWith("/") ? body.slice(0, -1) : body;
40892
+ const inner = hasLeadingSlash ? withoutTrailing.slice(1) : withoutTrailing;
40893
+ const hasInternalSlash = inner.includes("/");
40894
+ if (!hasLeadingSlash && !hasInternalSlash)
40895
+ return raw;
40896
+ const stripped = hasLeadingSlash ? body.slice(1) : body;
40897
+ return `${negate ? "!" : ""}${prefix}/${stripped}`;
40898
+ }).join("\n");
40899
+ }
40814
40900
  function loadIgnorePatterns(baseDir) {
40815
40901
  const ig = (0, import_ignore.default)();
40816
40902
  const gitignores = findAllGitignores(baseDir);
40817
40903
  const rootDir = gitignores.length > 0 && gitignores[0] ? gitignores[0].dir : baseDir;
40818
40904
  for (const { path: gitignorePath } of gitignores) {
40819
- const content = readFileSync(gitignorePath, "utf8");
40820
- ig.add(content);
40905
+ ig.add(readFileSync(gitignorePath, "utf8"));
40821
40906
  }
40822
40907
  const truecourseignorePath = join(baseDir, ".truecourseignore");
40823
40908
  if (existsSync2(truecourseignorePath)) {
40824
40909
  const content = readFileSync(truecourseignorePath, "utf8");
40825
- ig.add(content);
40910
+ const prefix = relative(rootDir, baseDir).replace(/\\/g, "/");
40911
+ ig.add(reanchorTruecourseignore(content, prefix));
40826
40912
  }
40827
40913
  ig.add(".git");
40828
40914
  for (const pattern of getAllTestPatterns())
@@ -40831,7 +40917,7 @@ function loadIgnorePatterns(baseDir) {
40831
40917
  ig.add(pattern);
40832
40918
  return { ig, rootDir };
40833
40919
  }
40834
- function discoverFiles(dir) {
40920
+ function discoverFilesViaWalker(dir) {
40835
40921
  const files = [];
40836
40922
  const { ig, rootDir } = loadIgnorePatterns(dir);
40837
40923
  function traverse(currentPath) {
@@ -40842,23 +40928,27 @@ function discoverFiles(dir) {
40842
40928
  const relativePath = relative(rootDir, fullPath);
40843
40929
  const stat = statSync2(fullPath);
40844
40930
  const pathToCheck = stat.isDirectory() ? relativePath + "/" : relativePath;
40845
- if (ig.ignores(pathToCheck)) {
40931
+ if (ig.ignores(pathToCheck))
40846
40932
  continue;
40847
- }
40848
40933
  if (stat.isDirectory()) {
40849
40934
  traverse(fullPath);
40850
40935
  } else if (stat.isFile()) {
40851
- if (detectLanguage(fullPath)) {
40936
+ if (detectLanguage(fullPath))
40852
40937
  files.push(fullPath);
40853
- }
40854
40938
  }
40855
40939
  }
40856
- } catch (error) {
40940
+ } catch {
40857
40941
  }
40858
40942
  }
40859
40943
  traverse(dir);
40860
40944
  return files;
40861
40945
  }
40946
+ function discoverFiles(dir) {
40947
+ const viaGit = discoverFilesViaGit(dir);
40948
+ if (viaGit !== null)
40949
+ return viaGit;
40950
+ return discoverFilesViaWalker(dir);
40951
+ }
40862
40952
  var import_ignore;
40863
40953
  var init_file_discovery = __esm({
40864
40954
  "packages/analyzer/dist/file-discovery.js"() {
@@ -140509,6 +140599,30 @@ function createSocketLlmEstimateHandler(repoId) {
140509
140599
  }
140510
140600
  });
140511
140601
  }
140602
+ function createSocketStashConfirmHandler(repoId) {
140603
+ return (info) => new Promise((resolve7) => {
140604
+ const io3 = getIO();
140605
+ const room = `repo:${repoId}`;
140606
+ io3.to(room).emit("analysis:stash-confirm-request", {
140607
+ repoId,
140608
+ modifiedCount: info.modifiedCount,
140609
+ untrackedCount: info.untrackedCount
140610
+ });
140611
+ function onResponse(data) {
140612
+ if (data.repoId !== repoId) return;
140613
+ cleanup();
140614
+ resolve7(data.choice);
140615
+ }
140616
+ function cleanup() {
140617
+ for (const [, socket] of io3.sockets.sockets) {
140618
+ socket.removeListener("analysis:stash-confirm-response", onResponse);
140619
+ }
140620
+ }
140621
+ for (const [, socket] of io3.sockets.sockets) {
140622
+ socket.on("analysis:stash-confirm-response", onResponse);
140623
+ }
140624
+ });
140625
+ }
140512
140626
 
140513
140627
  // apps/dashboard/server/src/socket/index.ts
140514
140628
  var io2 = null;
@@ -145631,9 +145745,9 @@ function deleteDiff(repoPath) {
145631
145745
  // packages/core/dist/commands/analyze-core.js
145632
145746
  init_logger();
145633
145747
  import { randomUUID as randomUUID6 } from "node:crypto";
145748
+ import path12 from "node:path";
145634
145749
 
145635
145750
  // packages/core/dist/services/analyzer.service.js
145636
- import path11 from "node:path";
145637
145751
  init_logger();
145638
145752
  init_dist2();
145639
145753
  function runDeterministicModuleChecks(result, enabledDeterministic) {
@@ -145668,150 +145782,122 @@ function runDeterministicServiceChecks(result, enabledDeterministic) {
145668
145782
  }
145669
145783
  async function runAnalysis(repoPath, _branch, onProgress, options) {
145670
145784
  let currentBranch = "unknown";
145671
- let didStash = false;
145672
- let isSubdirectory = false;
145673
- let hasChanges = false;
145674
- let git;
145675
145785
  if (!options?.skipGit) {
145676
- git = await getGit(repoPath);
145786
+ const git = await getGit(repoPath);
145677
145787
  currentBranch = (await git.branch()).current;
145678
- const statusResult = await git.status();
145679
- hasChanges = !statusResult.isClean();
145680
- const gitRoot = (await git.revparse(["--show-toplevel"])).trim();
145681
- isSubdirectory = path11.resolve(repoPath) !== path11.resolve(gitRoot);
145682
145788
  }
145683
- if (hasChanges && !options?.skipStash && !isSubdirectory && git) {
145684
- onProgress({ step: "stash", percent: 2, detail: "Stashing pending changes to analyze committed state..." });
145789
+ onProgress({ step: "discover", percent: 10, detail: "Discovering files..." });
145790
+ const analyzer = await Promise.resolve().then(() => (init_dist2(), dist_exports));
145791
+ await analyzer.initParsers();
145792
+ const files = await analyzer.discoverFiles(repoPath);
145793
+ onProgress({
145794
+ step: "discover",
145795
+ percent: 15,
145796
+ detail: `Found ${files.length} files`
145797
+ });
145798
+ onProgress({ step: "analyze", percent: 20, detail: "Analyzing files..." });
145799
+ const fileAnalyses = [];
145800
+ const totalFiles = files.length;
145801
+ for (let i = 0; i < totalFiles; i++) {
145802
+ if (options?.signal?.aborted)
145803
+ throw new DOMException("Analysis cancelled", "AbortError");
145804
+ const file = files[i];
145685
145805
  try {
145686
- const stashResult = await git.stash(["push", "--include-untracked", "-m", "truecourse-analysis-stash"]);
145687
- didStash = !stashResult.includes("No local changes");
145806
+ const analysis = await analyzer.analyzeFile(file);
145807
+ if (analysis) {
145808
+ fileAnalyses.push(analysis);
145809
+ }
145688
145810
  } catch (error) {
145689
- log.warn(`[Analyzer] Failed to stash changes, analyzing current state: ${error instanceof Error ? error.message : String(error)}`);
145811
+ log.warn(`[Analyzer] Failed to analyze ${file}: ${error instanceof Error ? error.message : String(error)}`);
145812
+ }
145813
+ if (i % 10 === 0 || i === totalFiles - 1) {
145814
+ const analyzePercent = 20 + Math.round((i + 1) / totalFiles * 40);
145815
+ onProgress({
145816
+ step: "analyze",
145817
+ percent: analyzePercent,
145818
+ detail: `Analyzed ${i + 1}/${totalFiles} files`
145819
+ });
145690
145820
  }
145691
145821
  }
145692
- try {
145693
- onProgress({ step: "discover", percent: 10, detail: "Discovering files..." });
145694
- const analyzer = await Promise.resolve().then(() => (init_dist2(), dist_exports));
145695
- await analyzer.initParsers();
145696
- const files = await analyzer.discoverFiles(repoPath);
145697
- onProgress({
145698
- step: "discover",
145699
- percent: 15,
145700
- detail: `Found ${files.length} files`
145701
- });
145702
- onProgress({ step: "analyze", percent: 20, detail: "Analyzing files..." });
145703
- const fileAnalyses = [];
145704
- const totalFiles = files.length;
145705
- for (let i = 0; i < totalFiles; i++) {
145706
- if (options?.signal?.aborted)
145707
- throw new DOMException("Analysis cancelled", "AbortError");
145708
- const file = files[i];
145709
- try {
145710
- const analysis = await analyzer.analyzeFile(file);
145711
- if (analysis) {
145712
- fileAnalyses.push(analysis);
145713
- }
145714
- } catch (error) {
145715
- log.warn(`[Analyzer] Failed to analyze ${file}: ${error instanceof Error ? error.message : String(error)}`);
145716
- }
145717
- if (i % 10 === 0 || i === totalFiles - 1) {
145718
- const analyzePercent = 20 + Math.round((i + 1) / totalFiles * 40);
145719
- onProgress({
145720
- step: "analyze",
145721
- percent: analyzePercent,
145722
- detail: `Analyzed ${i + 1}/${totalFiles} files`
145723
- });
145822
+ onProgress({
145823
+ step: "dependencies",
145824
+ percent: 65,
145825
+ detail: "Building dependency graph..."
145826
+ });
145827
+ const scopedOptions = buildScopedCompilerOptions(repoPath);
145828
+ if (scopedOptions.length > 0) {
145829
+ const filePaths = fileAnalyses.map((fa) => fa.filePath);
145830
+ const { exportMap } = analyzeSemantics(filePaths, scopedOptions);
145831
+ for (const fa of fileAnalyses) {
145832
+ const fileExports = exportMap.get(fa.filePath);
145833
+ if (!fileExports)
145834
+ continue;
145835
+ const defaultExportedFn = fa.exports.find((e) => e.isDefault)?.name;
145836
+ for (const fn of fa.functions) {
145837
+ fn.isExported = fileExports.has(fn.name) || fileExports.has("default") && fn.name === defaultExportedFn;
145724
145838
  }
145725
145839
  }
145726
- onProgress({
145727
- step: "dependencies",
145728
- percent: 65,
145729
- detail: "Building dependency graph..."
145730
- });
145731
- const scopedOptions = buildScopedCompilerOptions(repoPath);
145732
- if (scopedOptions.length > 0) {
145733
- const filePaths = fileAnalyses.map((fa) => fa.filePath);
145734
- const { exportMap } = analyzeSemantics(filePaths, scopedOptions);
145735
- for (const fa of fileAnalyses) {
145840
+ }
145841
+ const filesByLanguage = /* @__PURE__ */ new Map();
145842
+ for (const fa of fileAnalyses) {
145843
+ const list = filesByLanguage.get(fa.language) || [];
145844
+ list.push(fa);
145845
+ filesByLanguage.set(fa.language, list);
145846
+ }
145847
+ for (const [language, files2] of filesByLanguage) {
145848
+ const serverConfig = getLspServerConfig(language);
145849
+ if (!serverConfig)
145850
+ continue;
145851
+ try {
145852
+ const lspClient = new LspClient(serverConfig);
145853
+ await lspClient.start(repoPath);
145854
+ const filePaths = files2.map((fa) => fa.filePath);
145855
+ const { exportMap } = await lspClient.analyzeSemantics(filePaths.map((fp) => fp.startsWith(repoPath) ? fp.slice(repoPath.length + 1) : fp));
145856
+ for (const fa of files2) {
145736
145857
  const fileExports = exportMap.get(fa.filePath);
145737
145858
  if (!fileExports)
145738
145859
  continue;
145739
- const defaultExportedFn = fa.exports.find((e) => e.isDefault)?.name;
145740
145860
  for (const fn of fa.functions) {
145741
- fn.isExported = fileExports.has(fn.name) || fileExports.has("default") && fn.name === defaultExportedFn;
145861
+ fn.isExported = fileExports.has(fn.name);
145742
145862
  }
145743
145863
  }
145744
- }
145745
- const filesByLanguage = /* @__PURE__ */ new Map();
145746
- for (const fa of fileAnalyses) {
145747
- const list = filesByLanguage.get(fa.language) || [];
145748
- list.push(fa);
145749
- filesByLanguage.set(fa.language, list);
145750
- }
145751
- for (const [language, files2] of filesByLanguage) {
145752
- const serverConfig = getLspServerConfig(language);
145753
- if (!serverConfig)
145754
- continue;
145755
- try {
145756
- const lspClient = new LspClient(serverConfig);
145757
- await lspClient.start(repoPath);
145758
- const filePaths = files2.map((fa) => fa.filePath);
145759
- const { exportMap } = await lspClient.analyzeSemantics(filePaths.map((fp) => fp.startsWith(repoPath) ? fp.slice(repoPath.length + 1) : fp));
145760
- for (const fa of files2) {
145761
- const fileExports = exportMap.get(fa.filePath);
145762
- if (!fileExports)
145763
- continue;
145764
- for (const fn of fa.functions) {
145765
- fn.isExported = fileExports.has(fn.name);
145766
- }
145767
- }
145768
- await lspClient.stop();
145769
- } catch (error) {
145770
- log.warn(`[Analyzer] ${serverConfig.name} LSP analysis failed, using tree-sitter heuristics: ${error instanceof Error ? error.message : String(error)}`);
145771
- }
145772
- }
145773
- const moduleDependencies = analyzer.buildDependencyGraph(fileAnalyses, repoPath);
145774
- onProgress({
145775
- step: "services",
145776
- percent: 75,
145777
- detail: "Detecting services..."
145778
- });
145779
- const splitResult = analyzer.performSplitAnalysis(repoPath, fileAnalyses, moduleDependencies);
145780
- onProgress({
145781
- step: "saving",
145782
- percent: 80,
145783
- detail: `Saving results: ${splitResult.services.length} services detected`
145784
- });
145785
- return {
145786
- architecture: splitResult.architecture,
145787
- services: splitResult.services,
145788
- dependencies: splitResult.dependencies,
145789
- layerDetails: splitResult.layerDetails,
145790
- databaseResult: splitResult.databaseResult,
145791
- modules: splitResult.modules,
145792
- methods: splitResult.methods,
145793
- moduleLevelDependencies: splitResult.moduleLevelDependencies,
145794
- methodLevelDependencies: splitResult.methodLevelDependencies,
145795
- fileAnalyses,
145796
- moduleDependencies,
145797
- entryPointFiles: new Set(findEntryPoints(fileAnalyses, moduleDependencies)),
145798
- metadata: {
145799
- totalFiles: files.length,
145800
- analyzedFiles: fileAnalyses.length,
145801
- branch: currentBranch || "HEAD",
145802
- analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
145803
- }
145804
- };
145805
- } finally {
145806
- if (didStash && git) {
145807
- onProgress({ step: "unstash", percent: 80, detail: "Restoring pending changes..." });
145808
- try {
145809
- await git.stash(["pop"]);
145810
- } catch (error) {
145811
- log.error(`[Analyzer] Failed to restore stashed changes. Run "git stash pop" manually. ${error instanceof Error ? error.message : String(error)}`);
145812
- }
145864
+ await lspClient.stop();
145865
+ } catch (error) {
145866
+ log.warn(`[Analyzer] ${serverConfig.name} LSP analysis failed, using tree-sitter heuristics: ${error instanceof Error ? error.message : String(error)}`);
145813
145867
  }
145814
145868
  }
145869
+ const moduleDependencies = analyzer.buildDependencyGraph(fileAnalyses, repoPath);
145870
+ onProgress({
145871
+ step: "services",
145872
+ percent: 75,
145873
+ detail: "Detecting services..."
145874
+ });
145875
+ const splitResult = analyzer.performSplitAnalysis(repoPath, fileAnalyses, moduleDependencies);
145876
+ onProgress({
145877
+ step: "saving",
145878
+ percent: 80,
145879
+ detail: `Saving results: ${splitResult.services.length} services detected`
145880
+ });
145881
+ return {
145882
+ architecture: splitResult.architecture,
145883
+ services: splitResult.services,
145884
+ dependencies: splitResult.dependencies,
145885
+ layerDetails: splitResult.layerDetails,
145886
+ databaseResult: splitResult.databaseResult,
145887
+ modules: splitResult.modules,
145888
+ methods: splitResult.methods,
145889
+ moduleLevelDependencies: splitResult.moduleLevelDependencies,
145890
+ methodLevelDependencies: splitResult.methodLevelDependencies,
145891
+ fileAnalyses,
145892
+ moduleDependencies,
145893
+ entryPointFiles: new Set(findEntryPoints(fileAnalyses, moduleDependencies)),
145894
+ metadata: {
145895
+ totalFiles: files.length,
145896
+ analyzedFiles: fileAnalyses.length,
145897
+ branch: currentBranch || "HEAD",
145898
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
145899
+ }
145900
+ };
145815
145901
  }
145816
145902
 
145817
145903
  // packages/core/dist/services/analysis-persistence.service.js
@@ -146282,7 +146368,7 @@ async function enrichFlowWithLLM(repoPath, flowId) {
146282
146368
  init_dist2();
146283
146369
  import { randomUUID as randomUUID5 } from "node:crypto";
146284
146370
  import fs9 from "node:fs";
146285
- import path12 from "node:path";
146371
+ import path11 from "node:path";
146286
146372
 
146287
146373
  // packages/core/dist/services/rules.service.js
146288
146374
  init_dist2();
@@ -147184,7 +147270,7 @@ async function runViolationPipeline(input) {
147184
147270
  const enabledLlmCodeRules = enableLlmRules !== false ? allRules.filter((r) => (r.domain ? codeDomains.has(r.domain) : r.category === "code") && r.type === "llm" && r.prompt) : [];
147185
147271
  const archLlmRules = allRules.filter((r) => r.type === "llm" && r.prompt && r.domain === "architecture").map((r) => ({ key: r.key, name: r.name, severity: r.severity, prompt: r.prompt, category: r.category }));
147186
147272
  const dbSchemaLlmRules = allRules.filter((r) => r.type === "llm" && r.prompt && r.domain === "database" && r.category === "database").map((r) => ({ key: r.key, name: r.name, severity: r.severity, prompt: r.prompt, category: r.category }));
147187
- const filesToScan = changedFileSet ? [...changedFileSet].map((relPath) => ({ filePath: relPath, resolve: true })) : (result.fileAnalyses || []).map((fa) => ({ filePath: fa.filePath, resolve: !path12.isAbsolute(fa.filePath) }));
147273
+ const filesToScan = changedFileSet ? [...changedFileSet].map((relPath) => ({ filePath: relPath, resolve: true })) : (result.fileAnalyses || []).map((fa) => ({ filePath: fa.filePath, resolve: !path11.isAbsolute(fa.filePath) }));
147188
147274
  const hasLlm = enabledLlm.length > 0;
147189
147275
  if (hasLlm)
147190
147276
  tracker?.start("scan", "Reading files...");
@@ -147196,7 +147282,7 @@ async function runViolationPipeline(input) {
147196
147282
  const lang = detectLanguage(filePath);
147197
147283
  if (!lang)
147198
147284
  continue;
147199
- const absPath = resolve7 ? path12.resolve(repoPath, filePath) : path12.isAbsolute(filePath) ? filePath : path12.join(repoPath, filePath);
147285
+ const absPath = resolve7 ? path11.resolve(repoPath, filePath) : path11.isAbsolute(filePath) ? filePath : path11.join(repoPath, filePath);
147200
147286
  if (!fs9.existsSync(absPath))
147201
147287
  continue;
147202
147288
  const content = fs9.readFileSync(absPath, "utf-8");
@@ -147212,7 +147298,7 @@ async function runViolationPipeline(input) {
147212
147298
  let typeQuery;
147213
147299
  const enabledCodeKeys = new Set(enabledCodeRules.filter((r) => r.type === "deterministic" && r.enabled).map((r) => r.key));
147214
147300
  if (hasTypeAwareVisitors(enabledCodeKeys)) {
147215
- const tsFiles = filesToScan.filter(({ filePath: fp }) => /\.(ts|tsx|js|jsx)$/.test(fp)).map(({ filePath: fp, resolve: res }) => res ? path12.resolve(repoPath, fp) : path12.isAbsolute(fp) ? fp : path12.join(repoPath, fp));
147301
+ const tsFiles = filesToScan.filter(({ filePath: fp }) => /\.(ts|tsx|js|jsx)$/.test(fp)).map(({ filePath: fp, resolve: res }) => res ? path11.resolve(repoPath, fp) : path11.isAbsolute(fp) ? fp : path11.join(repoPath, fp));
147216
147302
  if (tsFiles.length > 0) {
147217
147303
  const scoped = buildScopedCompilerOptions(repoPath);
147218
147304
  typeQuery = createTypeQueryService(tsFiles, scoped);
@@ -147291,7 +147377,7 @@ async function runViolationPipeline(input) {
147291
147377
  const lang = detectLanguage(filePath);
147292
147378
  if (!lang)
147293
147379
  continue;
147294
- const absPath = resolve7 ? path12.resolve(repoPath, filePath) : path12.isAbsolute(filePath) ? filePath : path12.join(repoPath, filePath);
147380
+ const absPath = resolve7 ? path11.resolve(repoPath, filePath) : path11.isAbsolute(filePath) ? filePath : path11.join(repoPath, filePath);
147295
147381
  const key = changedFileSet ? absPath : filePath;
147296
147382
  const fc = fileContents.get(key);
147297
147383
  if (!fc)
@@ -147305,7 +147391,7 @@ async function runViolationPipeline(input) {
147305
147391
  }
147306
147392
  log.info(`[Pipeline] Code scan: ${allCodeViolations.length} violations from ${filesToScan.length} files (${enabledCodeRules.length} det rules, ${enabledLlmCodeRules.length} LLM rules)`);
147307
147393
  if (enabledCodeRules.some((r) => r.key === "bugs/deterministic/invalid-pyproject-toml")) {
147308
- const pyprojectPath = path12.join(repoPath, "pyproject.toml");
147394
+ const pyprojectPath = path11.join(repoPath, "pyproject.toml");
147309
147395
  if (fs9.existsSync(pyprojectPath)) {
147310
147396
  try {
147311
147397
  const { checkPyprojectToml: checkPyprojectToml2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports));
@@ -148128,7 +148214,7 @@ function processLlmCodeViolations(codeResult, validFilePaths, fileContents, allC
148128
148214
  for (const v of codeResult.violations) {
148129
148215
  let filePath = v.filePath;
148130
148216
  if (!validFilePaths.has(filePath)) {
148131
- const resolved = path12.resolve(repoPath, filePath);
148217
+ const resolved = path11.resolve(repoPath, filePath);
148132
148218
  if (validFilePaths.has(resolved)) {
148133
148219
  filePath = resolved;
148134
148220
  } else {
@@ -148222,116 +148308,153 @@ async function analyzeCore(project, options) {
148222
148308
  const start = Date.now();
148223
148309
  const effectiveCategories = options.enabledCategoriesOverride?.length ? options.enabledCategoriesOverride : projectConfig.enabledCategories ?? void 0;
148224
148310
  const effectiveLlmRules = projectConfig.enableLlmRules ?? options.enableLlmRulesOverride ?? true;
148225
- options.tracker?.start("parse", isDiff2 ? "Analyzing working tree..." : "Starting analysis...");
148226
- const result = await runAnalysis(project.path, branch ?? void 0, (progress) => {
148227
- options.tracker?.detail("parse", progress.detail ?? "Analyzing...");
148228
- options.onProgress?.({ detail: progress.detail });
148229
- }, { signal, skipStash: isDiff2 });
148230
- if (signal?.aborted) {
148231
- throw new DOMException(isDiff2 ? "Diff cancelled" : "Analysis cancelled", "AbortError");
148232
- }
148233
- const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
148234
- let changedFiles = [];
148235
- let changedFileSet;
148236
- if (isDiff2) {
148311
+ let didStash = false;
148312
+ let stashGit;
148313
+ if (!isDiff2 && !skipGit && !options.skipStash) {
148237
148314
  try {
148238
- const git = await getGit(project.path);
148239
- const statusResult = await git.status();
148240
- for (const f2 of statusResult.not_added)
148241
- changedFiles.push({ path: f2, status: "new" });
148242
- for (const f2 of statusResult.created)
148243
- changedFiles.push({ path: f2, status: "new" });
148244
- for (const f2 of statusResult.modified)
148245
- changedFiles.push({ path: f2, status: "modified" });
148246
- for (const f2 of statusResult.staged) {
148247
- if (!changedFiles.some((cf) => cf.path === f2)) {
148315
+ stashGit = await getGit(project.path);
148316
+ const status = await stashGit.status();
148317
+ if (!status.isClean()) {
148318
+ const gitRoot = (await stashGit.revparse(["--show-toplevel"])).trim();
148319
+ const isSubdirectory = path12.resolve(project.path) !== path12.resolve(gitRoot);
148320
+ if (!isSubdirectory) {
148321
+ options.tracker?.detail("parse", "Stashing pending changes...");
148322
+ options.onProgress?.({ detail: "Stashing pending changes to analyze committed state..." });
148323
+ const stashResult = await stashGit.stash([
148324
+ "push",
148325
+ "--include-untracked",
148326
+ "-m",
148327
+ "truecourse-analysis-stash"
148328
+ ]);
148329
+ didStash = !stashResult.includes("No local changes");
148330
+ }
148331
+ }
148332
+ } catch (error) {
148333
+ log.warn(`[Analyzer] Failed to stash changes, analyzing current state: ${error instanceof Error ? error.message : String(error)}`);
148334
+ }
148335
+ }
148336
+ try {
148337
+ options.tracker?.start("parse", isDiff2 ? "Analyzing working tree..." : "Starting analysis...");
148338
+ const result = await runAnalysis(project.path, branch ?? void 0, (progress) => {
148339
+ options.tracker?.detail("parse", progress.detail ?? "Analyzing...");
148340
+ options.onProgress?.({ detail: progress.detail });
148341
+ }, { signal });
148342
+ if (signal?.aborted) {
148343
+ throw new DOMException(isDiff2 ? "Diff cancelled" : "Analysis cancelled", "AbortError");
148344
+ }
148345
+ const { graph, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = buildGraph(result);
148346
+ let changedFiles = [];
148347
+ let changedFileSet;
148348
+ if (isDiff2) {
148349
+ try {
148350
+ const git = await getGit(project.path);
148351
+ const statusResult = await git.status();
148352
+ for (const f2 of statusResult.not_added)
148353
+ changedFiles.push({ path: f2, status: "new" });
148354
+ for (const f2 of statusResult.created)
148355
+ changedFiles.push({ path: f2, status: "new" });
148356
+ for (const f2 of statusResult.modified)
148248
148357
  changedFiles.push({ path: f2, status: "modified" });
148358
+ for (const f2 of statusResult.staged) {
148359
+ if (!changedFiles.some((cf) => cf.path === f2)) {
148360
+ changedFiles.push({ path: f2, status: "modified" });
148361
+ }
148249
148362
  }
148363
+ for (const f2 of statusResult.deleted)
148364
+ changedFiles.push({ path: f2, status: "deleted" });
148365
+ } catch (err) {
148366
+ log.warn(`[Diff] git status failed: ${err instanceof Error ? err.message : String(err)}`);
148367
+ }
148368
+ } else if (latestBaseline?.analysis.commitHash && !skipGit) {
148369
+ try {
148370
+ const git = await getGit(project.path);
148371
+ const diffOutput = await git.diff([latestBaseline.analysis.commitHash, "HEAD", "--name-only"]);
148372
+ const files = diffOutput.trim().split("\n").filter(Boolean);
148373
+ if (files.length > 0)
148374
+ changedFileSet = new Set(files);
148375
+ } catch {
148250
148376
  }
148251
- for (const f2 of statusResult.deleted)
148252
- changedFiles.push({ path: f2, status: "deleted" });
148253
- } catch (err) {
148254
- log.warn(`[Diff] git status failed: ${err instanceof Error ? err.message : String(err)}`);
148255
148377
  }
148256
- } else if (latestBaseline?.analysis.commitHash && !skipGit) {
148257
- try {
148258
- const git = await getGit(project.path);
148259
- const diffOutput = await git.diff([latestBaseline.analysis.commitHash, "HEAD", "--name-only"]);
148260
- const files = diffOutput.trim().split("\n").filter(Boolean);
148261
- if (files.length > 0)
148262
- changedFileSet = new Set(files);
148263
- } catch {
148378
+ if (!isDiff2) {
148379
+ try {
148380
+ graph.flows = detectFlows(result);
148381
+ } catch (flowError) {
148382
+ log.error(`[Flows] Detection failed: ${flowError instanceof Error ? flowError.message : String(flowError)}`);
148383
+ graph.flows = [];
148384
+ }
148385
+ touchProject(project.slug);
148386
+ }
148387
+ options.tracker?.done("parse", `${result.services.length} services, ${result.fileAnalyses?.length ?? 0} files`);
148388
+ const previousActiveViolations = latestBaseline ? latestBaseline.violations.filter((v) => branch == null || latestBaseline.analysis.branch == null || latestBaseline.analysis.branch === branch) : [];
148389
+ const previousAnalysisId = latestBaseline?.analysis.id ?? null;
148390
+ const provider = options.provider ?? (effectiveLlmRules ? createLLMProvider() : void 0);
148391
+ if (provider) {
148392
+ provider.setAnalysisId(analysisId);
148393
+ provider.setRepoPath(project.path);
148394
+ if (signal)
148395
+ provider.setAbortSignal(signal);
148396
+ }
148397
+ const pipelineResult = await runViolationPipeline({
148398
+ repoPath: project.path,
148399
+ analysisId,
148400
+ now,
148401
+ result,
148402
+ serviceIdMap,
148403
+ moduleIdMap,
148404
+ methodIdMap,
148405
+ dbIdMap,
148406
+ previousActiveViolations,
148407
+ changedFileSet,
148408
+ tracker: options.tracker,
148409
+ enabledCategories: effectiveCategories,
148410
+ enableLlmRules: effectiveLlmRules,
148411
+ provider,
148412
+ signal,
148413
+ onLlmEstimate: options.onLlmEstimate ? async (estimate) => {
148414
+ const proceed = await options.onLlmEstimate(estimate);
148415
+ options.onLlmResolved?.(proceed);
148416
+ return proceed;
148417
+ } : void 0
148418
+ });
148419
+ if (pipelineResult.serviceDescriptions.length > 0) {
148420
+ for (const desc of pipelineResult.serviceDescriptions) {
148421
+ const svc = graph.services.find((s) => s.id === desc.id);
148422
+ if (svc)
148423
+ svc.description = desc.description;
148424
+ }
148425
+ }
148426
+ const usage = provider ? toUsageRecords(provider.flushUsage()) : [];
148427
+ enforceLocationInvariant(pipelineResult.added);
148428
+ enforceLocationInvariant(pipelineResult.unchanged);
148429
+ enforceLocationInvariant(pipelineResult.resolved);
148430
+ log.info(`[${isDiff2 ? "Diff" : "Analysis"}] core complete in ${Date.now() - start}ms \u2014 ${pipelineResult.added.length} added, ${pipelineResult.unchanged.length} unchanged, ${pipelineResult.resolvedRefs.length} resolved`);
148431
+ return {
148432
+ mode,
148433
+ analysisId,
148434
+ now,
148435
+ branch,
148436
+ commitHash,
148437
+ architecture: result.architecture,
148438
+ metadata: result.metadata ?? null,
148439
+ graph,
148440
+ changedFiles,
148441
+ pipelineResult,
148442
+ usage,
148443
+ latestBaseline,
148444
+ previousAnalysisId,
148445
+ analysisResult: result
148446
+ };
148447
+ } finally {
148448
+ if (didStash && stashGit) {
148449
+ options.tracker?.detail("parse", "Restoring pending changes...");
148450
+ options.onProgress?.({ detail: "Restoring pending changes..." });
148451
+ try {
148452
+ await stashGit.stash(["pop"]);
148453
+ } catch (error) {
148454
+ log.error(`[Analyzer] Failed to restore stashed changes. Run "git stash pop" manually. ${error instanceof Error ? error.message : String(error)}`);
148455
+ }
148264
148456
  }
148265
148457
  }
148266
- if (!isDiff2) {
148267
- try {
148268
- graph.flows = detectFlows(result);
148269
- } catch (flowError) {
148270
- log.error(`[Flows] Detection failed: ${flowError instanceof Error ? flowError.message : String(flowError)}`);
148271
- graph.flows = [];
148272
- }
148273
- touchProject(project.slug);
148274
- }
148275
- options.tracker?.done("parse", `${result.services.length} services, ${result.fileAnalyses?.length ?? 0} files`);
148276
- const previousActiveViolations = latestBaseline ? latestBaseline.violations.filter((v) => branch == null || latestBaseline.analysis.branch == null || latestBaseline.analysis.branch === branch) : [];
148277
- const previousAnalysisId = latestBaseline?.analysis.id ?? null;
148278
- const provider = options.provider ?? (effectiveLlmRules ? createLLMProvider() : void 0);
148279
- if (provider) {
148280
- provider.setAnalysisId(analysisId);
148281
- provider.setRepoPath(project.path);
148282
- if (signal)
148283
- provider.setAbortSignal(signal);
148284
- }
148285
- const pipelineResult = await runViolationPipeline({
148286
- repoPath: project.path,
148287
- analysisId,
148288
- now,
148289
- result,
148290
- serviceIdMap,
148291
- moduleIdMap,
148292
- methodIdMap,
148293
- dbIdMap,
148294
- previousActiveViolations,
148295
- changedFileSet,
148296
- tracker: options.tracker,
148297
- enabledCategories: effectiveCategories,
148298
- enableLlmRules: effectiveLlmRules,
148299
- provider,
148300
- signal,
148301
- onLlmEstimate: options.onLlmEstimate ? async (estimate) => {
148302
- const proceed = await options.onLlmEstimate(estimate);
148303
- options.onLlmResolved?.(proceed);
148304
- return proceed;
148305
- } : void 0
148306
- });
148307
- if (pipelineResult.serviceDescriptions.length > 0) {
148308
- for (const desc of pipelineResult.serviceDescriptions) {
148309
- const svc = graph.services.find((s) => s.id === desc.id);
148310
- if (svc)
148311
- svc.description = desc.description;
148312
- }
148313
- }
148314
- const usage = provider ? toUsageRecords(provider.flushUsage()) : [];
148315
- enforceLocationInvariant(pipelineResult.added);
148316
- enforceLocationInvariant(pipelineResult.unchanged);
148317
- enforceLocationInvariant(pipelineResult.resolved);
148318
- log.info(`[${isDiff2 ? "Diff" : "Analysis"}] core complete in ${Date.now() - start}ms \u2014 ${pipelineResult.added.length} added, ${pipelineResult.unchanged.length} unchanged, ${pipelineResult.resolvedRefs.length} resolved`);
148319
- return {
148320
- mode,
148321
- analysisId,
148322
- now,
148323
- branch,
148324
- commitHash,
148325
- architecture: result.architecture,
148326
- metadata: result.metadata ?? null,
148327
- graph,
148328
- changedFiles,
148329
- pipelineResult,
148330
- usage,
148331
- latestBaseline,
148332
- previousAnalysisId,
148333
- analysisResult: result
148334
- };
148335
148458
  } finally {
148336
148459
  releaseAnalyzeLock(project.path);
148337
148460
  }
@@ -152743,7 +152866,28 @@ router2.delete("/:id/analyses/:analysisId", async (req, res, next) => {
152743
152866
  next(error);
152744
152867
  }
152745
152868
  });
152869
+ async function resolveStashDecisionForRoute(repoId, repoPath) {
152870
+ let modifiedCount = 0;
152871
+ let untrackedCount = 0;
152872
+ try {
152873
+ const git = await getGit(repoPath);
152874
+ const status = await git.status();
152875
+ if (status.isClean()) return "stash";
152876
+ const gitRoot = (await git.revparse(["--show-toplevel"])).trim();
152877
+ if (path15.resolve(repoPath) !== path15.resolve(gitRoot)) return "stash";
152878
+ modifiedCount = status.modified.length + status.staged.length + status.deleted.length + status.created.length;
152879
+ untrackedCount = status.not_added.length;
152880
+ } catch {
152881
+ return "stash";
152882
+ }
152883
+ return createSocketStashConfirmHandler(repoId)({ modifiedCount, untrackedCount });
152884
+ }
152746
152885
  async function runFullAnalyze(id, repo, opts) {
152886
+ const stashDecision = await resolveStashDecisionForRoute(id, repo.path);
152887
+ if (stashDecision === "cancel") {
152888
+ emitAnalysisCanceled(id);
152889
+ return;
152890
+ }
152747
152891
  const provider = opts.effectiveLlmRules ? createLLMProvider() : void 0;
152748
152892
  if (provider) {
152749
152893
  provider.setRepoId(id);
@@ -152752,6 +152896,7 @@ async function runFullAnalyze(id, repo, opts) {
152752
152896
  }
152753
152897
  const outcome = await analyzeInProcess(repo, {
152754
152898
  skipGit: opts.skipGit,
152899
+ skipStash: stashDecision === "no-stash",
152755
152900
  enabledCategoriesOverride: opts.effectiveCategories,
152756
152901
  enableLlmRulesOverride: opts.effectiveLlmRules,
152757
152902
  tracker: opts.tracker,