technical-debt-radar 1.0.2 → 1.0.3

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.
Files changed (2) hide show
  1. package/dist/index.js +418 -74
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ var require_constants = __commonJS({
39
39
  "../../packages/shared/dist/constants/index.js"(exports2) {
40
40
  "use strict";
41
41
  Object.defineProperty(exports2, "__esModule", { value: true });
42
- exports2.DEBT_DELTA_WARN_THRESHOLD = exports2.DEBT_DELTA_BLOCK_THRESHOLD = exports2.DEFAULT_COMPLEXITY_THRESHOLD = exports2.MAX_CHANGED_FILES_PER_PR = exports2.FIRST_SCAN_TIMEOUT_MS = exports2.PR_ANALYSIS_TIMEOUT_MS = exports2.MAX_AI_OUTPUT_TOKENS = exports2.MAX_AI_TOKENS_PER_FUNCTION = exports2.MAX_SUSPECT_FUNCTIONS_PER_PR = exports2.MAX_CALLER_TRACE_DEPTH = exports2.MAX_CROSS_FILE_SUSPECTS = exports2.CROSS_FILE_RULES = exports2.ARCHITECTURE_RULES = exports2.MAINTAINABILITY_RULES = exports2.PERFORMANCE_RULES = exports2.RELIABILITY_RULES = exports2.RUNTIME_RISK_RULES = exports2.VOLUME_THRESHOLDS = exports2.DEFAULT_SCORING = exports2.DEFAULT_MODE = void 0;
42
+ exports2.DEBT_DELTA_WARN_THRESHOLD = exports2.DEBT_DELTA_BLOCK_THRESHOLD = exports2.DEFAULT_LARGE_FILE_THRESHOLD = exports2.DEFAULT_COMPLEXITY_THRESHOLD = exports2.MAX_CHANGED_FILES_PER_PR = exports2.FIRST_SCAN_TIMEOUT_MS = exports2.PR_ANALYSIS_TIMEOUT_MS = exports2.MAX_AI_OUTPUT_TOKENS = exports2.MAX_AI_TOKENS_PER_FUNCTION = exports2.MAX_SUSPECT_FUNCTIONS_PER_PR = exports2.MAX_CALLER_TRACE_DEPTH = exports2.MAX_CROSS_FILE_SUSPECTS = exports2.CROSS_FILE_RULES = exports2.ARCHITECTURE_RULES = exports2.MAINTAINABILITY_RULES = exports2.PERFORMANCE_RULES = exports2.RELIABILITY_RULES = exports2.RUNTIME_RISK_RULES = exports2.VOLUME_THRESHOLDS = exports2.DEFAULT_SCORING = exports2.DEFAULT_MODE = void 0;
43
43
  exports2.DEFAULT_MODE = "warn";
44
44
  exports2.DEFAULT_SCORING = {
45
45
  architecture_violation: 5,
@@ -97,10 +97,12 @@ var require_constants = __commonJS({
97
97
  FETCH_ALL_FILTER_MEMORY: "fetch-all-filter-in-memory",
98
98
  MISSING_PAGINATION: "missing-pagination-endpoint",
99
99
  UNFILTERED_COUNT: "unfiltered-count-large-table",
100
- RAW_SQL_NO_LIMIT: "raw-sql-no-limit"
100
+ RAW_SQL_NO_LIMIT: "raw-sql-no-limit",
101
+ RAW_SQL_UNSAFE: "raw-sql-unsafe"
101
102
  };
102
103
  exports2.MAINTAINABILITY_RULES = {
103
104
  HIGH_COMPLEXITY: "high-complexity",
105
+ LARGE_FILE: "large-file",
104
106
  CODE_DUPLICATION: "code-duplication",
105
107
  MISSING_TEST_FILE: "missing-test-file",
106
108
  UNUSED_EXPORT: "unused-export",
@@ -131,6 +133,7 @@ var require_constants = __commonJS({
131
133
  exports2.FIRST_SCAN_TIMEOUT_MS = 9e5;
132
134
  exports2.MAX_CHANGED_FILES_PER_PR = 100;
133
135
  exports2.DEFAULT_COMPLEXITY_THRESHOLD = 10;
136
+ exports2.DEFAULT_LARGE_FILE_THRESHOLD = 300;
134
137
  exports2.DEBT_DELTA_BLOCK_THRESHOLD = 15;
135
138
  exports2.DEBT_DELTA_WARN_THRESHOLD = 8;
136
139
  }
@@ -495,6 +498,7 @@ var require_parser = __commonJS({
495
498
  const exceptions = parseExceptions(raw.exceptions);
496
499
  const standards = parseStandards(raw.standards);
497
500
  const scoring = parseScoring(raw.scoring);
501
+ const shared_infrastructure = parseSharedInfrastructure(raw.shared_infrastructure);
498
502
  const result = {
499
503
  mode,
500
504
  stack,
@@ -507,7 +511,8 @@ var require_parser = __commonJS({
507
511
  gates,
508
512
  exceptions,
509
513
  standards,
510
- scoring
514
+ scoring,
515
+ shared_infrastructure
511
516
  };
512
517
  if (architecture !== void 0) {
513
518
  result.architecture = architecture;
@@ -845,6 +850,19 @@ var require_parser = __commonJS({
845
850
  }
846
851
  return result;
847
852
  }
853
+ function parseSharedInfrastructure(value) {
854
+ if (value === void 0 || value === null)
855
+ return [];
856
+ if (!Array.isArray(value)) {
857
+ throw new PolicyValidationError("shared_infrastructure must be an array of folder names");
858
+ }
859
+ return value.map((item, i) => {
860
+ if (typeof item !== "string") {
861
+ throw new PolicyValidationError(`shared_infrastructure[${i}] must be a string`);
862
+ }
863
+ return item;
864
+ });
865
+ }
848
866
  function parseScoring(value) {
849
867
  if (value === void 0 || value === null)
850
868
  return {};
@@ -7489,6 +7507,10 @@ var require_validator = __commonJS({
7489
7507
  items: { type: "string", enum: ["ddd", "hexagonal", "clean", "layered", "mvc", "event-driven", "feature-module"] }
7490
7508
  }
7491
7509
  ]
7510
+ },
7511
+ shared_infrastructure: {
7512
+ type: "array",
7513
+ items: { type: "string" }
7492
7514
  }
7493
7515
  },
7494
7516
  additionalProperties: false
@@ -9531,7 +9553,8 @@ var require_compiler = __commonJS({
9531
9553
  exceptions,
9532
9554
  standards: config.standards,
9533
9555
  scoring,
9534
- architecture: config.architecture
9556
+ architecture: config.architecture,
9557
+ sharedInfrastructure: config.shared_infrastructure ?? []
9535
9558
  };
9536
9559
  }
9537
9560
  function compileLayers(layers) {
@@ -10058,9 +10081,9 @@ var require_layered = __commonJS({
10058
10081
  gates: shared_rules_1.DEFAULT_GATES,
10059
10082
  scoring: shared_rules_1.DEFAULT_PRESET_SCORING,
10060
10083
  layerMapping: {
10061
- presentation: "api",
10062
- business: "application",
10063
- "data-access": "infrastructure"
10084
+ presentation: ["api", "controllers", "routes", "handlers", "web", "rest", "graphql"],
10085
+ business: ["application", "services", "use-cases", "usecases", "domain"],
10086
+ "data-access": ["infrastructure", "repositories", "persistence", "data", "dal", "db"]
10064
10087
  }
10065
10088
  };
10066
10089
  }
@@ -10507,7 +10530,13 @@ var require_project_detector = __commonJS({
10507
10530
  } catch {
10508
10531
  return [];
10509
10532
  }
10510
- return Array.from(found.values());
10533
+ const uniqueByName = /* @__PURE__ */ new Map();
10534
+ for (const layer of found.values()) {
10535
+ if (!uniqueByName.has(layer.name)) {
10536
+ uniqueByName.set(layer.name, layer);
10537
+ }
10538
+ }
10539
+ return Array.from(uniqueByName.values());
10511
10540
  }
10512
10541
  async function scanForLayers(dirPath, relativePath, found) {
10513
10542
  let entries;
@@ -10618,7 +10647,7 @@ var require_project_detector = __commonJS({
10618
10647
  for (const [pattern, scoreMap] of Object.entries(SCORING_TABLE)) {
10619
10648
  if (detectedPatterns.has(pattern)) {
10620
10649
  for (const [arch, points] of Object.entries(scoreMap)) {
10621
- scores[arch] += points;
10650
+ scores[arch] += points ?? 0;
10622
10651
  }
10623
10652
  }
10624
10653
  }
@@ -11519,9 +11548,12 @@ var require_rules_generator = __commonJS({
11519
11548
  if (placeholder === "contracts_path")
11520
11549
  return null;
11521
11550
  if (presetMapping && placeholder in presetMapping) {
11522
- const mappedName = presetMapping[placeholder];
11523
- if (layerMap.has(mappedName)) {
11524
- return layerMap.get(mappedName);
11551
+ const mapped = presetMapping[placeholder];
11552
+ const candidates = Array.isArray(mapped) ? mapped : [mapped];
11553
+ for (const candidate of candidates) {
11554
+ if (layerMap.has(candidate)) {
11555
+ return layerMap.get(candidate);
11556
+ }
11525
11557
  }
11526
11558
  }
11527
11559
  if (layerMap.has(placeholder)) {
@@ -12567,7 +12599,11 @@ var require_import_graph = __commonJS({
12567
12599
  const specifier = decl.getModuleSpecifierValue();
12568
12600
  const resolved = resolveModuleSpecifier(sourcePath, specifier, project);
12569
12601
  if (resolved) {
12570
- edges.push(buildEdge(sourcePath, resolved, "static", policy));
12602
+ const isTypeOnly = decl.isTypeOnly();
12603
+ const edge = buildEdge(sourcePath, resolved, "static", policy);
12604
+ if (isTypeOnly)
12605
+ edge.typeOnly = true;
12606
+ edges.push(edge);
12571
12607
  }
12572
12608
  }
12573
12609
  for (const decl of sourceFile.getExportDeclarations()) {
@@ -12762,10 +12798,14 @@ var require_boundary_checker = __commonJS({
12762
12798
  const patterns = Array.isArray(policy.architecture) ? policy.architecture : [policy.architecture];
12763
12799
  return patterns.includes("feature-module");
12764
12800
  }
12765
- function isFeatureModuleExemptImport(sourcePath, targetPath) {
12801
+ function isNestJSFramework(policy) {
12802
+ return /nestjs/i.test(policy.stack.framework);
12803
+ }
12804
+ var suffixPattern = (suffix) => new RegExp(`\\.${suffix}(\\.(ts|tsx|js|jsx))?$`);
12805
+ var NESTJS_SHARED_INFRA_FOLDERS = ["guards", "decorators", "interceptors", "pipes", "filters", "middleware", "shared"];
12806
+ function isNestJSExemptImport(sourcePath, targetPath, customSharedFolders = []) {
12766
12807
  const srcBase = sourcePath.replace(/\\/g, "/");
12767
12808
  const tgtBase = targetPath.replace(/\\/g, "/");
12768
- const suffixPattern = (suffix) => new RegExp(`\\.${suffix}(\\.(ts|tsx|js|jsx))?$`);
12769
12809
  if (suffixPattern("module").test(srcBase))
12770
12810
  return true;
12771
12811
  if (suffixPattern("module").test(tgtBase))
@@ -12776,10 +12816,24 @@ var require_boundary_checker = __commonJS({
12776
12816
  return true;
12777
12817
  if (suffixPattern("decorator").test(tgtBase))
12778
12818
  return true;
12779
- if (/\/dto\//.test(tgtBase) || suffixPattern("dto").test(tgtBase))
12819
+ if (suffixPattern("guard").test(tgtBase))
12820
+ return true;
12821
+ if (suffixPattern("interceptor").test(tgtBase))
12780
12822
  return true;
12781
- if (/\/shared\//.test(tgtBase))
12823
+ if (suffixPattern("pipe").test(tgtBase))
12782
12824
  return true;
12825
+ if (suffixPattern("filter").test(tgtBase))
12826
+ return true;
12827
+ if (/\/dto\//.test(tgtBase) || suffixPattern("dto").test(tgtBase))
12828
+ return true;
12829
+ for (const folder of NESTJS_SHARED_INFRA_FOLDERS) {
12830
+ if (new RegExp(`/${folder}/`).test(tgtBase))
12831
+ return true;
12832
+ }
12833
+ for (const folder of customSharedFolders) {
12834
+ if (new RegExp(`/${folder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`).test(tgtBase))
12835
+ return true;
12836
+ }
12783
12837
  return false;
12784
12838
  }
12785
12839
  var NPM_PACKAGE_LAYER_MAP = {
@@ -12814,6 +12868,10 @@ var require_boundary_checker = __commonJS({
12814
12868
  const violations = [];
12815
12869
  const activeExceptions = policy.exceptions.filter((e) => e.isActive);
12816
12870
  for (const edge of edges) {
12871
+ if (edge.typeOnly)
12872
+ continue;
12873
+ if (/\.module(\.(ts|tsx|js|jsx))?$/.test(edge.source))
12874
+ continue;
12817
12875
  const violation = checkEdge(edge, policy, activeExceptions);
12818
12876
  if (violation) {
12819
12877
  violations.push(violation);
@@ -12873,20 +12931,23 @@ var require_boundary_checker = __commonJS({
12873
12931
  continue;
12874
12932
  const hasDenyRule = policy.rules.some((r) => r.type === "deny" && matchesPattern(sourceLayer, r.source) && (matchesPattern(targetLayer, r.target) || r.target === "__db__" && targetLayer === "infrastructure"));
12875
12933
  if (hasDenyRule) {
12876
- if (isExcepted(shared_1.ARCHITECTURE_RULES.LAYER_VIOLATION, filePath, activeExceptions))
12934
+ const isFrameworkImport = targetLayer === "api";
12935
+ const effectiveRuleId = isFrameworkImport ? shared_1.ARCHITECTURE_RULES.FORBIDDEN_FRAMEWORK : shared_1.ARCHITECTURE_RULES.LAYER_VIOLATION;
12936
+ const effectiveType = isFrameworkImport ? "forbidden-framework-in-layer" : "layer-boundary-violation";
12937
+ if (isExcepted(effectiveRuleId, filePath, activeExceptions))
12877
12938
  continue;
12878
12939
  violations.push({
12879
12940
  category: "architecture",
12880
- type: "layer-boundary-violation",
12881
- ruleId: shared_1.ARCHITECTURE_RULES.LAYER_VIOLATION,
12941
+ type: effectiveType,
12942
+ ruleId: effectiveRuleId,
12882
12943
  severity: "critical",
12883
12944
  source: "deterministic",
12884
12945
  confidence: "high",
12885
12946
  file: filePath,
12886
12947
  line,
12887
12948
  layer: sourceLayer,
12888
- message: `Forbidden dependency: "${sourceLayer}" \u2192 "${targetLayer}" \u2014 import '${pkgName}' violates layer boundary (${filePath})`,
12889
- explanation: `The ${sourceLayer} layer must not depend on ${targetLayer} packages. Move this import to the ${targetLayer} layer.`,
12949
+ message: isFrameworkImport ? `Forbidden framework import: "${sourceLayer}" imports '${pkgName}' \u2014 framework symbols not allowed in ${sourceLayer} layer (${filePath})` : `Forbidden dependency: "${sourceLayer}" \u2192 "${targetLayer}" \u2014 import '${pkgName}' violates layer boundary (${filePath})`,
12950
+ explanation: isFrameworkImport ? `The ${sourceLayer} layer must not import framework packages. Move this import to the api/controller layer.` : `The ${sourceLayer} layer must not depend on ${targetLayer} packages. Move this import to the ${targetLayer} layer.`,
12890
12951
  debtPoints: 5,
12891
12952
  gateAction: "block"
12892
12953
  });
@@ -12918,12 +12979,16 @@ var require_boundary_checker = __commonJS({
12918
12979
  }
12919
12980
  const specifiers = [];
12920
12981
  for (const decl of sourceFile.getImportDeclarations()) {
12921
- specifiers.push({ specifier: decl.getModuleSpecifierValue(), line: decl.getStartLineNumber() });
12982
+ specifiers.push({
12983
+ specifier: decl.getModuleSpecifierValue(),
12984
+ line: decl.getStartLineNumber(),
12985
+ typeOnly: decl.isTypeOnly()
12986
+ });
12922
12987
  }
12923
12988
  for (const reqSpec of extractRequireSpecifiers(sourceFile)) {
12924
12989
  specifiers.push(reqSpec);
12925
12990
  }
12926
- for (const { specifier, line } of specifiers) {
12991
+ for (const { specifier, line, typeOnly } of specifiers) {
12927
12992
  if (!specifier.startsWith(".") && !specifier.startsWith("/"))
12928
12993
  continue;
12929
12994
  const sourceDir = filePath.replace(/\/[^/]+$/, "");
@@ -12940,10 +13005,31 @@ var require_boundary_checker = __commonJS({
12940
13005
  if (isExcepted(shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY, filePath, activeExceptions))
12941
13006
  continue;
12942
13007
  const isFeatureModule = isFeatureModuleArchitecture(policy);
13008
+ const isNestJS = isNestJSFramework(policy);
12943
13009
  if (isEntityCrossModuleImport(filePath, resolvedTarget, isFeatureModule))
12944
13010
  continue;
12945
- if (isFeatureModule && isFeatureModuleExemptImport(filePath, resolvedTarget))
13011
+ if (isNestJS && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
13012
+ continue;
13013
+ if (!isNestJS && isFeatureModule && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
13014
+ continue;
13015
+ if (typeOnly) {
13016
+ violations.push({
13017
+ category: "architecture",
13018
+ type: "module-boundary-violation",
13019
+ ruleId: shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY,
13020
+ severity: "warning",
13021
+ source: "deterministic",
13022
+ confidence: "high",
13023
+ file: filePath,
13024
+ line,
13025
+ module: sourceModule,
13026
+ message: `Cross-module type import: "${sourceModule}" \u2192 "${targetModule}" \u2014 type-only import from '${specifier}' (${filePath})`,
13027
+ explanation: `Type-only import across modules does not create a runtime dependency, but indicates coupling. Consider moving the type to a shared contract.`,
13028
+ debtPoints: 1,
13029
+ gateAction: "warn"
13030
+ });
12946
13031
  continue;
13032
+ }
12947
13033
  violations.push({
12948
13034
  category: "architecture",
12949
13035
  type: "module-boundary-violation",
@@ -12971,7 +13057,7 @@ var require_boundary_checker = __commonJS({
12971
13057
  const isTargetEntity2 = entityFilePattern.test(targetPath);
12972
13058
  return isSourceEntity2 && isTargetEntity2;
12973
13059
  }
12974
- const entityDirPattern = /\/entities?\//;
13060
+ const entityDirPattern = /\/(entities?|domain)\//;
12975
13061
  const isSourceEntity = entityFilePattern.test(sourcePath) && entityDirPattern.test(sourcePath);
12976
13062
  const isTargetEntity = entityFilePattern.test(targetPath) && entityDirPattern.test(targetPath);
12977
13063
  return isSourceEntity && isTargetEntity;
@@ -13051,10 +13137,15 @@ var require_boundary_checker = __commonJS({
13051
13137
  return void 0;
13052
13138
  if (edge.sourceModule === edge.targetModule)
13053
13139
  return void 0;
13140
+ if (edge.typeOnly)
13141
+ return void 0;
13054
13142
  const isFeatureModule = policy ? isFeatureModuleArchitecture(policy) : false;
13143
+ const isNestJS = policy ? isNestJSFramework(policy) : false;
13055
13144
  if (isEntityCrossModuleImport(edge.source, edge.target, isFeatureModule))
13056
13145
  return void 0;
13057
- if (isFeatureModule && isFeatureModuleExemptImport(edge.source, edge.target))
13146
+ if (isNestJS && isNestJSExemptImport(edge.source, edge.target, policy?.sharedInfrastructure))
13147
+ return void 0;
13148
+ if (!isNestJS && isFeatureModule && isNestJSExemptImport(edge.source, edge.target, policy?.sharedInfrastructure))
13058
13149
  return void 0;
13059
13150
  const matchingRules = rules.filter((r) => matchesPattern(edge.sourceModule, r.source) && matchesPattern(edge.targetModule, r.target));
13060
13151
  if (matchingRules.some((r) => r.type === "allow"))
@@ -14075,16 +14166,22 @@ var require_runtime_risk_detector = __commonJS({
14075
14166
  }
14076
14167
  if (ts_morph_1.Node.isForStatement(node)) {
14077
14168
  const condText = node.getCondition()?.getText() ?? "";
14169
+ const bodyText = body.getText();
14170
+ const hasAwait = /\bawait\b/.test(bodyText);
14171
+ const hasCpuWork = /Math\.|\.sqrt|\.pow|\.random|\.toString|\.charCodeAt|parseInt|parseFloat|\+\+|--|\*|\//.test(bodyText);
14078
14172
  const largeBoundMatch = condText.match(/[<]=?\s*(\d+)/);
14079
14173
  if (largeBoundMatch) {
14080
14174
  const bound = parseInt(largeBoundMatch[1], 10);
14081
- if (bound >= 1e4) {
14082
- const bodyText = body.getText();
14083
- const hasAwait = /\bawait\b/.test(bodyText);
14084
- const hasCpuWork = /Math\.|\.sqrt|\.pow|\.random|\.toString|\.charCodeAt|parseInt|parseFloat|\+\+|--|\*|\//.test(bodyText);
14085
- if (!hasAwait && hasCpuWork) {
14086
- violations.push(makeViolation(shared_1.RUNTIME_RISK_RULES.CPU_HEAVY_LOOP_IN_HANDLER, filePath, node.getStartLineNumber(), `CPU-heavy loop (${bound.toLocaleString()} iterations) inside handler '${handler.name}' blocks the event loop`, policy, handler.name, "Move heavy computation to a worker thread or break into chunks with setImmediate"));
14087
- }
14175
+ if (bound >= 1e4 && !hasAwait && hasCpuWork) {
14176
+ violations.push(makeViolation(shared_1.RUNTIME_RISK_RULES.CPU_HEAVY_LOOP_IN_HANDLER, filePath, node.getStartLineNumber(), `CPU-heavy loop (${bound.toLocaleString()} iterations) inside handler '${handler.name}' blocks the event loop`, policy, handler.name, "Move heavy computation to a worker thread or break into chunks with setImmediate"));
14177
+ return;
14178
+ }
14179
+ }
14180
+ if (!largeBoundMatch && !hasAwait && hasCpuWork) {
14181
+ const dynamicBound = condText.match(/[<]=?\s*([a-zA-Z_]\w*)/);
14182
+ if (dynamicBound) {
14183
+ violations.push(makeViolation(shared_1.RUNTIME_RISK_RULES.CPU_HEAVY_LOOP_IN_HANDLER, filePath, node.getStartLineNumber(), `CPU-heavy loop with dynamic bound '${dynamicBound[1]}' inside handler '${handler.name}' \u2014 user-controlled iteration count blocks event loop`, policy, handler.name, "Cap the iteration count, move to a worker thread, or break into chunks with setImmediate"));
14184
+ return;
14088
14185
  }
14089
14186
  }
14090
14187
  }
@@ -14364,7 +14461,13 @@ var require_perf_pattern_detector = __commonJS({
14364
14461
  }
14365
14462
  if (ts_morph_1.Node.isIdentifier(obj)) {
14366
14463
  const name = obj.getText();
14367
- return name.replace(/Repo(sitory)?|Service|Model|Dao$/i, "").toLowerCase() || void 0;
14464
+ const stripped = name.replace(/Repo(sitory)?|Service|Model|Dao$/i, "");
14465
+ if (stripped && stripped !== name)
14466
+ return stripped.toLowerCase();
14467
+ const sourceFile = callExpr.getSourceFile();
14468
+ const fromVar = extractEntityFromVariable(name, sourceFile);
14469
+ if (fromVar)
14470
+ return fromVar;
14368
14471
  }
14369
14472
  return void 0;
14370
14473
  }
@@ -14405,6 +14508,56 @@ var require_perf_pattern_detector = __commonJS({
14405
14508
  }
14406
14509
  return void 0;
14407
14510
  }
14511
+ function extractEntityAny(callExpr, sourceFile) {
14512
+ let entity = extractEntity(callExpr);
14513
+ if (entity)
14514
+ return entity;
14515
+ entity = extractTypeORMEntity(callExpr);
14516
+ if (entity)
14517
+ return entity;
14518
+ entity = extractSequelizeEntity(callExpr);
14519
+ if (entity)
14520
+ return entity;
14521
+ const expr = callExpr.getExpression();
14522
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
14523
+ const obj = expr.getExpression();
14524
+ if (ts_morph_1.Node.isIdentifier(obj)) {
14525
+ entity = extractEntityFromVariable(obj.getText(), sourceFile);
14526
+ if (entity)
14527
+ return entity;
14528
+ }
14529
+ if (ts_morph_1.Node.isPropertyAccessExpression(obj)) {
14530
+ const propName = obj.getName();
14531
+ entity = extractEntityFromVariable(propName, sourceFile);
14532
+ if (entity)
14533
+ return entity;
14534
+ const stripped = propName.replace(/Repo(sitory)?$/i, "");
14535
+ if (stripped && stripped !== propName)
14536
+ return stripped.toLowerCase();
14537
+ }
14538
+ }
14539
+ return void 0;
14540
+ }
14541
+ function extractEntityFromVariable(varName, sourceFile) {
14542
+ const fileText = sourceFile.getFullText();
14543
+ const typeAnnotation = new RegExp(`\\b${escapeRegex(varName)}\\s*[:\\!]\\s*Repository\\s*<\\s*(\\w+)\\s*>`);
14544
+ const typeMatch = fileText.match(typeAnnotation);
14545
+ if (typeMatch)
14546
+ return typeMatch[1];
14547
+ const asTypeAssert = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*[^;]*\\bas\\s+Repository\\s*<\\s*(\\w+)\\s*>`);
14548
+ const asMatch = fileText.match(asTypeAssert);
14549
+ if (asMatch)
14550
+ return asMatch[1];
14551
+ const getRepoAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*getRepository\\s*\\(\\s*(\\w+)\\s*\\)`);
14552
+ const getRepoMatch = fileText.match(getRepoAssign);
14553
+ if (getRepoMatch)
14554
+ return getRepoMatch[1];
14555
+ const connRepoAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*\\w+\\.getRepository\\s*\\(\\s*(\\w+)\\s*\\)`);
14556
+ const connMatch = fileText.match(connRepoAssign);
14557
+ if (connMatch)
14558
+ return connMatch[1];
14559
+ return void 0;
14560
+ }
14408
14561
  function extractSequelizeEntity(callExpr) {
14409
14562
  const expr = callExpr.getExpression();
14410
14563
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
@@ -15079,15 +15232,30 @@ var require_perf_pattern_detector = __commonJS({
15079
15232
  const relationsProp = objArg.getProperty("relations");
15080
15233
  if (relationsProp) {
15081
15234
  const relText = relationsProp.getText();
15082
- const dotMatches = relText.match(/['"][^'"]*\.[^'"]*\.[^'"]*['"]/g);
15083
- if (dotMatches && dotMatches.length > 0) {
15235
+ const allRelations = relText.match(/['"][^'"]+['"]/g) ?? [];
15236
+ const dottedRelations = allRelations.filter((m) => m.includes("."));
15237
+ const totalRelCount = allRelations.length;
15238
+ const maxDepth = allRelations.length > 0 ? Math.max(...allRelations.map((m) => m.replace(/['"]/g, "").split(".").length)) : 0;
15239
+ if (dottedRelations.length > 0 || totalRelCount >= 4) {
15084
15240
  let entity = extractEntity(node);
15085
15241
  if (!entity)
15086
15242
  entity = extractTypeORMEntity(node);
15087
15243
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15088
15244
  const fn = getEnclosingFn(node, fns);
15089
- const maxDepth = Math.max(...dotMatches.map((m) => m.split(".").length));
15090
- pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.NESTED_INCLUDE_LARGE, filePath, node.getStartLineNumber(), `Deeply nested relations (depth ${maxDepth})${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may cause cartesian explosion`, policy, vol, fn?.name, "Flatten relations or use separate queries to avoid loading excessive data"));
15245
+ pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.NESTED_INCLUDE_LARGE, filePath, node.getStartLineNumber(), `Deeply nested relations (depth ${maxDepth}, ${totalRelCount} relations)${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may cause cartesian explosion`, policy, vol, fn?.name, "Flatten relations or use separate queries to avoid loading excessive data"));
15246
+ }
15247
+ const seqIncludeProp = objArg.getProperty("include");
15248
+ if (seqIncludeProp) {
15249
+ const seqIncText = seqIncludeProp.getText();
15250
+ const nestCount = countNestedIncludes(seqIncText);
15251
+ if (nestCount >= 2) {
15252
+ let entity = extractEntity(node);
15253
+ if (!entity)
15254
+ entity = extractSequelizeEntity(node);
15255
+ const vol = entity ? resolveVolume(entity, policy) : void 0;
15256
+ const fn = getEnclosingFn(node, fns);
15257
+ pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.NESTED_INCLUDE_LARGE, filePath, node.getStartLineNumber(), `Deeply nested include (depth ${nestCount})${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may cause cartesian explosion`, policy, vol, fn?.name, "Flatten includes or use separate queries to avoid loading excessive data"));
15258
+ }
15091
15259
  }
15092
15260
  }
15093
15261
  });
@@ -15134,6 +15302,7 @@ var require_perf_pattern_detector = __commonJS({
15134
15302
  "countDocuments",
15135
15303
  "estimatedDocumentCount"
15136
15304
  ]);
15305
+ var DB_CALL_METHOD_PATTERN_EXTENDED = /^(find|get|fetch|load|search|query|count|list)(By|All|Many|One|First|Where|With)?/i;
15137
15306
  function detectNPlusOne(sourceFile, filePath, fns, policy, violations) {
15138
15307
  sourceFile.forEachDescendant((node) => {
15139
15308
  if (!ts_morph_1.Node.isAwaitExpression(node))
@@ -15147,7 +15316,7 @@ var require_perf_pattern_detector = __commonJS({
15147
15316
  const callMethodName = callExpr.getName();
15148
15317
  let isDrizzleOrKnexChain = false;
15149
15318
  let chainEntity;
15150
- if (!DB_CALL_METHODS.has(callMethodName)) {
15319
+ if (!DB_CALL_METHODS.has(callMethodName) && !DB_CALL_METHOD_PATTERN_EXTENDED.test(callMethodName)) {
15151
15320
  const drizzle = traceDrizzleChain(awaited);
15152
15321
  if (drizzle.isDrizzle) {
15153
15322
  isDrizzleOrKnexChain = true;
@@ -15235,7 +15404,7 @@ var require_perf_pattern_detector = __commonJS({
15235
15404
  });
15236
15405
  });
15237
15406
  }
15238
- var DB_CALL_METHODS_PATTERN = /\.(findMany|findFirst|findUnique|findOne|find|findById|create|update|delete|upsert|count|countDocuments|estimatedDocumentCount|aggregate|query|execute|save|remove|insert|getMany|getOne|findAll|findByPk|findAndCountAll|findOrCreate|destroy|from|select|first|raw)\s*\(/;
15407
+ var DB_CALL_METHODS_PATTERN = /\.(findMany|findFirst|findUnique|findOne|find|findById|findBy\w+|getBy\w+|fetchBy\w+|create|update|delete|upsert|count|countDocuments|estimatedDocumentCount|aggregate|query|execute|save|remove|insert|getMany|getOne|findAll|findByPk|findAndCountAll|findOrCreate|destroy|from|select|first|raw)\s*\(/;
15239
15408
  function detectRecursiveNPlusOne(sourceFile, filePath, fns, policy, violations) {
15240
15409
  const DB_METHOD_NAMES = /* @__PURE__ */ new Set([
15241
15410
  "find",
@@ -15307,6 +15476,7 @@ var require_perf_pattern_detector = __commonJS({
15307
15476
  }
15308
15477
  return false;
15309
15478
  }
15479
+ var IN_MEMORY_OPERATIONS = /* @__PURE__ */ new Set(["filter", "sort", "map", "reduce", "flatMap"]);
15310
15480
  function detectFetchAllFilterMemory(sourceFile, filePath, fns, policy, violations) {
15311
15481
  sourceFile.forEachDescendant((node) => {
15312
15482
  if (!ts_morph_1.Node.isCallExpression(node))
@@ -15314,7 +15484,7 @@ var require_perf_pattern_detector = __commonJS({
15314
15484
  const expr = node.getExpression();
15315
15485
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
15316
15486
  return;
15317
- if (expr.getName() !== "filter")
15487
+ if (!IN_MEMORY_OPERATIONS.has(expr.getName()))
15318
15488
  return;
15319
15489
  const obj = expr.getExpression();
15320
15490
  if (ts_morph_1.Node.isParenthesizedExpression(obj)) {
@@ -15342,26 +15512,65 @@ var require_perf_pattern_detector = __commonJS({
15342
15512
  const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
15343
15513
  if (!block)
15344
15514
  return;
15345
- const blockText = block.getText();
15346
- const nodePos = node.getStart() - block.getStart();
15347
- const textBefore = blockText.substring(0, nodePos);
15348
- const assignPattern = new RegExp(`${escapeRegex(varName)}\\s*=\\s*await\\s+(?:\\S+\\.(findMany|find|getMany|findAll|from|select)\\s*\\(|(?:knex|db)\\s*\\()`);
15349
- const assignMatch = textBefore.match(new RegExp(`(\\w+)\\.${escapeRegex(varName)}|\\b(\\w+)\\.(findMany|find|getMany|findAll)\\s*\\(`));
15350
- if (assignPattern.test(textBefore)) {
15351
- let entity;
15352
- const chain = textBefore.match(/(\w+)\.(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
15353
- if (chain)
15354
- entity = chain[2];
15355
- if (!entity) {
15356
- const seqMatch = textBefore.match(/([A-Z]\w+)\.(findAll|find)\s*\(/);
15357
- if (seqMatch)
15358
- entity = seqMatch[1];
15515
+ let foundDbAssignment = false;
15516
+ let entity;
15517
+ block.forEachDescendant((child) => {
15518
+ if (foundDbAssignment)
15519
+ return;
15520
+ if (!ts_morph_1.Node.isVariableDeclaration(child))
15521
+ return;
15522
+ if (child.getName() !== varName)
15523
+ return;
15524
+ if (child.getStart() >= node.getStart())
15525
+ return;
15526
+ const init = child.getInitializer();
15527
+ if (!init)
15528
+ return;
15529
+ let awaitedCall;
15530
+ if (ts_morph_1.Node.isAwaitExpression(init)) {
15531
+ const awaited = init.getExpression();
15532
+ if (ts_morph_1.Node.isCallExpression(awaited))
15533
+ awaitedCall = awaited;
15534
+ } else if (ts_morph_1.Node.isCallExpression(init)) {
15535
+ awaitedCall = init;
15359
15536
  }
15360
- if (!entity) {
15361
- const typeormMatch = textBefore.match(/getRepository\(\s*(\w+)\s*\)\.(findMany|find|getMany)\s*\(/);
15362
- if (typeormMatch)
15363
- entity = typeormMatch[1];
15537
+ if (!awaitedCall)
15538
+ return;
15539
+ const callExpr = awaitedCall.getExpression();
15540
+ if (!ts_morph_1.Node.isPropertyAccessExpression(callExpr))
15541
+ return;
15542
+ const methodName = callExpr.getName();
15543
+ const dbMethods = /* @__PURE__ */ new Set([
15544
+ "findMany",
15545
+ "find",
15546
+ "getMany",
15547
+ "findAll",
15548
+ "findAndCountAll",
15549
+ "from",
15550
+ "select",
15551
+ "getAll",
15552
+ "fetchAll",
15553
+ "findBy",
15554
+ "query"
15555
+ ]);
15556
+ if (!dbMethods.has(methodName) && !/^(find|get|fetch|load)(By|All|Many)/.test(methodName))
15557
+ return;
15558
+ foundDbAssignment = true;
15559
+ entity = extractEntityAny(awaitedCall, sourceFile);
15560
+ });
15561
+ if (!foundDbAssignment) {
15562
+ const blockText = block.getText();
15563
+ const nodePos = node.getStart() - block.getStart();
15564
+ const textBefore = blockText.substring(0, nodePos);
15565
+ const assignPattern = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+[\\w.]+\\.(findMany|find|getMany|findAll|findBy\\w+|getAll|fetchAll|from|select)\\s*\\(`);
15566
+ if (assignPattern.test(textBefore)) {
15567
+ foundDbAssignment = true;
15568
+ const chain = textBefore.match(/(\w+)\.(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
15569
+ if (chain)
15570
+ entity = chain[2];
15364
15571
  }
15572
+ }
15573
+ if (foundDbAssignment) {
15365
15574
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15366
15575
  const fn = getEnclosingFn(node, fns);
15367
15576
  pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.FETCH_ALL_FILTER_MEMORY, filePath, node.getStartLineNumber(), `Fetching all '${varName}' then filtering in memory${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 move filter to where clause`, policy, vol, fn?.name, "Use a where clause in the query instead of .filter() in memory"));
@@ -15433,6 +15642,11 @@ var require_perf_pattern_detector = __commonJS({
15433
15642
  if (/\.(controller|handler|route|endpoint)\./i.test(fp))
15434
15643
  return true;
15435
15644
  }
15645
+ if (ts_morph_1.Node.isMethodDeclaration(node)) {
15646
+ const fp = node.getSourceFile().getFilePath();
15647
+ if (/\.(controller|handler)\./i.test(fp))
15648
+ return true;
15649
+ }
15436
15650
  return false;
15437
15651
  }
15438
15652
  function detectMissingPagination(sourceFile, filePath, fns, policy, violations) {
@@ -15445,7 +15659,17 @@ var require_perf_pattern_detector = __commonJS({
15445
15659
  if (/page|skip|offset|cursor|take|limit|per_page|pageSize/i.test(bodyText))
15446
15660
  continue;
15447
15661
  const entityMatch = bodyText.match(/(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
15448
- const entity = entityMatch ? entityMatch[1] : void 0;
15662
+ let entity = entityMatch ? entityMatch[1] : void 0;
15663
+ if (entity) {
15664
+ const stripped = entity.replace(/Repo(sitory)?|Service|Model|Dao$/i, "");
15665
+ if (stripped && stripped !== entity)
15666
+ entity = stripped;
15667
+ }
15668
+ if (entity) {
15669
+ const varEntity = extractEntityFromVariable(entity, sourceFile);
15670
+ if (varEntity)
15671
+ entity = varEntity;
15672
+ }
15449
15673
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15450
15674
  if (vol && vol.size === "S")
15451
15675
  continue;
@@ -15534,6 +15758,13 @@ var require_perf_pattern_detector = __commonJS({
15534
15758
  const args = node.getArguments();
15535
15759
  if (args.length > 0 && argsContainKey(node, "where"))
15536
15760
  return;
15761
+ {
15762
+ const countObj = expr.getExpression();
15763
+ if (ts_morph_1.Node.isIdentifier(countObj) && /^(em|entityManager|manager|orm)$/i.test(countObj.getText())) {
15764
+ if (args.length > 1)
15765
+ return;
15766
+ }
15767
+ }
15537
15768
  let entity = extractEntity(node);
15538
15769
  if (!entity)
15539
15770
  entity = extractTypeORMEntity(node);
@@ -15587,7 +15818,7 @@ var require_perf_pattern_detector = __commonJS({
15587
15818
  const args = node.getArguments();
15588
15819
  if (args.length === 0)
15589
15820
  return;
15590
- const isUnsafe = method === "$queryRawUnsafe" || method === "$executeRawUnsafe";
15821
+ let isUnsafe = method === "$queryRawUnsafe" || method === "$executeRawUnsafe";
15591
15822
  let sqlText = "";
15592
15823
  const firstArg = args[0];
15593
15824
  if (ts_morph_1.Node.isStringLiteral(firstArg)) {
@@ -15596,6 +15827,7 @@ var require_perf_pattern_detector = __commonJS({
15596
15827
  sqlText = firstArg.getText();
15597
15828
  } else if (ts_morph_1.Node.isBinaryExpression(firstArg)) {
15598
15829
  sqlText = firstArg.getText();
15830
+ isUnsafe = true;
15599
15831
  }
15600
15832
  if (sqlText) {
15601
15833
  checkSqlForLimit(sqlText, filePath, node.getStartLineNumber(), fns, policy, violations, node, isUnsafe);
@@ -15616,6 +15848,21 @@ var require_perf_pattern_detector = __commonJS({
15616
15848
  debtPoints: policy.scoring.performance_risk_critical,
15617
15849
  gateAction: "block"
15618
15850
  });
15851
+ violations.push({
15852
+ category: "performance",
15853
+ type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15854
+ ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15855
+ severity: "critical",
15856
+ source: "deterministic",
15857
+ confidence: "high",
15858
+ file: filePath,
15859
+ line: node.getStartLineNumber(),
15860
+ function: fn?.name,
15861
+ message: `${method}() used \u2014 SQL injection risk via string interpolation/concatenation`,
15862
+ suggestion: "Use $queryRaw with tagged template literals and parameterized queries instead",
15863
+ debtPoints: policy.scoring.performance_risk_critical,
15864
+ gateAction: "block"
15865
+ });
15619
15866
  }
15620
15867
  }
15621
15868
  if (ts_morph_1.Node.isTaggedTemplateExpression(node)) {
@@ -15638,15 +15885,15 @@ var require_perf_pattern_detector = __commonJS({
15638
15885
  const fn2 = getEnclosingFn(node, fns);
15639
15886
  violations.push({
15640
15887
  category: "performance",
15641
- type: shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT,
15642
- ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT,
15888
+ type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15889
+ ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15643
15890
  severity: "critical",
15644
15891
  source: "deterministic",
15645
15892
  confidence: "high",
15646
15893
  file: filePath,
15647
15894
  line,
15648
15895
  function: fn2?.name,
15649
- message: `$queryRawUnsafe() used \u2014 SQL injection risk and unbounded query`,
15896
+ message: `$queryRawUnsafe() used \u2014 SQL injection risk`,
15650
15897
  suggestion: "Use $queryRaw with tagged template literals instead of $queryRawUnsafe",
15651
15898
  debtPoints: policy.scoring.performance_risk_critical,
15652
15899
  gateAction: "block"
@@ -15664,22 +15911,24 @@ var require_perf_pattern_detector = __commonJS({
15664
15911
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15665
15912
  const fn = getEnclosingFn(node, fns);
15666
15913
  if (isUnsafe) {
15667
- const severity = hasLimit ? "warning" : "critical";
15668
15914
  violations.push({
15669
15915
  category: "performance",
15670
- type: shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT,
15671
- ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT,
15672
- severity,
15916
+ type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15917
+ ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15918
+ severity: "critical",
15673
15919
  source: "deterministic",
15674
15920
  confidence: "high",
15675
15921
  file: filePath,
15676
15922
  line,
15677
15923
  function: fn?.name,
15678
- message: hasLimit ? `$queryRawUnsafe() used${vol ? ` on '${entity}'` : ""} \u2014 SQL injection risk (has LIMIT but uses unsafe method)` : `$queryRawUnsafe() used${vol ? ` on '${entity}'` : ""} \u2014 SQL injection risk and unbounded query`,
15924
+ message: `$queryRawUnsafe() used${vol ? ` on '${entity}'` : ""} \u2014 SQL injection risk`,
15679
15925
  suggestion: "Use $queryRaw with tagged template literals instead of $queryRawUnsafe",
15680
- debtPoints: severity === "critical" ? policy.scoring.performance_risk_critical : policy.scoring.performance_risk_warning,
15681
- gateAction: severity === "critical" ? "block" : "warn"
15926
+ debtPoints: policy.scoring.performance_risk_critical,
15927
+ gateAction: "block"
15682
15928
  });
15929
+ if (!hasLimit) {
15930
+ pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT, filePath, line, `Raw SQL SELECT without LIMIT${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may return unbounded results`, policy, vol, fn?.name, "Add LIMIT clause to the SQL query"));
15931
+ }
15683
15932
  } else {
15684
15933
  pushIfNotNull(violations, makeViolation(shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT, filePath, line, `Raw SQL SELECT without LIMIT${vol ? ` on '${entity}' (${vol.size} volume)` : ""} \u2014 may return unbounded results`, policy, vol, fn?.name, "Add LIMIT clause to the SQL query"));
15685
15934
  }
@@ -15788,7 +16037,7 @@ var require_reliability_detector = __commonJS({
15788
16037
  function isNestJSInjectable(classNode) {
15789
16038
  return classNode.getDecorators().some((d) => {
15790
16039
  const name = d.getName();
15791
- return name === "Injectable" || name === "Controller" || name === "Resolver";
16040
+ return name === "Injectable";
15792
16041
  });
15793
16042
  }
15794
16043
  function isInsideNestJSInjectable(node) {
@@ -16069,6 +16318,12 @@ var require_reliability_detector = __commonJS({
16069
16318
  violations.push(makeViolation(shared_1.RELIABILITY_RULES.UNHANDLED_PROMISE_REJECTION, filePath, node.getStartLineNumber(), `Fire-and-forget: '${calledName}' called without await \u2014 promise result is silently dropped`, policy, fn?.name, `Add await before the call or handle with .catch()`));
16070
16319
  return;
16071
16320
  }
16321
+ const ASYNC_METHOD_PATTERNS = /^(fetch|get|send|post|put|patch|delete|process|execute|run|dispatch|emit|publish|notify|sync|upload|download|import|export|create|update|remove|find|search|load|save|handle|trigger)/i;
16322
+ if (ASYNC_METHOD_PATTERNS.test(calledName)) {
16323
+ const fn = getEnclosingFn(node, fns);
16324
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.UNHANDLED_PROMISE_REJECTION, filePath, node.getStartLineNumber(), `Fire-and-forget: '${calledName}' called without await \u2014 promise result is silently dropped`, policy, fn?.name, `Add await before the call or handle with .catch()`));
16325
+ return;
16326
+ }
16072
16327
  } else if (ts_morph_1.Node.isIdentifier(callExpr)) {
16073
16328
  calledName = callExpr.getText();
16074
16329
  const matchFn = fns.find((f) => f.name === calledName && f.isAsync);
@@ -16276,6 +16531,7 @@ var require_reliability_detector = __commonJS({
16276
16531
  violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_ERROR_LOGGING, filePath, node.getStartLineNumber(), "catch block has no error logging or reporting", policy, fn?.name, "Add logger.error(err) or re-throw the error"));
16277
16532
  });
16278
16533
  }
16534
+ var TRANSACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["$transaction", "transaction"]);
16279
16535
  function detectTransactionNoTimeout(sourceFile, filePath, fns, policy, violations) {
16280
16536
  sourceFile.forEachDescendant((node) => {
16281
16537
  if (!ts_morph_1.Node.isCallExpression(node))
@@ -16283,7 +16539,7 @@ var require_reliability_detector = __commonJS({
16283
16539
  const expr = node.getExpression();
16284
16540
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
16285
16541
  return;
16286
- if (expr.getName() !== "$transaction")
16542
+ if (!TRANSACTION_METHOD_NAMES.has(expr.getName()))
16287
16543
  return;
16288
16544
  const args = node.getArguments();
16289
16545
  const hasTimeout = args.some((arg) => {
@@ -16299,6 +16555,8 @@ var require_reliability_detector = __commonJS({
16299
16555
  });
16300
16556
  }
16301
16557
  function detectMissingNullGuard(sourceFile, filePath, fns, policy, violations) {
16558
+ detectNonNullAssertionOnNullable(sourceFile, filePath, fns, policy, violations);
16559
+ detectUnsafeArrayIndexAccess(sourceFile, filePath, fns, policy, violations);
16302
16560
  sourceFile.forEachDescendant((node) => {
16303
16561
  if (!ts_morph_1.Node.isVariableDeclaration(node))
16304
16562
  return;
@@ -16352,6 +16610,62 @@ var require_reliability_detector = __commonJS({
16352
16610
  }
16353
16611
  });
16354
16612
  }
16613
+ function detectNonNullAssertionOnNullable(sourceFile, filePath, fns, policy, violations) {
16614
+ sourceFile.forEachDescendant((node) => {
16615
+ if (!ts_morph_1.Node.isNonNullExpression(node))
16616
+ return;
16617
+ const inner = node.getExpression();
16618
+ if (!ts_morph_1.Node.isIdentifier(inner))
16619
+ return;
16620
+ const varName = inner.getText();
16621
+ const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
16622
+ if (!block)
16623
+ return;
16624
+ const blockText = block.getText();
16625
+ const nodePos = node.getStart() - block.getStart();
16626
+ const textBefore = blockText.substring(0, nodePos);
16627
+ const nullableAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+\\S+\\.(findUnique|findFirst|findOne|findByPk)\\s*\\(`);
16628
+ if (!nullableAssign.test(textBefore))
16629
+ return;
16630
+ const fn = getEnclosingFn(node, fns);
16631
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_NULL_GUARD, filePath, node.getStartLineNumber(), `Non-null assertion on '${varName}!' after nullable query \u2014 may throw at runtime`, policy, fn?.name, `Add a null guard: if (!${varName}) throw new NotFoundException();`));
16632
+ });
16633
+ }
16634
+ function detectUnsafeArrayIndexAccess(sourceFile, filePath, fns, policy, violations) {
16635
+ sourceFile.forEachDescendant((node) => {
16636
+ if (!ts_morph_1.Node.isElementAccessExpression(node))
16637
+ return;
16638
+ const obj = node.getExpression();
16639
+ if (!ts_morph_1.Node.isIdentifier(obj))
16640
+ return;
16641
+ const varName = obj.getText();
16642
+ const indexArg = node.getArgumentExpression();
16643
+ if (!indexArg || !ts_morph_1.Node.isNumericLiteral(indexArg))
16644
+ return;
16645
+ const parent = node.getParent();
16646
+ if (!parent || !ts_morph_1.Node.isPropertyAccessExpression(parent))
16647
+ return;
16648
+ const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
16649
+ if (!block)
16650
+ return;
16651
+ const blockText = block.getText();
16652
+ const nodePos = node.getStart() - block.getStart();
16653
+ const textBefore = blockText.substring(0, nodePos);
16654
+ const arrayAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+\\S+\\.(findMany|find|findAll|getMany|query)\\s*\\(`);
16655
+ if (!arrayAssign.test(textBefore))
16656
+ return;
16657
+ const lengthGuard = new RegExp(`${escapeRegex(varName)}\\s*\\.\\s*length|${escapeRegex(varName)}\\s*\\[\\s*0\\s*\\]\\s*&&|if\\s*\\(\\s*!?${escapeRegex(varName)}`);
16658
+ const assignMatch = textBefore.match(arrayAssign);
16659
+ if (assignMatch) {
16660
+ const assignEnd = textBefore.indexOf(assignMatch[0]) + assignMatch[0].length;
16661
+ const textBetween = textBefore.substring(assignEnd);
16662
+ if (lengthGuard.test(textBetween))
16663
+ return;
16664
+ }
16665
+ const fn = getEnclosingFn(node, fns);
16666
+ violations.push(makeViolation(shared_1.RELIABILITY_RULES.MISSING_NULL_GUARD, filePath, node.getStartLineNumber(), `Array index access '${varName}[${indexArg.getText()}]' without length guard \u2014 may throw on empty results`, policy, fn?.name, `Add a guard: if (${varName}.length === 0) throw new NotFoundException();`));
16667
+ });
16668
+ }
16355
16669
  function escapeRegex(str) {
16356
16670
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16357
16671
  }
@@ -18054,6 +18368,7 @@ var require_orchestrator = __commonJS({
18054
18368
  crossFileAnalysis = await (0, cross_file_analyzer_1.analyzeCrossFile)(unflaggedPatterns, importGraph, filteredInput, policy);
18055
18369
  }
18056
18370
  const complexityViolations = complexityDeltasToViolations(complexityDeltas, filteredInput);
18371
+ const largeFileViolations = detectLargeFiles(filteredInput);
18057
18372
  const violations = [
18058
18373
  ...boundaryViolations,
18059
18374
  ...circularViolations,
@@ -18061,6 +18376,7 @@ var require_orchestrator = __commonJS({
18061
18376
  ...perfViolations,
18062
18377
  ...reliabilityViolations,
18063
18378
  ...complexityViolations,
18379
+ ...largeFileViolations,
18064
18380
  ...duplicationResult.violations,
18065
18381
  ...missingTestsResult?.violations ?? [],
18066
18382
  ...deadCodeResult.violations,
@@ -18078,6 +18394,34 @@ var require_orchestrator = __commonJS({
18078
18394
  crossFileAnalysis
18079
18395
  };
18080
18396
  }
18397
+ function detectLargeFiles(input) {
18398
+ const threshold = shared_1.DEFAULT_LARGE_FILE_THRESHOLD;
18399
+ const violations = [];
18400
+ for (const file of input.changedFiles) {
18401
+ if (file.status === "deleted")
18402
+ continue;
18403
+ if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
18404
+ continue;
18405
+ const lineCount = file.content.split("\n").length;
18406
+ if (lineCount <= threshold)
18407
+ continue;
18408
+ violations.push({
18409
+ category: "maintainability",
18410
+ type: shared_1.MAINTAINABILITY_RULES.LARGE_FILE,
18411
+ ruleId: shared_1.MAINTAINABILITY_RULES.LARGE_FILE,
18412
+ severity: "warning",
18413
+ source: "deterministic",
18414
+ confidence: "high",
18415
+ file: file.path,
18416
+ line: 1,
18417
+ message: `File has ${lineCount} lines (threshold: ${threshold}) \u2014 consider splitting into smaller modules`,
18418
+ suggestion: "Extract helper functions, types, or constants into separate files",
18419
+ debtPoints: Math.floor((lineCount - threshold) / 50) + 1,
18420
+ gateAction: "warn"
18421
+ });
18422
+ }
18423
+ return violations;
18424
+ }
18081
18425
  function complexityDeltasToViolations(deltas, input) {
18082
18426
  const threshold = shared_1.DEFAULT_COMPLEXITY_THRESHOLD;
18083
18427
  const violations = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "technical-debt-radar",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Stop Node.js production crashes before merge. 47 detection patterns across 5 categories.",
5
5
  "bin": {
6
6
  "radar": "dist/index.js",