truecourse 0.2.0 → 0.2.1

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
@@ -31191,6 +31191,8 @@ var init_schema2 = __esm({
31191
31191
  id: uuid("id").defaultRandom().primaryKey(),
31192
31192
  repoId: uuid("repo_id").notNull().references(() => repos.id, { onDelete: "cascade" }),
31193
31193
  branch: text("branch"),
31194
+ status: text("status").notNull().default("completed"),
31195
+ // 'running' | 'cancelling' | 'completed' | 'cancelled' | 'failed'
31194
31196
  architecture: text("architecture").notNull(),
31195
31197
  // 'monolith' | 'microservices'
31196
31198
  metadata: jsonb("metadata"),
@@ -48989,6 +48991,15 @@ function getAllIndexBaseNames() {
48989
48991
  }
48990
48992
  return names;
48991
48993
  }
48994
+ function getMaxParameters(filePath) {
48995
+ const lang = detectLanguage(filePath);
48996
+ if (lang) {
48997
+ const config3 = LANGUAGE_CONFIGS.find((c) => c.name === lang);
48998
+ if (config3)
48999
+ return config3.thresholds.maxParameters;
49000
+ }
49001
+ return 5;
49002
+ }
48992
49003
  function isBootstrapEntry(functionName, filePath) {
48993
49004
  return LANGUAGE_CONFIGS.some((c) => c.bootstrap.functionNames.includes(functionName) && c.bootstrap.filePattern.test(filePath));
48994
49005
  }
@@ -49063,6 +49074,9 @@ var init_language_config = __esm({
49063
49074
  filePattern: /(?:^|[/\\])(?:app|main|index|server)\.\w+$/,
49064
49075
  functionNames: ["start", "main", "bootstrap"]
49065
49076
  },
49077
+ thresholds: {
49078
+ maxParameters: 5
49079
+ },
49066
49080
  exportQuery: `
49067
49081
  (export_statement) @export
49068
49082
  `
@@ -49129,6 +49143,10 @@ var init_language_config = __esm({
49129
49143
  filePattern: /(?:^|[/\\])(?:app|main|wsgi|asgi)\.\w+$/,
49130
49144
  functionNames: ["start", "main", "__main__", "bootstrap"]
49131
49145
  },
49146
+ thresholds: {
49147
+ maxParameters: 7
49148
+ // Python uses keyword args extensively
49149
+ },
49132
49150
  importQuery: `
49133
49151
  (import_statement) @import
49134
49152
  (import_from_statement) @import
@@ -264143,6 +264161,15 @@ function isNestedInFunction2(node) {
264143
264161
  }
264144
264162
  return false;
264145
264163
  }
264164
+ function isInsideClass(node) {
264165
+ let current = node.parent;
264166
+ while (current) {
264167
+ if (current.type === "class_definition")
264168
+ return true;
264169
+ current = current.parent;
264170
+ }
264171
+ return false;
264172
+ }
264146
264173
  function extractDocstring(node) {
264147
264174
  const body = node.childForFieldName("body");
264148
264175
  if (!body)
@@ -264271,20 +264298,9 @@ function extractPythonParameters(paramsNode) {
264271
264298
  });
264272
264299
  break;
264273
264300
  }
264274
- case "list_splat_pattern": {
264275
- const name21 = child.namedChildren[0]?.text;
264276
- if (name21) {
264277
- parameters.push({ name: `*${name21}` });
264278
- }
264279
- break;
264280
- }
264281
- case "dictionary_splat_pattern": {
264282
- const name21 = child.namedChildren[0]?.text;
264283
- if (name21) {
264284
- parameters.push({ name: `**${name21}` });
264285
- }
264301
+ case "list_splat_pattern":
264302
+ case "dictionary_splat_pattern":
264286
264303
  break;
264287
- }
264288
264304
  }
264289
264305
  }
264290
264306
  return parameters;
@@ -264298,6 +264314,8 @@ function extractPythonClasses(tree, filePath) {
264298
264314
  if (seen.has(key))
264299
264315
  return;
264300
264316
  seen.add(key);
264317
+ if (isInsideClass(node))
264318
+ return;
264301
264319
  const name21 = node.childForFieldName("name")?.text;
264302
264320
  if (!name21)
264303
264321
  return;
@@ -264395,11 +264413,12 @@ function extractClassProperties(body) {
264395
264413
  continue;
264396
264414
  if (expr.type === "assignment") {
264397
264415
  const left = expr.childForFieldName("left");
264416
+ const typeNode = expr.childForFieldName("type");
264398
264417
  const right = expr.childForFieldName("right");
264399
264418
  if (left && left.type === "identifier") {
264400
264419
  properties.push({
264401
264420
  name: left.text,
264402
- type: right?.text
264421
+ type: typeNode?.text || right?.text
264403
264422
  });
264404
264423
  }
264405
264424
  } else if (expr.type === "type_alias_statement" || expr.type === "annotated_assignment") {
@@ -264553,6 +264572,21 @@ function extractPythonExports(tree, filePath) {
264553
264572
  }
264554
264573
  return exports2;
264555
264574
  }
264575
+ const classesWithInstances = /* @__PURE__ */ new Set();
264576
+ for (const child of root.namedChildren) {
264577
+ if (child.type === "expression_statement") {
264578
+ const expr = child.namedChildren[0];
264579
+ if (expr?.type === "assignment") {
264580
+ const right = expr.childForFieldName("right");
264581
+ if (right?.type === "call") {
264582
+ const fn = right.childForFieldName("function");
264583
+ if (fn?.type === "identifier") {
264584
+ classesWithInstances.add(fn.text);
264585
+ }
264586
+ }
264587
+ }
264588
+ }
264589
+ }
264556
264590
  for (const child of root.namedChildren) {
264557
264591
  if (child.type === "function_definition") {
264558
264592
  const name21 = child.childForFieldName("name")?.text;
@@ -264570,7 +264604,7 @@ function extractPythonExports(tree, filePath) {
264570
264604
  }
264571
264605
  } else if (child.type === "class_definition") {
264572
264606
  const name21 = child.childForFieldName("name")?.text;
264573
- if (name21 && !name21.startsWith("_")) {
264607
+ if (name21 && !name21.startsWith("_") && !classesWithInstances.has(name21)) {
264574
264608
  exports2.push({ name: name21, isDefault: false });
264575
264609
  }
264576
264610
  } else if (child.type === "expression_statement") {
@@ -267317,6 +267351,10 @@ function hasAPILayerPatterns(analysis) {
267317
267351
  const reasons = [];
267318
267352
  for (const imp of analysis.imports) {
267319
267353
  if (apiLayerPatterns.frameworks.some((fw) => matchesPattern(imp.source, fw))) {
267354
+ const importedNames = imp.specifiers.map((s) => s.name);
267355
+ if (importedNames.length > 0 && importedNames.every((n) => FRAMEWORK_UTILITY_IMPORTS.has(n))) {
267356
+ continue;
267357
+ }
267320
267358
  reasons.push(`Imports API framework: ${imp.source}`);
267321
267359
  return { match: true, reasons };
267322
267360
  }
@@ -267333,8 +267371,12 @@ function hasAPILayerPatterns(analysis) {
267333
267371
  return { match: true, reasons };
267334
267372
  }
267335
267373
  }
267374
+ const broadDirPatterns = /* @__PURE__ */ new Set(["**/api/**", "**/views/**"]);
267375
+ const hasRoutes = (analysis.routeRegistrations?.length ?? 0) > 0;
267336
267376
  for (const pattern of apiLayerPatterns.filePatterns) {
267337
267377
  if (minimatch(analysis.filePath, pattern)) {
267378
+ if (broadDirPatterns.has(pattern) && !hasRoutes)
267379
+ continue;
267338
267380
  reasons.push(`File path matches API layer pattern: ${pattern}`);
267339
267381
  return { match: true, reasons };
267340
267382
  }
@@ -267394,12 +267436,32 @@ function calculateConfidence(layers2, reasons) {
267394
267436
  }
267395
267437
  return 0.7;
267396
267438
  }
267439
+ var FRAMEWORK_UTILITY_IMPORTS;
267397
267440
  var init_layer_detector = __esm({
267398
267441
  "packages/analyzer/dist/layer-detector.js"() {
267399
267442
  "use strict";
267400
267443
  init_layer_patterns();
267401
267444
  init_patterns();
267402
267445
  init_esm3();
267446
+ FRAMEWORK_UTILITY_IMPORTS = /* @__PURE__ */ new Set([
267447
+ "HTTPException",
267448
+ "Depends",
267449
+ "Header",
267450
+ "Body",
267451
+ "Query",
267452
+ "Path",
267453
+ "Form",
267454
+ "File",
267455
+ "UploadFile",
267456
+ "status",
267457
+ "Response",
267458
+ "JSONResponse",
267459
+ "BackgroundTasks",
267460
+ "Security",
267461
+ "Cookie",
267462
+ "Request",
267463
+ "WebSocket"
267464
+ ]);
267403
267465
  }
267404
267466
  });
267405
267467
 
@@ -267597,17 +267659,32 @@ function detectByEntryPoints(_rootPath, allFiles) {
267597
267659
  if (entryPoints.length <= 1) {
267598
267660
  return [];
267599
267661
  }
267600
- const serviceMap = /* @__PURE__ */ new Map();
267601
- for (const entry of entryPoints) {
267602
- const dir = dirname4(entry);
267603
- serviceMap.set(dir, allFiles.filter((f) => f.startsWith(dir)));
267662
+ const entryDirs = entryPoints.map((e) => dirname4(e)).sort((a, b) => a.length - b.length);
267663
+ const topLevelDirs = [];
267664
+ for (const dir of entryDirs) {
267665
+ const isNested = topLevelDirs.some((parent) => dir.startsWith(parent + "/"));
267666
+ if (!isNested) {
267667
+ topLevelDirs.push(dir);
267668
+ }
267669
+ }
267670
+ const nameCount = /* @__PURE__ */ new Map();
267671
+ for (const dir of topLevelDirs) {
267672
+ const dirName = basename2(dir);
267673
+ nameCount.set(dirName, (nameCount.get(dirName) || 0) + 1);
267604
267674
  }
267605
267675
  const services2 = [];
267606
- for (const [dir, files] of serviceMap.entries()) {
267676
+ for (const dir of topLevelDirs) {
267677
+ const files = allFiles.filter((f) => f.startsWith(dir));
267678
+ if (files.length === 0)
267679
+ continue;
267607
267680
  const dirName = basename2(dir);
267608
267681
  const isSrcDir = patterns.commonSourceDirectories.includes(dirName);
267609
267682
  const serviceRoot = isSrcDir ? dirname4(dir) : dir;
267610
- const name21 = isSrcDir ? basename2(dirname4(dir)) : dirName;
267683
+ let name21 = isSrcDir ? basename2(dirname4(dir)) : dirName;
267684
+ if ((nameCount.get(dirName) || 0) > 1) {
267685
+ const parentName = basename2(dirname4(dir));
267686
+ name21 = `${parentName}-${dirName}`;
267687
+ }
267611
267688
  services2.push({
267612
267689
  name: name21,
267613
267690
  rootPath: dir,
@@ -267977,7 +268054,42 @@ var init_drizzle = __esm({
267977
268054
  function parseSqlAlchemySchema(content) {
267978
268055
  const tables = [];
267979
268056
  const relations2 = [];
267980
- const lines = content.split("\n");
268057
+ const rawLines = content.split("\n");
268058
+ const lines = [];
268059
+ let pending = "";
268060
+ let parenDepth = 0;
268061
+ for (const raw of rawLines) {
268062
+ if (parenDepth > 0) {
268063
+ pending += " " + raw.trim();
268064
+ for (const ch of raw) {
268065
+ if (ch === "(")
268066
+ parenDepth++;
268067
+ else if (ch === ")")
268068
+ parenDepth--;
268069
+ }
268070
+ if (parenDepth <= 0) {
268071
+ lines.push(pending);
268072
+ pending = "";
268073
+ parenDepth = 0;
268074
+ }
268075
+ } else {
268076
+ let depth = 0;
268077
+ for (const ch of raw) {
268078
+ if (ch === "(")
268079
+ depth++;
268080
+ else if (ch === ")")
268081
+ depth--;
268082
+ }
268083
+ if (depth > 0) {
268084
+ pending = raw;
268085
+ parenDepth = depth;
268086
+ } else {
268087
+ lines.push(raw);
268088
+ }
268089
+ }
268090
+ }
268091
+ if (pending)
268092
+ lines.push(pending);
267981
268093
  const classToTable = /* @__PURE__ */ new Map();
267982
268094
  {
267983
268095
  let cls = null;
@@ -268047,13 +268159,38 @@ function parseSqlAlchemySchema(content) {
268047
268159
  continue;
268048
268160
  }
268049
268161
  const columnMatch = trimmed2.match(/^(\w+)\s*=\s*Column\((.+)\)/);
268050
- if (columnMatch) {
268051
- const colName = columnMatch[1];
268052
- const colArgs = columnMatch[2];
268053
- const colType = mapSqlAlchemyType(colArgs);
268162
+ let mappedColMatch = null;
268163
+ if (!columnMatch) {
268164
+ const mcPrefix = trimmed2.match(/^(\w+)\s*:\s*Mapped\[/);
268165
+ if (mcPrefix) {
268166
+ const afterMapped = trimmed2.slice(mcPrefix[0].length);
268167
+ let depth = 1, idx = 0;
268168
+ while (idx < afterMapped.length && depth > 0) {
268169
+ if (afterMapped[idx] === "[")
268170
+ depth++;
268171
+ else if (afterMapped[idx] === "]")
268172
+ depth--;
268173
+ idx++;
268174
+ }
268175
+ if (depth === 0) {
268176
+ const mappedTypeStr = afterMapped.slice(0, idx - 1);
268177
+ const rest = afterMapped.slice(idx);
268178
+ const argsMatch = rest.match(/^\s*=\s*mapped_column\((.+)\)/);
268179
+ if (argsMatch) {
268180
+ mappedColMatch = [trimmed2, mcPrefix[1], mappedTypeStr, argsMatch[1]];
268181
+ }
268182
+ }
268183
+ }
268184
+ }
268185
+ const colMatch = columnMatch || mappedColMatch;
268186
+ if (colMatch) {
268187
+ const colName = colMatch[1];
268188
+ const colArgs = columnMatch ? colMatch[2] : colMatch[3];
268189
+ const mappedType = mappedColMatch ? colMatch[2] : null;
268190
+ const colType = mappedType ? mapMappedType(mappedType, colArgs) : mapSqlAlchemyType(colArgs);
268054
268191
  const isPrimaryKey = colArgs.includes("primary_key=True");
268055
- const isNullable = colArgs.includes("nullable=True") || !colArgs.includes("nullable=False") && !isPrimaryKey;
268056
- const fkMatch = colArgs.match(/ForeignKey\(["'](\w+)\.(\w+)["']\)/);
268192
+ const isNullable = mappedType ? /Optional|None/.test(mappedType) : colArgs.includes("nullable=True") || !colArgs.includes("nullable=False") && !isPrimaryKey;
268193
+ const fkMatch = colArgs.match(/ForeignKey\(["'](\w+)\.(\w+)["'][^)]*\)/);
268057
268194
  const isForeignKey = !!fkMatch;
268058
268195
  const referencesTable = fkMatch?.[1];
268059
268196
  const referencesColumn = fkMatch?.[2];
@@ -268086,7 +268223,7 @@ function parseSqlAlchemySchema(content) {
268086
268223
  }
268087
268224
  continue;
268088
268225
  }
268089
- const relMatch = trimmed2.match(/^(\w+)\s*=\s*relationship\(["'](\w+)["']/);
268226
+ const relMatch = trimmed2.match(/^(\w+)\s*(?::\s*Mapped\[[^\]]+\]\s*)?=\s*relationship\(["'](\w+)["']/);
268090
268227
  if (relMatch && currentTableName) {
268091
268228
  const targetModel = relMatch[2];
268092
268229
  const targetTable = classToTable.get(targetModel);
@@ -268131,6 +268268,23 @@ function mapSqlAlchemyType(colArgs) {
268131
268268
  };
268132
268269
  return typeMap[firstArg] || firstArg;
268133
268270
  }
268271
+ function mapMappedType(mappedType, colArgs) {
268272
+ const argType = mapSqlAlchemyType(colArgs);
268273
+ if (argType && argType !== colArgs.split(",")[0].trim()) {
268274
+ return argType;
268275
+ }
268276
+ const inner = mappedType.replace(/Optional\[([^\]]+)\]/, "$1").replace(/List\[([^\]]+)\]/, "$1[]").replace(/Dict\[([^\]]+)\]/, "Json").replace(/list\[([^\]]+)\]/, "$1[]").replace(/dict\[([^\]]+)\]/, "Json").trim();
268277
+ const pyTypeMap = {
268278
+ "str": "String",
268279
+ "int": "Int",
268280
+ "float": "Float",
268281
+ "bool": "Boolean",
268282
+ "datetime": "DateTime",
268283
+ "date": "Date",
268284
+ "bytes": "Bytes"
268285
+ };
268286
+ return pyTypeMap[inner] || argType || inner;
268287
+ }
268134
268288
  var init_sqlalchemy = __esm({
268135
268289
  "packages/analyzer/dist/schema-parsers/sqlalchemy.js"() {
268136
268290
  "use strict";
@@ -268401,7 +268555,7 @@ function extractModulesAndMethods(analyses2, layerDetails, fileDependencies) {
268401
268555
  lineCount: cls.location.endLine - cls.location.startLine + 1
268402
268556
  });
268403
268557
  for (const method of namedMethods) {
268404
- methods2.push(toMethodInfo(method, modName, serviceName, analysis.filePath));
268558
+ methods2.push(toMethodInfo(method, modName, serviceName, analysis.filePath, analysis.language));
268405
268559
  }
268406
268560
  addToFileModules(fileToModules, analysis.filePath, modName);
268407
268561
  }
@@ -268428,7 +268582,7 @@ function extractModulesAndMethods(analyses2, layerDetails, fileDependencies) {
268428
268582
  lineCount: totalFileLines(analysis)
268429
268583
  });
268430
268584
  for (const fn of extraFunctions) {
268431
- methods2.push(toMethodInfo(fn, modName, serviceName, analysis.filePath));
268585
+ methods2.push(toMethodInfo(fn, modName, serviceName, analysis.filePath, analysis.language));
268432
268586
  }
268433
268587
  addToFileModules(fileToModules, analysis.filePath, modName);
268434
268588
  }
@@ -268451,7 +268605,7 @@ function extractModulesAndMethods(analyses2, layerDetails, fileDependencies) {
268451
268605
  lineCount: totalFileLines(analysis)
268452
268606
  });
268453
268607
  for (const fn of namedFunctions) {
268454
- methods2.push(toMethodInfo(fn, modName, serviceName, analysis.filePath));
268608
+ methods2.push(toMethodInfo(fn, modName, serviceName, analysis.filePath, analysis.language));
268455
268609
  }
268456
268610
  addToFileModules(fileToModules, analysis.filePath, modName);
268457
268611
  }
@@ -268466,10 +268620,11 @@ function extractModulesAndMethods(analyses2, layerDetails, fileDependencies) {
268466
268620
  const methodDependencies = buildMethodDependencies(analyses2, modules2, methods2, fileLookup);
268467
268621
  return { modules: modules2, methods: methods2, moduleDependencies, methodDependencies };
268468
268622
  }
268469
- function toMethodInfo(fn, moduleName2, serviceName, filePath) {
268623
+ function toMethodInfo(fn, moduleName2, serviceName, filePath, language) {
268470
268624
  const params = fn.params.map((p) => p.type ? `${p.name}: ${p.type}` : p.name).join(", ");
268471
268625
  const ret = fn.returnType ? `: ${fn.returnType}` : "";
268472
268626
  const signature = `${fn.name}(${params})${ret}`;
268627
+ const isImplicitCall = language === "python" && fn.name.startsWith("__") && fn.name.endsWith("__") && fn.name !== "__init__" || language !== "python" && fn.name === "constructor" || void 0;
268473
268628
  return {
268474
268629
  name: fn.name,
268475
268630
  moduleName: moduleName2,
@@ -268482,7 +268637,8 @@ function toMethodInfo(fn, moduleName2, serviceName, filePath) {
268482
268637
  isExported: fn.isExported,
268483
268638
  lineCount: fn.lineCount,
268484
268639
  statementCount: fn.statementCount,
268485
- maxNestingDepth: fn.maxNestingDepth
268640
+ maxNestingDepth: fn.maxNestingDepth,
268641
+ isImplicitCall
268486
268642
  };
268487
268643
  }
268488
268644
  function deriveModuleName(analysis) {
@@ -268803,6 +268959,28 @@ function buildMethodDependencies(analyses2, allModules, allMethods, fileLookup)
268803
268959
  }
268804
268960
  }
268805
268961
  }
268962
+ if (!targetMethod && calleeParts.length === 1) {
268963
+ for (const mod2 of allModules) {
268964
+ if (mod2.kind !== "class" || mod2.serviceName !== serviceName)
268965
+ continue;
268966
+ if (mod2.name === calleeMethodName) {
268967
+ targetMethod = methodLookup.get(`${mod2.serviceName}::${mod2.name}::__init__`);
268968
+ if (targetMethod)
268969
+ break;
268970
+ }
268971
+ }
268972
+ if (!targetMethod) {
268973
+ for (const mod2 of allModules) {
268974
+ if (mod2.kind !== "class" || mod2.serviceName === serviceName)
268975
+ continue;
268976
+ if (mod2.name === calleeMethodName) {
268977
+ targetMethod = methodLookup.get(`${mod2.serviceName}::${mod2.name}::__init__`);
268978
+ if (targetMethod)
268979
+ break;
268980
+ }
268981
+ }
268982
+ }
268983
+ }
268806
268984
  if (!targetMethod)
268807
268985
  continue;
268808
268986
  if (!usedFallbackCaller && targetMethod.moduleName === callerMethod.moduleName && targetMethod.name === callerMethod.name && targetMethod.serviceName === callerMethod.serviceName && targetMethod.filePath === callerMethod.filePath)
@@ -270215,9 +270393,12 @@ var init_analysis_graph = __esm({
270215
270393
  }
270216
270394
  }
270217
270395
  }
270396
+ const isEntryCandidate = (name21) => !name21.startsWith("_") && name21 !== "constructor";
270218
270397
  this.entryPoints = [];
270219
270398
  const servicesWithEntryPoints = /* @__PURE__ */ new Set();
270220
270399
  for (const method of input.methods) {
270400
+ if (!isEntryCandidate(method.name))
270401
+ continue;
270221
270402
  const mod2 = this.moduleByKey.get(`${method.serviceName}::${method.moduleName}`);
270222
270403
  if (!mod2)
270223
270404
  continue;
@@ -270233,6 +270414,8 @@ var init_analysis_graph = __esm({
270233
270414
  for (const method of input.methods) {
270234
270415
  if (servicesWithEntryPoints.has(method.serviceName))
270235
270416
  continue;
270417
+ if (!isEntryCandidate(method.name))
270418
+ continue;
270236
270419
  if (!method.isExported)
270237
270420
  continue;
270238
270421
  const mod2 = this.moduleByKey.get(`${method.serviceName}::${method.moduleName}`);
@@ -270854,7 +271037,34 @@ var init_rule_engine = __esm({
270854
271037
  });
270855
271038
 
270856
271039
  // packages/analyzer/dist/rules/module-rules-checker.js
270857
- function checkModuleRules(modules2, methods2, fileDependencies, enabledRules, moduleLevelDeps, dbConnectedModuleKeys, fileAnalyses, libraryServiceNames, entryPointFiles) {
271040
+ function buildSameFileCalls(fileAnalyses) {
271041
+ const result = /* @__PURE__ */ new Map();
271042
+ if (!fileAnalyses)
271043
+ return result;
271044
+ for (const fa of fileAnalyses) {
271045
+ const callees = /* @__PURE__ */ new Set();
271046
+ for (const call of fa.calls) {
271047
+ if (!call.callee.includes(".")) {
271048
+ callees.add(call.callee);
271049
+ }
271050
+ if (call.arguments) {
271051
+ for (const arg of call.arguments) {
271052
+ if (/^[A-Za-z_]\w*$/.test(arg)) {
271053
+ callees.add(arg);
271054
+ }
271055
+ const kwMatch = arg.match(/^[A-Za-z_]\w*=([A-Za-z_]\w*)$/);
271056
+ if (kwMatch) {
271057
+ callees.add(kwMatch[1]);
271058
+ }
271059
+ }
271060
+ }
271061
+ }
271062
+ if (callees.size > 0)
271063
+ result.set(fa.filePath, callees);
271064
+ }
271065
+ return result;
271066
+ }
271067
+ function checkModuleRules(modules2, methods2, fileDependencies, enabledRules, moduleLevelDeps, dbConnectedModuleKeys, fileAnalyses, libraryServiceNames, entryPointFiles, methodLevelDeps) {
270858
271068
  const violations2 = [];
270859
271069
  const ruleKeys = new Set(enabledRules.filter((r) => r.type === "deterministic" && r.enabled).map((r) => r.key));
270860
271070
  if (ruleKeys.has("arch/god-module")) {
@@ -270872,6 +271082,40 @@ function checkModuleRules(modules2, methods2, fileDependencies, enabledRules, mo
270872
271082
  }
270873
271083
  }
270874
271084
  }
271085
+ const calledInOwnFile = buildSameFileCalls(fileAnalyses);
271086
+ const usedAsType = /* @__PURE__ */ new Set();
271087
+ function addType(typeStr2) {
271088
+ if (!typeStr2)
271089
+ return;
271090
+ const parts = typeStr2.split(/[|,\[\]()]+/).map((s) => s.trim()).filter(Boolean);
271091
+ for (const part of parts) {
271092
+ if (/^(str|int|float|bool|None|Any|string|number|void|undefined|null|Optional|Union|List|Dict|Set|Tuple|list|dict|set|tuple|type)$/i.test(part))
271093
+ continue;
271094
+ usedAsType.add(part);
271095
+ }
271096
+ }
271097
+ if (fileAnalyses) {
271098
+ for (const fa of fileAnalyses) {
271099
+ for (const fn of fa.functions) {
271100
+ for (const p of fn.params)
271101
+ addType(p.type);
271102
+ addType(fn.returnType);
271103
+ }
271104
+ for (const cls of fa.classes) {
271105
+ if (cls.superClass)
271106
+ usedAsType.add(cls.superClass);
271107
+ for (const iface of cls.interfaces || [])
271108
+ usedAsType.add(iface);
271109
+ for (const m of cls.methods) {
271110
+ for (const p of m.params)
271111
+ addType(p.type);
271112
+ addType(m.returnType);
271113
+ }
271114
+ for (const prop of cls.properties)
271115
+ addType(prop.type);
271116
+ }
271117
+ }
271118
+ }
270875
271119
  if (ruleKeys.has("arch/unused-export")) {
270876
271120
  const importedTargets = /* @__PURE__ */ new Set();
270877
271121
  for (const dep of fileDependencies) {
@@ -270879,6 +271123,12 @@ function checkModuleRules(modules2, methods2, fileDependencies, enabledRules, mo
270879
271123
  importedTargets.add(name21);
270880
271124
  }
270881
271125
  }
271126
+ if (methodLevelDeps) {
271127
+ for (const dep of methodLevelDeps) {
271128
+ importedTargets.add(dep.calleeModule);
271129
+ importedTargets.add(dep.calleeMethod);
271130
+ }
271131
+ }
270882
271132
  const classModuleNames = new Set(modules2.filter((m) => m.kind === "class").map((m) => m.name));
270883
271133
  const routeHandlerNames = /* @__PURE__ */ new Set();
270884
271134
  if (fileAnalyses) {
@@ -270899,6 +271149,8 @@ function checkModuleRules(modules2, methods2, fileDependencies, enabledRules, mo
270899
271149
  continue;
270900
271150
  if (routeHandlerNames.has(method.name))
270901
271151
  continue;
271152
+ if (calledInOwnFile.get(method.filePath)?.has(method.name))
271153
+ continue;
270902
271154
  violations2.push({
270903
271155
  ruleKey: "arch/unused-export",
270904
271156
  title: `Unused export: ${method.name}`,
@@ -270915,6 +271167,8 @@ function checkModuleRules(modules2, methods2, fileDependencies, enabledRules, mo
270915
271167
  if (mod2.kind === "class" && mod2.exportCount > 0 && !importedTargets.has(mod2.name)) {
270916
271168
  if (entryPointFiles?.has(mod2.filePath))
270917
271169
  continue;
271170
+ if (usedAsType.has(mod2.name))
271171
+ continue;
270918
271172
  violations2.push({
270919
271173
  ruleKey: "arch/unused-export",
270920
271174
  title: `Unused export: ${mod2.name}`,
@@ -270933,11 +271187,32 @@ function checkModuleRules(modules2, methods2, fileDependencies, enabledRules, mo
270933
271187
  connectedModules.add(`${dep.sourceService}::${dep.sourceModule}`);
270934
271188
  connectedModules.add(`${dep.targetService}::${dep.targetModule}`);
270935
271189
  }
271190
+ if (methodLevelDeps) {
271191
+ for (const dep of methodLevelDeps) {
271192
+ connectedModules.add(`${dep.callerService}::${dep.callerModule}`);
271193
+ connectedModules.add(`${dep.calleeService}::${dep.calleeModule}`);
271194
+ }
271195
+ }
271196
+ const moduleMethodNames = /* @__PURE__ */ new Map();
271197
+ for (const m of methods2) {
271198
+ const mKey = `${m.serviceName}::${m.moduleName}`;
271199
+ const arr = moduleMethodNames.get(mKey) || [];
271200
+ arr.push(m.name);
271201
+ moduleMethodNames.set(mKey, arr);
271202
+ }
270936
271203
  for (const mod2 of modules2) {
270937
271204
  const key = `${mod2.serviceName}::${mod2.name}`;
270938
271205
  if (!connectedModules.has(key) && !dbConnectedModuleKeys?.has(key)) {
270939
271206
  if (entryPointFiles?.has(mod2.filePath))
270940
271207
  continue;
271208
+ if (usedAsType.has(mod2.name))
271209
+ continue;
271210
+ const sameFileRefs = calledInOwnFile.get(mod2.filePath);
271211
+ if (sameFileRefs) {
271212
+ const modMethods = moduleMethodNames.get(key) || [];
271213
+ if (sameFileRefs.has(mod2.name) || modMethods.some((m) => sameFileRefs.has(m)))
271214
+ continue;
271215
+ }
270941
271216
  violations2.push({
270942
271217
  ruleKey: "arch/dead-module",
270943
271218
  title: `Dead module: ${mod2.name}`,
@@ -271035,9 +271310,10 @@ function checkModuleRules(modules2, methods2, fileDependencies, enabledRules, mo
271035
271310
  }
271036
271311
  return violations2;
271037
271312
  }
271038
- function checkMethodRules(methods2, enabledRules, methodLevelDeps, entryPointFiles) {
271313
+ function checkMethodRules(methods2, enabledRules, methodLevelDeps, entryPointFiles, fileAnalyses) {
271039
271314
  const violations2 = [];
271040
271315
  const ruleKeys = new Set(enabledRules.filter((r) => r.type === "deterministic" && r.enabled).map((r) => r.key));
271316
+ const calledInOwnFile = buildSameFileCalls(fileAnalyses);
271041
271317
  if (ruleKeys.has("arch/long-method")) {
271042
271318
  for (const method of methods2) {
271043
271319
  if (method.statementCount != null && method.statementCount > LONG_METHOD_STATEMENTS) {
@@ -271056,11 +271332,12 @@ function checkMethodRules(methods2, enabledRules, methodLevelDeps, entryPointFil
271056
271332
  }
271057
271333
  if (ruleKeys.has("arch/too-many-parameters")) {
271058
271334
  for (const method of methods2) {
271059
- if (method.paramCount >= TOO_MANY_PARAMS) {
271335
+ const paramThreshold = getMaxParameters(method.filePath);
271336
+ if (method.paramCount >= paramThreshold) {
271060
271337
  violations2.push({
271061
271338
  ruleKey: "arch/too-many-parameters",
271062
271339
  title: `Too many parameters: ${method.moduleName}.${method.name}`,
271063
- description: `${method.name} has ${method.paramCount} parameters (threshold: ${TOO_MANY_PARAMS}). Consider using an options object or splitting the function.`,
271340
+ description: `${method.name} has ${method.paramCount} parameters (threshold: ${paramThreshold}). Consider using an options object or splitting the function.`,
271064
271341
  severity: "low",
271065
271342
  serviceName: method.serviceName,
271066
271343
  moduleName: method.moduleName,
@@ -271097,6 +271374,10 @@ function checkMethodRules(methods2, enabledRules, methodLevelDeps, entryPointFil
271097
271374
  if (!connectedMethods.has(key)) {
271098
271375
  if (entryPointFiles?.has(method.filePath))
271099
271376
  continue;
271377
+ if (calledInOwnFile.get(method.filePath)?.has(method.name))
271378
+ continue;
271379
+ if (method.isImplicitCall)
271380
+ continue;
271100
271381
  violations2.push({
271101
271382
  ruleKey: "arch/dead-method",
271102
271383
  title: `Dead method: ${method.moduleName}.${method.name}`,
@@ -271112,13 +271393,13 @@ function checkMethodRules(methods2, enabledRules, methodLevelDeps, entryPointFil
271112
271393
  }
271113
271394
  return violations2;
271114
271395
  }
271115
- var GOD_MODULE_THRESHOLD, LONG_METHOD_STATEMENTS, TOO_MANY_PARAMS, DEEP_NESTING_THRESHOLD;
271396
+ var GOD_MODULE_THRESHOLD, LONG_METHOD_STATEMENTS, DEEP_NESTING_THRESHOLD;
271116
271397
  var init_module_rules_checker = __esm({
271117
271398
  "packages/analyzer/dist/rules/module-rules-checker.js"() {
271118
271399
  "use strict";
271400
+ init_language_config();
271119
271401
  GOD_MODULE_THRESHOLD = 15;
271120
271402
  LONG_METHOD_STATEMENTS = 30;
271121
- TOO_MANY_PARAMS = 5;
271122
271403
  DEEP_NESTING_THRESHOLD = 4;
271123
271404
  }
271124
271405
  });
@@ -271482,7 +271763,7 @@ var init_python5 = __esm({
271482
271763
  return null;
271483
271764
  const exceptIdx = children.indexOf(exceptKeyword);
271484
271765
  const colonIdx = children.indexOf(colon);
271485
- const hasCatchType = children.slice(exceptIdx + 1, colonIdx).some((c) => c.type === "identifier" || c.type === "as_pattern" || c.type === "dotted_name");
271766
+ const hasCatchType = children.slice(exceptIdx + 1, colonIdx).some((c) => c.type === "identifier" || c.type === "as_pattern" || c.type === "dotted_name" || c.type === "attribute" || c.type === "tuple");
271486
271767
  if (!hasCatchType) {
271487
271768
  return makeViolation(this.ruleKey, node, filePath, "high", "Bare except clause", "Bare `except:` catches all exceptions including KeyboardInterrupt and SystemExit. Use `except Exception:` instead.", sourceCode, "Replace `except:` with `except Exception:` or a more specific exception type.");
271488
271769
  }
@@ -271565,9 +271846,10 @@ var init_universal = __esm({
271565
271846
  nodeTypes: ["string", "template_string"],
271566
271847
  visit(node, filePath, sourceCode) {
271567
271848
  const text3 = node.text;
271568
- const value = text3.slice(1, -1);
271569
- if (value.length < 8)
271849
+ const stripped = text3.replace(/^[fFbBrRuU]*['"`]{1,3}|['"`]{1,3}$/g, "");
271850
+ if (stripped.length < 8)
271570
271851
  return null;
271852
+ const value = stripped;
271571
271853
  for (const pattern of SECRET_PATTERNS) {
271572
271854
  if (pattern.test(value)) {
271573
271855
  return makeViolation(this.ruleKey, node, filePath, "critical", "Hardcoded secret detected", "This string looks like a hardcoded API key, token, or password. Use environment variables instead.", sourceCode, "Move this secret to an environment variable and reference it via process.env.");
@@ -271575,6 +271857,9 @@ var init_universal = __esm({
271575
271857
  }
271576
271858
  const parent = node.parent;
271577
271859
  if (parent) {
271860
+ if (parent.type === "pair" && parent.childForFieldName("key") === node) {
271861
+ return null;
271862
+ }
271578
271863
  const varDeclarator = parent.type === "variable_declarator" ? parent : null;
271579
271864
  const assignment = parent.type === "assignment_expression" || parent.type === "assignment" ? parent : null;
271580
271865
  const propAssignment = parent.type === "pair" ? parent : null;
@@ -271582,7 +271867,9 @@ var init_universal = __esm({
271582
271867
  if (nameNode) {
271583
271868
  const name21 = nameNode.text.toLowerCase();
271584
271869
  const secretNames = ["password", "passwd", "secret", "apikey", "api_key", "token", "auth_token", "access_token", "private_key"];
271585
- if (secretNames.some((s) => name21.includes(s)) && value.length >= 8 && !/^(true|false|null|undefined|localhost|https?:\/\/)/.test(value)) {
271870
+ const isNonSecretName = /(?:uri|url|endpoint|type|scope|name|header|grant|method)/.test(name21);
271871
+ const isNonSecretValue = /https?:\/\//.test(value) || /^(true|false|null|undefined|localhost|None|True|False|Bearer)$/i.test(value) || /[[\]<>{}()#.=\s]/.test(value);
271872
+ if (secretNames.some((s) => name21.includes(s)) && value.length >= 8 && !isNonSecretName && !isNonSecretValue) {
271586
271873
  return makeViolation(this.ruleKey, node, filePath, "critical", "Hardcoded secret detected", `Variable "${nameNode.text}" contains what appears to be a hardcoded secret. Use environment variables instead.`, sourceCode, "Move this secret to an environment variable.");
271587
271874
  }
271588
271875
  }
@@ -271745,6 +272032,7 @@ __export(dist_exports, {
271745
272032
  getAllTestPatterns: () => getAllTestPatterns,
271746
272033
  getLanguageConfig: () => getLanguageConfig,
271747
272034
  getLspServerConfig: () => getLspServerConfig,
272035
+ getMaxParameters: () => getMaxParameters,
271748
272036
  getParser: () => getParser,
271749
272037
  hasLspServer: () => hasLspServer,
271750
272038
  isBootstrapEntry: () => isBootstrapEntry,
@@ -327578,10 +327866,26 @@ function buildFlowTemplateVars(context2) {
327578
327866
  stepList
327579
327867
  };
327580
327868
  }
327581
- function getLangfuse() {
327869
+ async function getLangfuse() {
327582
327870
  if (!(config.langfuse.publicKey && config.langfuse.secretKey)) {
327583
327871
  return null;
327584
327872
  }
327873
+ if (!langfuseChecked) {
327874
+ langfuseChecked = true;
327875
+ try {
327876
+ const res = await fetch(`${config.langfuse.baseUrl || "http://localhost:3001"}/api/public/health`, {
327877
+ signal: AbortSignal.timeout(3e3)
327878
+ });
327879
+ langfuseAvailable = res.ok;
327880
+ } catch {
327881
+ langfuseAvailable = false;
327882
+ }
327883
+ if (!langfuseAvailable) {
327884
+ console.log("[Langfuse] Unavailable \u2014 using local prompts for this session.");
327885
+ return null;
327886
+ }
327887
+ }
327888
+ if (!langfuseAvailable) return null;
327585
327889
  if (!langfuseInstance) {
327586
327890
  langfuseInstance = new Langfuse({
327587
327891
  publicKey: config.langfuse.publicKey,
@@ -327592,7 +327896,7 @@ function getLangfuse() {
327592
327896
  return langfuseInstance;
327593
327897
  }
327594
327898
  async function getPrompt(name21, variables) {
327595
- const langfuse = getLangfuse();
327899
+ const langfuse = await getLangfuse();
327596
327900
  const localDef = PROMPT_DEFINITIONS[name21];
327597
327901
  if (langfuse) {
327598
327902
  try {
@@ -327612,7 +327916,7 @@ async function getPrompt(name21, variables) {
327612
327916
  }
327613
327917
  return { text: text3, langfusePrompt: null };
327614
327918
  }
327615
- var PROMPT_DEFINITIONS, langfuseInstance;
327919
+ var PROMPT_DEFINITIONS, langfuseInstance, langfuseChecked, langfuseAvailable;
327616
327920
  var init_prompts = __esm({
327617
327921
  "apps/server/src/services/llm/prompts.ts"() {
327618
327922
  "use strict";
@@ -327937,6 +328241,8 @@ When suggesting improvements, provide actionable advice that could be passed to
327937
328241
  }
327938
328242
  };
327939
328243
  langfuseInstance = null;
328244
+ langfuseChecked = false;
328245
+ langfuseAvailable = false;
327940
328246
  }
327941
328247
  });
327942
328248
 
@@ -395890,7 +396196,9 @@ var MethodInfoSchema = external_exports.object({
395890
396196
  isExported: external_exports.boolean(),
395891
396197
  lineCount: external_exports.number().optional(),
395892
396198
  statementCount: external_exports.number().optional(),
395893
- maxNestingDepth: external_exports.number().optional()
396199
+ maxNestingDepth: external_exports.number().optional(),
396200
+ /** Method is called implicitly by the runtime (e.g., Python __init__, __str__, JS constructor) */
396201
+ isImplicitCall: external_exports.boolean().optional()
395894
396202
  });
395895
396203
  var ModuleLevelDependencySchema = external_exports.object({
395896
396204
  sourceModule: external_exports.string(),
@@ -396202,7 +396510,8 @@ var CreateRepoSchema = external_exports.object({
396202
396510
  });
396203
396511
  var AnalyzeRepoSchema = external_exports.object({
396204
396512
  branch: external_exports.string().optional(),
396205
- codeReview: external_exports.boolean().optional().default(false)
396513
+ codeReview: external_exports.boolean().optional().default(false),
396514
+ deterministicOnly: external_exports.boolean().optional().default(false)
396206
396515
  });
396207
396516
  var GenerateViolationsSchema = external_exports.object({
396208
396517
  analysisId: external_exports.string().uuid().optional()
@@ -400960,7 +401269,8 @@ function runDeterministicModuleChecks(result, enabledDeterministic) {
400960
401269
  dbConnectedModuleKeys,
400961
401270
  result.fileAnalyses,
400962
401271
  libraryServiceNames,
400963
- result.entryPointFiles
401272
+ result.entryPointFiles,
401273
+ result.methodLevelDependencies || []
400964
401274
  );
400965
401275
  }
400966
401276
  function runDeterministicMethodChecks(result, enabledDeterministic) {
@@ -401007,6 +401317,7 @@ async function runAnalysis(repoPath, _branch, onProgress, options) {
401007
401317
  const fileAnalyses = [];
401008
401318
  const totalFiles = files.length;
401009
401319
  for (let i = 0; i < totalFiles; i++) {
401320
+ if (options?.signal?.aborted) throw new DOMException("Analysis cancelled", "AbortError");
401010
401321
  const file2 = files[i];
401011
401322
  try {
401012
401323
  const analysis = await analyzer.analyzeFile(file2);
@@ -401146,14 +401457,26 @@ init_drizzle_orm();
401146
401457
  init_database();
401147
401458
  init_schema2();
401148
401459
  async function persistAnalysisResult(params) {
401149
- const { repoId, branch, result, metadata, commitHash } = params;
401150
- const [analysis] = await db.insert(analyses).values({
401151
- repoId,
401152
- branch: branch || null,
401153
- architecture: result.architecture,
401154
- metadata: metadata ?? result.metadata,
401155
- commitHash: commitHash || null
401156
- }).returning();
401460
+ const { repoId, branch, result, metadata, commitHash, existingAnalysisId } = params;
401461
+ let analysis;
401462
+ if (existingAnalysisId) {
401463
+ const [updated] = await db.update(analyses).set({
401464
+ status: "completed",
401465
+ architecture: result.architecture,
401466
+ metadata: metadata ?? result.metadata
401467
+ }).where(eq(analyses.id, existingAnalysisId)).returning();
401468
+ analysis = updated;
401469
+ } else {
401470
+ const [created] = await db.insert(analyses).values({
401471
+ repoId,
401472
+ branch: branch || null,
401473
+ status: "completed",
401474
+ architecture: result.architecture,
401475
+ metadata: metadata ?? result.metadata,
401476
+ commitHash: commitHash || null
401477
+ }).returning();
401478
+ analysis = created;
401479
+ }
401157
401480
  const serviceIdMap = /* @__PURE__ */ new Map();
401158
401481
  for (const svc of result.services) {
401159
401482
  const [saved] = await db.insert(services).values({
@@ -402643,6 +402966,7 @@ async function runViolationPipeline(input) {
402643
402966
  tracker,
402644
402967
  provider: externalProvider,
402645
402968
  includeCodeReview,
402969
+ deterministicOnly,
402646
402970
  signal
402647
402971
  } = input;
402648
402972
  const allRules = await getEnabledRules();
@@ -402687,7 +403011,7 @@ async function runViolationPipeline(input) {
402687
403011
  detViolationIdMap.set(`${category}::${v.ruleKey}::${v.serviceName}::${v.title}`, row.id);
402688
403012
  }
402689
403013
  const enabledCodeRules = allRules.filter((r) => r.category === "code" && r.type === "deterministic");
402690
- const enabledLlmCodeRules = includeCodeReview ? allRules.filter((r) => r.category === "code" && r.type === "llm" && r.prompt) : [];
403014
+ const enabledLlmCodeRules = includeCodeReview && !deterministicOnly ? allRules.filter((r) => r.category === "code" && r.type === "llm" && r.prompt) : [];
402691
403015
  const allCodeViolations = [];
402692
403016
  const fileContents = /* @__PURE__ */ new Map();
402693
403017
  const filesToScan = changedFileSet ? [...changedFileSet].map((relPath) => ({ filePath: relPath, resolve: true })) : (result.fileAnalyses || []).map((fa) => ({ filePath: fa.filePath, resolve: !path6.isAbsolute(fa.filePath) }));
@@ -402823,12 +403147,14 @@ async function runViolationPipeline(input) {
402823
403147
  }
402824
403148
  const architectureContext = `Architecture: ${result.architecture}
402825
403149
  Services: ${result.services.map((s) => `${s.name} (${s.type})`).join(", ")}`;
402826
- const provider = externalProvider ?? createLLMProvider();
403150
+ const provider = deterministicOnly ? void 0 : externalProvider ?? createLLMProvider();
402827
403151
  const now2 = /* @__PURE__ */ new Date();
402828
403152
  const allNewViolations = [];
402829
403153
  const allResolvedViolationIds = [];
402830
- tracker?.start("enrich", `Enriching ${allDetEntries.length} detections...`);
402831
- tracker?.start("architecture");
403154
+ if (!deterministicOnly) {
403155
+ tracker?.start("enrich", `Enriching ${allDetEntries.length} detections...`);
403156
+ tracker?.start("architecture");
403157
+ }
402832
403158
  const deterministicPromise = (async () => {
402833
403159
  let detectionsToEnrich;
402834
403160
  if (previousDeterministicViolations.length > 0) {
@@ -402891,62 +403217,101 @@ Services: ${result.services.map((s) => `${s.name} (${s.type})`).join(", ")}`;
402891
403217
  detectionsToEnrich = allDetEntries;
402892
403218
  }
402893
403219
  if (detectionsToEnrich.length > 0) {
402894
- const detByViolationId = new Map(detectionsToEnrich.map((d) => [d.detViolationId, d]));
402895
- const enrichmentResult = await provider.enrichDeterministicViolations(
402896
- detectionsToEnrich.map((d) => ({
402897
- id: d.detViolationId,
402898
- ruleKey: d.ruleKey,
402899
- title: d.title,
402900
- description: d.description,
402901
- severity: d.severity,
402902
- category: d.category,
402903
- serviceName: d.serviceName,
402904
- moduleName: d.moduleName,
402905
- methodName: d.methodName
402906
- })),
402907
- architectureContext
402908
- );
402909
- for (const enriched of enrichmentResult.enrichedViolations) {
402910
- const det = detByViolationId.get(enriched.id);
402911
- if (!det) continue;
402912
- const violationId = v4_default();
402913
- await db.insert(violations).values({
402914
- id: violationId,
402915
- repoId,
402916
- analysisId,
402917
- type: det.violationType,
402918
- title: enriched.title,
402919
- content: enriched.content,
402920
- severity: det.severity,
402921
- status: "new",
402922
- targetServiceId: det.targetServiceId,
402923
- targetModuleId: det.targetModuleId,
402924
- targetMethodId: det.targetMethodId,
402925
- fixPrompt: enriched.fixPrompt,
402926
- ruleKey: det.ruleKey,
402927
- deterministicViolationId: det.detViolationId,
402928
- firstSeenAnalysisId: analysisId,
402929
- firstSeenAt: now2
402930
- });
402931
- allNewViolations.push({
402932
- type: det.violationType,
402933
- title: enriched.title,
402934
- content: enriched.content,
402935
- severity: det.severity,
402936
- targetServiceId: det.targetServiceId,
402937
- targetModuleId: det.targetModuleId,
402938
- targetMethodId: det.targetMethodId,
402939
- targetServiceName: det.serviceName || null,
402940
- targetModuleName: det.moduleName || null,
402941
- targetMethodName: det.methodName || null,
402942
- fixPrompt: enriched.fixPrompt,
402943
- ruleKey: det.ruleKey
402944
- });
403220
+ if (deterministicOnly) {
403221
+ for (const det of detectionsToEnrich) {
403222
+ const violationId = v4_default();
403223
+ await db.insert(violations).values({
403224
+ id: violationId,
403225
+ repoId,
403226
+ analysisId,
403227
+ type: det.violationType,
403228
+ title: det.title,
403229
+ content: det.description,
403230
+ severity: det.severity,
403231
+ status: "new",
403232
+ targetServiceId: det.targetServiceId,
403233
+ targetModuleId: det.targetModuleId,
403234
+ targetMethodId: det.targetMethodId,
403235
+ fixPrompt: null,
403236
+ ruleKey: det.ruleKey,
403237
+ deterministicViolationId: det.detViolationId,
403238
+ firstSeenAnalysisId: analysisId,
403239
+ firstSeenAt: now2
403240
+ });
403241
+ allNewViolations.push({
403242
+ type: det.violationType,
403243
+ title: det.title,
403244
+ content: det.description,
403245
+ severity: det.severity,
403246
+ targetServiceId: det.targetServiceId,
403247
+ targetModuleId: det.targetModuleId,
403248
+ targetMethodId: det.targetMethodId,
403249
+ targetServiceName: det.serviceName || null,
403250
+ targetModuleName: det.moduleName || null,
403251
+ targetMethodName: det.methodName || null,
403252
+ fixPrompt: null,
403253
+ ruleKey: det.ruleKey
403254
+ });
403255
+ }
403256
+ emitProgress(88, "Deterministic violations persisted");
403257
+ } else {
403258
+ const detByViolationId = new Map(detectionsToEnrich.map((d) => [d.detViolationId, d]));
403259
+ const enrichmentResult = await provider.enrichDeterministicViolations(
403260
+ detectionsToEnrich.map((d) => ({
403261
+ id: d.detViolationId,
403262
+ ruleKey: d.ruleKey,
403263
+ title: d.title,
403264
+ description: d.description,
403265
+ severity: d.severity,
403266
+ category: d.category,
403267
+ serviceName: d.serviceName,
403268
+ moduleName: d.moduleName,
403269
+ methodName: d.methodName
403270
+ })),
403271
+ architectureContext
403272
+ );
403273
+ for (const enriched of enrichmentResult.enrichedViolations) {
403274
+ const det = detByViolationId.get(enriched.id);
403275
+ if (!det) continue;
403276
+ const violationId = v4_default();
403277
+ await db.insert(violations).values({
403278
+ id: violationId,
403279
+ repoId,
403280
+ analysisId,
403281
+ type: det.violationType,
403282
+ title: enriched.title,
403283
+ content: enriched.content,
403284
+ severity: det.severity,
403285
+ status: "new",
403286
+ targetServiceId: det.targetServiceId,
403287
+ targetModuleId: det.targetModuleId,
403288
+ targetMethodId: det.targetMethodId,
403289
+ fixPrompt: enriched.fixPrompt,
403290
+ ruleKey: det.ruleKey,
403291
+ deterministicViolationId: det.detViolationId,
403292
+ firstSeenAnalysisId: analysisId,
403293
+ firstSeenAt: now2
403294
+ });
403295
+ allNewViolations.push({
403296
+ type: det.violationType,
403297
+ title: enriched.title,
403298
+ content: enriched.content,
403299
+ severity: det.severity,
403300
+ targetServiceId: det.targetServiceId,
403301
+ targetModuleId: det.targetModuleId,
403302
+ targetMethodId: det.targetMethodId,
403303
+ targetServiceName: det.serviceName || null,
403304
+ targetModuleName: det.moduleName || null,
403305
+ targetMethodName: det.methodName || null,
403306
+ fixPrompt: enriched.fixPrompt,
403307
+ ruleKey: det.ruleKey
403308
+ });
403309
+ }
403310
+ tracker?.done("enrich", `${enrichmentResult.enrichedViolations.length} violations enriched`);
403311
+ emitProgress(88, "Deterministic violations enriched");
402945
403312
  }
402946
- tracker?.done("enrich", `${enrichmentResult.enrichedViolations.length} violations enriched`);
402947
- emitProgress(88, "Deterministic violations enriched");
402948
403313
  } else {
402949
- tracker?.done("enrich", "No detections to enrich");
403314
+ if (!deterministicOnly) tracker?.done("enrich", "No detections to enrich");
402950
403315
  }
402951
403316
  })();
402952
403317
  const enabledLlmRules = allRules.filter((r) => r.type === "llm" && r.prompt && r.category !== "code").map((r) => ({ key: r.key, name: r.name, severity: r.severity, prompt: r.prompt, category: r.category }));
@@ -403046,13 +403411,13 @@ Services: ${result.services.map((s) => `${s.name} (${s.type})`).join(", ")}`;
403046
403411
  existingDatabaseViolations: hasLlmOnlyExistingViolations ? existingDatabaseViolations : void 0,
403047
403412
  existingModuleViolations: hasLlmOnlyExistingViolations ? existingModuleViolations : void 0
403048
403413
  };
403049
- const llmCodePromise = llmCodeBatches.length > 0 ? provider.generateAllCodeViolations(llmCodeBatches) : Promise.resolve({ violations: [] });
403414
+ const llmCodePromise = llmCodeBatches.length > 0 && !deterministicOnly ? provider.generateAllCodeViolations(llmCodeBatches) : Promise.resolve({ violations: [] });
403050
403415
  let serviceDescriptions = [];
403051
403416
  let llmCodeResolvedIds = [];
403052
403417
  let llmCodeUnchangedIds = [];
403053
- emitProgress(86, "Analyzing architecture & modules...");
403418
+ if (!deterministicOnly) emitProgress(86, "Analyzing architecture & modules...");
403054
403419
  let llmStepCount = 0;
403055
- const llmRulePromise = (async () => {
403420
+ const llmRulePromise = deterministicOnly ? Promise.resolve() : (async () => {
403056
403421
  if (hasLlmOnlyExistingViolations) {
403057
403422
  const archResult = await generateViolationsWithLifecycle(violationInput, (step) => {
403058
403423
  llmStepCount++;
@@ -403124,6 +403489,7 @@ Services: ${result.services.map((s) => `${s.name} (${s.type})`).join(", ")}`;
403124
403489
  });
403125
403490
  }
403126
403491
  }
403492
+ tracker?.done("architecture");
403127
403493
  })();
403128
403494
  const codePromise = llmCodePromise;
403129
403495
  const [detResult, llmResult] = await Promise.allSettled([deterministicPromise, llmRulePromise]);
@@ -403137,9 +403503,6 @@ Services: ${result.services.map((s) => `${s.name} (${s.type})`).join(", ")}`;
403137
403503
  log2(`[Violations] LLM rule analysis failed: ${msg}`);
403138
403504
  tracker?.error("architecture", `Failed: ${msg.slice(0, 80)}`);
403139
403505
  }
403140
- if (llmResult.status === "fulfilled") {
403141
- tracker?.done("architecture");
403142
- }
403143
403506
  throwIfAborted(signal);
403144
403507
  tracker?.start("persist", "Saving results...");
403145
403508
  emitProgress(95, "Analysis complete");
@@ -403473,7 +403836,7 @@ router2.post(
403473
403836
  if (!parsed.success) {
403474
403837
  throw createAppError("Invalid request body", 400);
403475
403838
  }
403476
- const { codeReview: includeCodeReview } = parsed.data;
403839
+ const { codeReview: includeCodeReview, deterministicOnly } = parsed.data;
403477
403840
  const [repo] = await db.select().from(repos).where(eq(repos.id, id)).limit(1);
403478
403841
  if (!repo) {
403479
403842
  throw createAppError("Repo not found", 404);
@@ -403481,22 +403844,31 @@ router2.post(
403481
403844
  const git = await getGit(repo.path);
403482
403845
  const branch = (await git.branch()).current || null;
403483
403846
  const commitHash = (await git.revparse(["HEAD"])).trim();
403484
- res.status(202).json({ message: "Analysis started", repoId: id, branch });
403485
- const abortController = registerAnalysis(id, "pending");
403847
+ const [runningAnalysis] = await db.insert(analyses).values({
403848
+ repoId: id,
403849
+ branch,
403850
+ status: "running",
403851
+ architecture: "unknown",
403852
+ commitHash
403853
+ }).returning();
403854
+ res.status(202).json({ message: "Analysis started", repoId: id, branch, analysisId: runningAnalysis.id });
403855
+ const abortController = registerAnalysis(id, runningAnalysis.id);
403486
403856
  try {
403487
403857
  const trackerSteps = [
403488
403858
  { key: "parse", label: "Parsing repository" },
403489
403859
  { key: "detect", label: "Deterministic checks" },
403490
- { key: "enrich", label: "Enriching detections" },
403491
- { key: "architecture", label: "Architecture analysis" },
403860
+ ...!deterministicOnly ? [
403861
+ { key: "enrich", label: "Enriching detections" },
403862
+ { key: "architecture", label: "Architecture analysis" }
403863
+ ] : [],
403492
403864
  { key: "persist", label: "Saving results" },
403493
- ...includeCodeReview ? [{ key: "code-review", label: "Code review" }] : []
403865
+ ...!deterministicOnly && includeCodeReview ? [{ key: "code-review", label: "Code review" }] : []
403494
403866
  ];
403495
403867
  const tracker = new StepTracker(id, trackerSteps);
403496
403868
  tracker.start("parse", "Starting analysis...");
403497
403869
  const result = await runAnalysis(repo.path, branch ?? void 0, (progress) => {
403498
403870
  tracker.detail("parse", progress.detail ?? "Analyzing...");
403499
- });
403871
+ }, { signal: abortController.signal });
403500
403872
  const prevConditions = [eq(analyses.repoId, id), notDiffAnalysis2];
403501
403873
  if (branch) prevConditions.push(eq(analyses.branch, branch));
403502
403874
  const prevAnalyses = await db.select().from(analyses).where(and(...prevConditions)).orderBy(desc(analyses.createdAt)).limit(1);
@@ -403520,7 +403892,13 @@ router2.post(
403520
403892
  for (const da of oldDiffAnalyses) {
403521
403893
  await db.delete(analyses).where(eq(analyses.id, da.id));
403522
403894
  }
403523
- const { analysisId: newAnalysisId, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = await persistAnalysisResult({ repoId: id, branch, result, commitHash, metadata: { codeReview: includeCodeReview } });
403895
+ if (abortController.signal.aborted) {
403896
+ await db.update(analyses).set({ status: "cancelled" }).where(eq(analyses.id, runningAnalysis.id));
403897
+ emitAnalysisCanceled(id);
403898
+ unregisterAnalysis(id);
403899
+ return;
403900
+ }
403901
+ const { analysisId: newAnalysisId, serviceIdMap, moduleIdMap, methodIdMap, dbIdMap } = await persistAnalysisResult({ repoId: id, branch, result, commitHash, metadata: { codeReview: includeCodeReview, deterministicOnly }, existingAnalysisId: runningAnalysis.id });
403524
403902
  const analysis = { id: newAnalysisId };
403525
403903
  const prevConditionsForLifecycle = [eq(analyses.repoId, id), notDiffAnalysis2];
403526
403904
  if (branch) prevConditionsForLifecycle.push(eq(analyses.branch, branch));
@@ -403638,10 +404016,12 @@ router2.post(
403638
404016
  }
403639
404017
  }
403640
404018
  tracker.done("parse", `${result.services.length} services, ${result.fileAnalyses?.length ?? 0} files`);
403641
- const provider = createLLMProvider();
403642
- provider.setAnalysisId(newAnalysisId);
403643
- provider.setRepoId(id);
403644
- provider.setAbortSignal(abortController.signal);
404019
+ const provider = deterministicOnly ? void 0 : createLLMProvider();
404020
+ if (provider) {
404021
+ provider.setAnalysisId(newAnalysisId);
404022
+ provider.setRepoId(id);
404023
+ provider.setAbortSignal(abortController.signal);
404024
+ }
403645
404025
  let codeReviewPromise = null;
403646
404026
  try {
403647
404027
  const pipelineResult = await runViolationPipeline({
@@ -403658,7 +404038,8 @@ router2.post(
403658
404038
  previousDeterministicViolations,
403659
404039
  changedFileSet,
403660
404040
  tracker,
403661
- includeCodeReview,
404041
+ includeCodeReview: deterministicOnly ? false : includeCodeReview,
404042
+ deterministicOnly,
403662
404043
  provider,
403663
404044
  signal: abortController.signal
403664
404045
  });
@@ -403676,7 +404057,7 @@ router2.post(
403676
404057
  emitViolationsReady(id, analysis.id);
403677
404058
  }
403678
404059
  try {
403679
- await provider.flushUsage();
404060
+ await provider?.flushUsage();
403680
404061
  } catch (usageError) {
403681
404062
  console.error("[Usage] Failed to record usage:", usageError instanceof Error ? usageError.message : String(usageError));
403682
404063
  }
@@ -403689,7 +404070,7 @@ router2.post(
403689
404070
  if (codeReviewPromise) {
403690
404071
  console.log(`[CodeReview] Started background code review for repo ${id}`);
403691
404072
  emitCodeReviewProgress(id, analysis.id);
403692
- codeReviewPromise.then(() => provider.flushUsage()).then(() => {
404073
+ codeReviewPromise.then(() => provider?.flushUsage()).then(() => {
403693
404074
  console.log(`[CodeReview] Completed for repo ${id}`);
403694
404075
  emitCodeReviewReady(id, analysis.id);
403695
404076
  }).catch((err) => {
@@ -403706,11 +404087,14 @@ router2.post(
403706
404087
  } catch (error40) {
403707
404088
  if (error40 instanceof DOMException && error40.name === "AbortError") {
403708
404089
  console.log(`[Analysis] Cancelled for repo ${id}`);
404090
+ await db.update(analyses).set({ status: "cancelled" }).where(eq(analyses.id, runningAnalysis.id));
404091
+ emitAnalysisCanceled(id);
403709
404092
  } else {
403710
404093
  console.error(
403711
404094
  `[Analysis] Failed for repo ${id}:`,
403712
404095
  error40 instanceof Error ? error40.message : String(error40)
403713
404096
  );
404097
+ await db.update(analyses).set({ status: "failed" }).where(eq(analyses.id, runningAnalysis.id));
403714
404098
  emitAnalysisProgress(id, {
403715
404099
  step: "error",
403716
404100
  percent: -1,
@@ -403731,11 +404115,9 @@ router2.post(
403731
404115
  const id = req.params.id;
403732
404116
  const canceled = cancelAnalysis(id);
403733
404117
  if (canceled) {
403734
- emitAnalysisCanceled(id);
403735
- res.json({ message: "Analysis cancelled" });
403736
- } else {
403737
- throw createAppError("No active analysis for this repo", 404);
404118
+ await db.update(analyses).set({ status: "cancelling" }).where(and(eq(analyses.repoId, id), eq(analyses.status, "running")));
403738
404119
  }
404120
+ res.json({ message: canceled ? "Analysis cancelling" : "No active analysis" });
403739
404121
  } catch (error40) {
403740
404122
  next(error40);
403741
404123
  }
@@ -403809,6 +404191,7 @@ router2.get(
403809
404191
  }
403810
404192
  const analysisList = await db.select({
403811
404193
  id: analyses.id,
404194
+ status: analyses.status,
403812
404195
  branch: analyses.branch,
403813
404196
  commitHash: analyses.commitHash,
403814
404197
  architecture: analyses.architecture,