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/README.md +20 -1
- package/cli.mjs +402 -255
- package/package.json +1 -1
- package/public/assets/index-DU0Bp1u7.css +1 -0
- package/public/assets/{index-BCi5yD5r.js → index-DaPvUT-_.js} +94 -94
- package/public/index.html +2 -2
- package/server.mjs +394 -249
- package/public/assets/index-C6z6cTBb.css +0 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
145684
|
-
|
|
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
|
|
145687
|
-
|
|
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
|
|
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
|
-
|
|
145693
|
-
|
|
145694
|
-
|
|
145695
|
-
|
|
145696
|
-
|
|
145697
|
-
|
|
145698
|
-
|
|
145699
|
-
|
|
145700
|
-
|
|
145701
|
-
|
|
145702
|
-
|
|
145703
|
-
|
|
145704
|
-
|
|
145705
|
-
|
|
145706
|
-
|
|
145707
|
-
|
|
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
|
-
|
|
145727
|
-
|
|
145728
|
-
|
|
145729
|
-
|
|
145730
|
-
|
|
145731
|
-
|
|
145732
|
-
|
|
145733
|
-
|
|
145734
|
-
|
|
145735
|
-
|
|
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)
|
|
145861
|
+
fn.isExported = fileExports.has(fn.name);
|
|
145742
145862
|
}
|
|
145743
145863
|
}
|
|
145744
|
-
|
|
145745
|
-
|
|
145746
|
-
|
|
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
|
|
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: !
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
148226
|
-
|
|
148227
|
-
|
|
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
|
-
|
|
148239
|
-
const
|
|
148240
|
-
|
|
148241
|
-
|
|
148242
|
-
|
|
148243
|
-
|
|
148244
|
-
|
|
148245
|
-
|
|
148246
|
-
|
|
148247
|
-
|
|
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
|
-
|
|
148257
|
-
|
|
148258
|
-
|
|
148259
|
-
|
|
148260
|
-
|
|
148261
|
-
|
|
148262
|
-
|
|
148263
|
-
|
|
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,
|