technical-debt-radar 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +493 -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
  });
@@ -12896,6 +12957,95 @@ var require_boundary_checker = __commonJS({
12896
12957
  }
12897
12958
  return violations;
12898
12959
  }
12960
+ function isInConstructorShorthandParam(node) {
12961
+ let current = node;
12962
+ while (current) {
12963
+ if (current.getKind() === ts_morph_1.SyntaxKind.Parameter) {
12964
+ const paramText = current.getText();
12965
+ if (/^\s*(private|protected|public|readonly)\s/.test(paramText)) {
12966
+ const paramParent = current.getParent();
12967
+ if (paramParent && paramParent.getKind() === ts_morph_1.SyntaxKind.Constructor) {
12968
+ return true;
12969
+ }
12970
+ }
12971
+ return false;
12972
+ }
12973
+ current = current.getParent();
12974
+ }
12975
+ return false;
12976
+ }
12977
+ function isTypePosition(node) {
12978
+ if (isInConstructorShorthandParam(node))
12979
+ return false;
12980
+ const parent = node.getParent();
12981
+ if (!parent)
12982
+ return false;
12983
+ const parentKind = parent.getKind();
12984
+ if (parentKind === ts_morph_1.SyntaxKind.TypeReference || parentKind === ts_morph_1.SyntaxKind.TypeQuery || parentKind === ts_morph_1.SyntaxKind.TypeAliasDeclaration || parentKind === ts_morph_1.SyntaxKind.InterfaceDeclaration || parentKind === ts_morph_1.SyntaxKind.AsExpression || parentKind === ts_morph_1.SyntaxKind.TypeAssertionExpression || parentKind === ts_morph_1.SyntaxKind.ExpressionWithTypeArguments || parentKind === ts_morph_1.SyntaxKind.MappedType || parentKind === ts_morph_1.SyntaxKind.ConditionalType || parentKind === ts_morph_1.SyntaxKind.IntersectionType || parentKind === ts_morph_1.SyntaxKind.UnionType || parentKind === ts_morph_1.SyntaxKind.TupleType || parentKind === ts_morph_1.SyntaxKind.ArrayType || parentKind === ts_morph_1.SyntaxKind.IndexedAccessType || parentKind === ts_morph_1.SyntaxKind.TypeOperator || parentKind === ts_morph_1.SyntaxKind.ParenthesizedType) {
12985
+ return true;
12986
+ }
12987
+ if (parentKind === ts_morph_1.SyntaxKind.Parameter || parentKind === ts_morph_1.SyntaxKind.PropertyDeclaration || parentKind === ts_morph_1.SyntaxKind.PropertySignature || parentKind === ts_morph_1.SyntaxKind.VariableDeclaration || parentKind === ts_morph_1.SyntaxKind.FunctionDeclaration || parentKind === ts_morph_1.SyntaxKind.MethodDeclaration || parentKind === ts_morph_1.SyntaxKind.MethodSignature || parentKind === ts_morph_1.SyntaxKind.ArrowFunction || parentKind === ts_morph_1.SyntaxKind.GetAccessor || parentKind === ts_morph_1.SyntaxKind.SetAccessor) {
12988
+ const colonToken = parent.getFirstChildByKind(ts_morph_1.SyntaxKind.ColonToken);
12989
+ if (colonToken && node.getPos() > colonToken.getPos()) {
12990
+ const equalsToken = parent.getFirstChildByKind(ts_morph_1.SyntaxKind.EqualsToken);
12991
+ if (!equalsToken || node.getPos() < equalsToken.getPos()) {
12992
+ return true;
12993
+ }
12994
+ }
12995
+ }
12996
+ if (parentKind === ts_morph_1.SyntaxKind.HeritageClause) {
12997
+ return true;
12998
+ }
12999
+ if (parentKind === ts_morph_1.SyntaxKind.QualifiedName) {
13000
+ const grandParent = parent.getParent();
13001
+ if (grandParent && grandParent.getKind() === ts_morph_1.SyntaxKind.TypeReference) {
13002
+ return true;
13003
+ }
13004
+ }
13005
+ return false;
13006
+ }
13007
+ function isImplicitlyTypeOnly(decl, sourceFile) {
13008
+ if (decl.isTypeOnly())
13009
+ return false;
13010
+ if (decl.getNamespaceImport())
13011
+ return false;
13012
+ const defaultImport = decl.getDefaultImport();
13013
+ const namedImports = decl.getNamedImports();
13014
+ if (!defaultImport && namedImports.length === 0)
13015
+ return false;
13016
+ const importedNames = [];
13017
+ if (defaultImport) {
13018
+ importedNames.push(defaultImport.getText());
13019
+ }
13020
+ for (const named of namedImports) {
13021
+ if (named.isTypeOnly())
13022
+ continue;
13023
+ importedNames.push(named.getName());
13024
+ }
13025
+ if (importedNames.length === 0)
13026
+ return true;
13027
+ for (const name of importedNames) {
13028
+ const identifiers = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.Identifier).filter((id) => id.getText() === name);
13029
+ const usages = identifiers.filter((id) => {
13030
+ const parent = id.getParent();
13031
+ if (!parent)
13032
+ return false;
13033
+ const parentKind = parent.getKind();
13034
+ if (parentKind === ts_morph_1.SyntaxKind.ImportSpecifier || parentKind === ts_morph_1.SyntaxKind.ImportClause) {
13035
+ return false;
13036
+ }
13037
+ return true;
13038
+ });
13039
+ if (usages.length === 0)
13040
+ return false;
13041
+ for (const usage of usages) {
13042
+ if (!isTypePosition(usage)) {
13043
+ return false;
13044
+ }
13045
+ }
13046
+ }
13047
+ return true;
13048
+ }
12899
13049
  function checkCrossModuleDirectImports(input, policy, activeExceptions) {
12900
13050
  const violations = [];
12901
13051
  const hasCrossModuleDeny = policy.rules.some((r) => r.type === "deny" && /cross-module/i.test(r.description ?? "") || /cross-module/i.test(r.source));
@@ -12918,12 +13068,18 @@ var require_boundary_checker = __commonJS({
12918
13068
  }
12919
13069
  const specifiers = [];
12920
13070
  for (const decl of sourceFile.getImportDeclarations()) {
12921
- specifiers.push({ specifier: decl.getModuleSpecifierValue(), line: decl.getStartLineNumber() });
13071
+ const explicitTypeOnly = decl.isTypeOnly();
13072
+ const implicitTypeOnly = !explicitTypeOnly && isImplicitlyTypeOnly(decl, sourceFile);
13073
+ specifiers.push({
13074
+ specifier: decl.getModuleSpecifierValue(),
13075
+ line: decl.getStartLineNumber(),
13076
+ typeOnly: explicitTypeOnly || implicitTypeOnly
13077
+ });
12922
13078
  }
12923
13079
  for (const reqSpec of extractRequireSpecifiers(sourceFile)) {
12924
13080
  specifiers.push(reqSpec);
12925
13081
  }
12926
- for (const { specifier, line } of specifiers) {
13082
+ for (const { specifier, line, typeOnly } of specifiers) {
12927
13083
  if (!specifier.startsWith(".") && !specifier.startsWith("/"))
12928
13084
  continue;
12929
13085
  const sourceDir = filePath.replace(/\/[^/]+$/, "");
@@ -12940,9 +13096,14 @@ var require_boundary_checker = __commonJS({
12940
13096
  if (isExcepted(shared_1.ARCHITECTURE_RULES.MODULE_BOUNDARY, filePath, activeExceptions))
12941
13097
  continue;
12942
13098
  const isFeatureModule = isFeatureModuleArchitecture(policy);
13099
+ const isNestJS = isNestJSFramework(policy);
12943
13100
  if (isEntityCrossModuleImport(filePath, resolvedTarget, isFeatureModule))
12944
13101
  continue;
12945
- if (isFeatureModule && isFeatureModuleExemptImport(filePath, resolvedTarget))
13102
+ if (isNestJS && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
13103
+ continue;
13104
+ if (!isNestJS && isFeatureModule && isNestJSExemptImport(filePath, resolvedTarget, policy.sharedInfrastructure))
13105
+ continue;
13106
+ if (typeOnly)
12946
13107
  continue;
12947
13108
  violations.push({
12948
13109
  category: "architecture",
@@ -12971,7 +13132,7 @@ var require_boundary_checker = __commonJS({
12971
13132
  const isTargetEntity2 = entityFilePattern.test(targetPath);
12972
13133
  return isSourceEntity2 && isTargetEntity2;
12973
13134
  }
12974
- const entityDirPattern = /\/entities?\//;
13135
+ const entityDirPattern = /\/(entities?|domain)\//;
12975
13136
  const isSourceEntity = entityFilePattern.test(sourcePath) && entityDirPattern.test(sourcePath);
12976
13137
  const isTargetEntity = entityFilePattern.test(targetPath) && entityDirPattern.test(targetPath);
12977
13138
  return isSourceEntity && isTargetEntity;
@@ -13051,10 +13212,15 @@ var require_boundary_checker = __commonJS({
13051
13212
  return void 0;
13052
13213
  if (edge.sourceModule === edge.targetModule)
13053
13214
  return void 0;
13215
+ if (edge.typeOnly)
13216
+ return void 0;
13054
13217
  const isFeatureModule = policy ? isFeatureModuleArchitecture(policy) : false;
13218
+ const isNestJS = policy ? isNestJSFramework(policy) : false;
13055
13219
  if (isEntityCrossModuleImport(edge.source, edge.target, isFeatureModule))
13056
13220
  return void 0;
13057
- if (isFeatureModule && isFeatureModuleExemptImport(edge.source, edge.target))
13221
+ if (isNestJS && isNestJSExemptImport(edge.source, edge.target, policy?.sharedInfrastructure))
13222
+ return void 0;
13223
+ if (!isNestJS && isFeatureModule && isNestJSExemptImport(edge.source, edge.target, policy?.sharedInfrastructure))
13058
13224
  return void 0;
13059
13225
  const matchingRules = rules.filter((r) => matchesPattern(edge.sourceModule, r.source) && matchesPattern(edge.targetModule, r.target));
13060
13226
  if (matchingRules.some((r) => r.type === "allow"))
@@ -14075,16 +14241,22 @@ var require_runtime_risk_detector = __commonJS({
14075
14241
  }
14076
14242
  if (ts_morph_1.Node.isForStatement(node)) {
14077
14243
  const condText = node.getCondition()?.getText() ?? "";
14244
+ const bodyText = body.getText();
14245
+ const hasAwait = /\bawait\b/.test(bodyText);
14246
+ const hasCpuWork = /Math\.|\.sqrt|\.pow|\.random|\.toString|\.charCodeAt|parseInt|parseFloat|\+\+|--|\*|\//.test(bodyText);
14078
14247
  const largeBoundMatch = condText.match(/[<]=?\s*(\d+)/);
14079
14248
  if (largeBoundMatch) {
14080
14249
  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
- }
14250
+ if (bound >= 1e4 && !hasAwait && hasCpuWork) {
14251
+ 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"));
14252
+ return;
14253
+ }
14254
+ }
14255
+ if (!largeBoundMatch && !hasAwait && hasCpuWork) {
14256
+ const dynamicBound = condText.match(/[<]=?\s*([a-zA-Z_]\w*)/);
14257
+ if (dynamicBound) {
14258
+ 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"));
14259
+ return;
14088
14260
  }
14089
14261
  }
14090
14262
  }
@@ -14364,7 +14536,13 @@ var require_perf_pattern_detector = __commonJS({
14364
14536
  }
14365
14537
  if (ts_morph_1.Node.isIdentifier(obj)) {
14366
14538
  const name = obj.getText();
14367
- return name.replace(/Repo(sitory)?|Service|Model|Dao$/i, "").toLowerCase() || void 0;
14539
+ const stripped = name.replace(/Repo(sitory)?|Service|Model|Dao$/i, "");
14540
+ if (stripped && stripped !== name)
14541
+ return stripped.toLowerCase();
14542
+ const sourceFile = callExpr.getSourceFile();
14543
+ const fromVar = extractEntityFromVariable(name, sourceFile);
14544
+ if (fromVar)
14545
+ return fromVar;
14368
14546
  }
14369
14547
  return void 0;
14370
14548
  }
@@ -14405,6 +14583,56 @@ var require_perf_pattern_detector = __commonJS({
14405
14583
  }
14406
14584
  return void 0;
14407
14585
  }
14586
+ function extractEntityAny(callExpr, sourceFile) {
14587
+ let entity = extractEntity(callExpr);
14588
+ if (entity)
14589
+ return entity;
14590
+ entity = extractTypeORMEntity(callExpr);
14591
+ if (entity)
14592
+ return entity;
14593
+ entity = extractSequelizeEntity(callExpr);
14594
+ if (entity)
14595
+ return entity;
14596
+ const expr = callExpr.getExpression();
14597
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
14598
+ const obj = expr.getExpression();
14599
+ if (ts_morph_1.Node.isIdentifier(obj)) {
14600
+ entity = extractEntityFromVariable(obj.getText(), sourceFile);
14601
+ if (entity)
14602
+ return entity;
14603
+ }
14604
+ if (ts_morph_1.Node.isPropertyAccessExpression(obj)) {
14605
+ const propName = obj.getName();
14606
+ entity = extractEntityFromVariable(propName, sourceFile);
14607
+ if (entity)
14608
+ return entity;
14609
+ const stripped = propName.replace(/Repo(sitory)?$/i, "");
14610
+ if (stripped && stripped !== propName)
14611
+ return stripped.toLowerCase();
14612
+ }
14613
+ }
14614
+ return void 0;
14615
+ }
14616
+ function extractEntityFromVariable(varName, sourceFile) {
14617
+ const fileText = sourceFile.getFullText();
14618
+ const typeAnnotation = new RegExp(`\\b${escapeRegex(varName)}\\s*[:\\!]\\s*Repository\\s*<\\s*(\\w+)\\s*>`);
14619
+ const typeMatch = fileText.match(typeAnnotation);
14620
+ if (typeMatch)
14621
+ return typeMatch[1];
14622
+ const asTypeAssert = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*[^;]*\\bas\\s+Repository\\s*<\\s*(\\w+)\\s*>`);
14623
+ const asMatch = fileText.match(asTypeAssert);
14624
+ if (asMatch)
14625
+ return asMatch[1];
14626
+ const getRepoAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*getRepository\\s*\\(\\s*(\\w+)\\s*\\)`);
14627
+ const getRepoMatch = fileText.match(getRepoAssign);
14628
+ if (getRepoMatch)
14629
+ return getRepoMatch[1];
14630
+ const connRepoAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*\\w+\\.getRepository\\s*\\(\\s*(\\w+)\\s*\\)`);
14631
+ const connMatch = fileText.match(connRepoAssign);
14632
+ if (connMatch)
14633
+ return connMatch[1];
14634
+ return void 0;
14635
+ }
14408
14636
  function extractSequelizeEntity(callExpr) {
14409
14637
  const expr = callExpr.getExpression();
14410
14638
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
@@ -15079,15 +15307,30 @@ var require_perf_pattern_detector = __commonJS({
15079
15307
  const relationsProp = objArg.getProperty("relations");
15080
15308
  if (relationsProp) {
15081
15309
  const relText = relationsProp.getText();
15082
- const dotMatches = relText.match(/['"][^'"]*\.[^'"]*\.[^'"]*['"]/g);
15083
- if (dotMatches && dotMatches.length > 0) {
15310
+ const allRelations = relText.match(/['"][^'"]+['"]/g) ?? [];
15311
+ const dottedRelations = allRelations.filter((m) => m.includes("."));
15312
+ const totalRelCount = allRelations.length;
15313
+ const maxDepth = allRelations.length > 0 ? Math.max(...allRelations.map((m) => m.replace(/['"]/g, "").split(".").length)) : 0;
15314
+ if (dottedRelations.length > 0 || totalRelCount >= 4) {
15084
15315
  let entity = extractEntity(node);
15085
15316
  if (!entity)
15086
15317
  entity = extractTypeORMEntity(node);
15087
15318
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15088
15319
  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"));
15320
+ 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"));
15321
+ }
15322
+ const seqIncludeProp = objArg.getProperty("include");
15323
+ if (seqIncludeProp) {
15324
+ const seqIncText = seqIncludeProp.getText();
15325
+ const nestCount = countNestedIncludes(seqIncText);
15326
+ if (nestCount >= 2) {
15327
+ let entity = extractEntity(node);
15328
+ if (!entity)
15329
+ entity = extractSequelizeEntity(node);
15330
+ const vol = entity ? resolveVolume(entity, policy) : void 0;
15331
+ const fn = getEnclosingFn(node, fns);
15332
+ 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"));
15333
+ }
15091
15334
  }
15092
15335
  }
15093
15336
  });
@@ -15134,6 +15377,7 @@ var require_perf_pattern_detector = __commonJS({
15134
15377
  "countDocuments",
15135
15378
  "estimatedDocumentCount"
15136
15379
  ]);
15380
+ var DB_CALL_METHOD_PATTERN_EXTENDED = /^(find|get|fetch|load|search|query|count|list)(By|All|Many|One|First|Where|With)?/i;
15137
15381
  function detectNPlusOne(sourceFile, filePath, fns, policy, violations) {
15138
15382
  sourceFile.forEachDescendant((node) => {
15139
15383
  if (!ts_morph_1.Node.isAwaitExpression(node))
@@ -15147,7 +15391,7 @@ var require_perf_pattern_detector = __commonJS({
15147
15391
  const callMethodName = callExpr.getName();
15148
15392
  let isDrizzleOrKnexChain = false;
15149
15393
  let chainEntity;
15150
- if (!DB_CALL_METHODS.has(callMethodName)) {
15394
+ if (!DB_CALL_METHODS.has(callMethodName) && !DB_CALL_METHOD_PATTERN_EXTENDED.test(callMethodName)) {
15151
15395
  const drizzle = traceDrizzleChain(awaited);
15152
15396
  if (drizzle.isDrizzle) {
15153
15397
  isDrizzleOrKnexChain = true;
@@ -15235,7 +15479,7 @@ var require_perf_pattern_detector = __commonJS({
15235
15479
  });
15236
15480
  });
15237
15481
  }
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*\(/;
15482
+ 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
15483
  function detectRecursiveNPlusOne(sourceFile, filePath, fns, policy, violations) {
15240
15484
  const DB_METHOD_NAMES = /* @__PURE__ */ new Set([
15241
15485
  "find",
@@ -15307,6 +15551,7 @@ var require_perf_pattern_detector = __commonJS({
15307
15551
  }
15308
15552
  return false;
15309
15553
  }
15554
+ var IN_MEMORY_OPERATIONS = /* @__PURE__ */ new Set(["filter", "sort", "map", "reduce", "flatMap"]);
15310
15555
  function detectFetchAllFilterMemory(sourceFile, filePath, fns, policy, violations) {
15311
15556
  sourceFile.forEachDescendant((node) => {
15312
15557
  if (!ts_morph_1.Node.isCallExpression(node))
@@ -15314,7 +15559,7 @@ var require_perf_pattern_detector = __commonJS({
15314
15559
  const expr = node.getExpression();
15315
15560
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
15316
15561
  return;
15317
- if (expr.getName() !== "filter")
15562
+ if (!IN_MEMORY_OPERATIONS.has(expr.getName()))
15318
15563
  return;
15319
15564
  const obj = expr.getExpression();
15320
15565
  if (ts_morph_1.Node.isParenthesizedExpression(obj)) {
@@ -15342,26 +15587,65 @@ var require_perf_pattern_detector = __commonJS({
15342
15587
  const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
15343
15588
  if (!block)
15344
15589
  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];
15590
+ let foundDbAssignment = false;
15591
+ let entity;
15592
+ block.forEachDescendant((child) => {
15593
+ if (foundDbAssignment)
15594
+ return;
15595
+ if (!ts_morph_1.Node.isVariableDeclaration(child))
15596
+ return;
15597
+ if (child.getName() !== varName)
15598
+ return;
15599
+ if (child.getStart() >= node.getStart())
15600
+ return;
15601
+ const init = child.getInitializer();
15602
+ if (!init)
15603
+ return;
15604
+ let awaitedCall;
15605
+ if (ts_morph_1.Node.isAwaitExpression(init)) {
15606
+ const awaited = init.getExpression();
15607
+ if (ts_morph_1.Node.isCallExpression(awaited))
15608
+ awaitedCall = awaited;
15609
+ } else if (ts_morph_1.Node.isCallExpression(init)) {
15610
+ awaitedCall = init;
15359
15611
  }
15360
- if (!entity) {
15361
- const typeormMatch = textBefore.match(/getRepository\(\s*(\w+)\s*\)\.(findMany|find|getMany)\s*\(/);
15362
- if (typeormMatch)
15363
- entity = typeormMatch[1];
15612
+ if (!awaitedCall)
15613
+ return;
15614
+ const callExpr = awaitedCall.getExpression();
15615
+ if (!ts_morph_1.Node.isPropertyAccessExpression(callExpr))
15616
+ return;
15617
+ const methodName = callExpr.getName();
15618
+ const dbMethods = /* @__PURE__ */ new Set([
15619
+ "findMany",
15620
+ "find",
15621
+ "getMany",
15622
+ "findAll",
15623
+ "findAndCountAll",
15624
+ "from",
15625
+ "select",
15626
+ "getAll",
15627
+ "fetchAll",
15628
+ "findBy",
15629
+ "query"
15630
+ ]);
15631
+ if (!dbMethods.has(methodName) && !/^(find|get|fetch|load)(By|All|Many)/.test(methodName))
15632
+ return;
15633
+ foundDbAssignment = true;
15634
+ entity = extractEntityAny(awaitedCall, sourceFile);
15635
+ });
15636
+ if (!foundDbAssignment) {
15637
+ const blockText = block.getText();
15638
+ const nodePos = node.getStart() - block.getStart();
15639
+ const textBefore = blockText.substring(0, nodePos);
15640
+ const assignPattern = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+[\\w.]+\\.(findMany|find|getMany|findAll|findBy\\w+|getAll|fetchAll|from|select)\\s*\\(`);
15641
+ if (assignPattern.test(textBefore)) {
15642
+ foundDbAssignment = true;
15643
+ const chain = textBefore.match(/(\w+)\.(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
15644
+ if (chain)
15645
+ entity = chain[2];
15364
15646
  }
15647
+ }
15648
+ if (foundDbAssignment) {
15365
15649
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15366
15650
  const fn = getEnclosingFn(node, fns);
15367
15651
  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 +15717,11 @@ var require_perf_pattern_detector = __commonJS({
15433
15717
  if (/\.(controller|handler|route|endpoint)\./i.test(fp))
15434
15718
  return true;
15435
15719
  }
15720
+ if (ts_morph_1.Node.isMethodDeclaration(node)) {
15721
+ const fp = node.getSourceFile().getFilePath();
15722
+ if (/\.(controller|handler)\./i.test(fp))
15723
+ return true;
15724
+ }
15436
15725
  return false;
15437
15726
  }
15438
15727
  function detectMissingPagination(sourceFile, filePath, fns, policy, violations) {
@@ -15445,7 +15734,17 @@ var require_perf_pattern_detector = __commonJS({
15445
15734
  if (/page|skip|offset|cursor|take|limit|per_page|pageSize/i.test(bodyText))
15446
15735
  continue;
15447
15736
  const entityMatch = bodyText.match(/(\w+)\.(findMany|find|getMany|findAll)\s*\(/);
15448
- const entity = entityMatch ? entityMatch[1] : void 0;
15737
+ let entity = entityMatch ? entityMatch[1] : void 0;
15738
+ if (entity) {
15739
+ const stripped = entity.replace(/Repo(sitory)?|Service|Model|Dao$/i, "");
15740
+ if (stripped && stripped !== entity)
15741
+ entity = stripped;
15742
+ }
15743
+ if (entity) {
15744
+ const varEntity = extractEntityFromVariable(entity, sourceFile);
15745
+ if (varEntity)
15746
+ entity = varEntity;
15747
+ }
15449
15748
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15450
15749
  if (vol && vol.size === "S")
15451
15750
  continue;
@@ -15534,6 +15833,13 @@ var require_perf_pattern_detector = __commonJS({
15534
15833
  const args = node.getArguments();
15535
15834
  if (args.length > 0 && argsContainKey(node, "where"))
15536
15835
  return;
15836
+ {
15837
+ const countObj = expr.getExpression();
15838
+ if (ts_morph_1.Node.isIdentifier(countObj) && /^(em|entityManager|manager|orm)$/i.test(countObj.getText())) {
15839
+ if (args.length > 1)
15840
+ return;
15841
+ }
15842
+ }
15537
15843
  let entity = extractEntity(node);
15538
15844
  if (!entity)
15539
15845
  entity = extractTypeORMEntity(node);
@@ -15587,7 +15893,7 @@ var require_perf_pattern_detector = __commonJS({
15587
15893
  const args = node.getArguments();
15588
15894
  if (args.length === 0)
15589
15895
  return;
15590
- const isUnsafe = method === "$queryRawUnsafe" || method === "$executeRawUnsafe";
15896
+ let isUnsafe = method === "$queryRawUnsafe" || method === "$executeRawUnsafe";
15591
15897
  let sqlText = "";
15592
15898
  const firstArg = args[0];
15593
15899
  if (ts_morph_1.Node.isStringLiteral(firstArg)) {
@@ -15596,6 +15902,7 @@ var require_perf_pattern_detector = __commonJS({
15596
15902
  sqlText = firstArg.getText();
15597
15903
  } else if (ts_morph_1.Node.isBinaryExpression(firstArg)) {
15598
15904
  sqlText = firstArg.getText();
15905
+ isUnsafe = true;
15599
15906
  }
15600
15907
  if (sqlText) {
15601
15908
  checkSqlForLimit(sqlText, filePath, node.getStartLineNumber(), fns, policy, violations, node, isUnsafe);
@@ -15616,6 +15923,21 @@ var require_perf_pattern_detector = __commonJS({
15616
15923
  debtPoints: policy.scoring.performance_risk_critical,
15617
15924
  gateAction: "block"
15618
15925
  });
15926
+ violations.push({
15927
+ category: "performance",
15928
+ type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15929
+ ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15930
+ severity: "critical",
15931
+ source: "deterministic",
15932
+ confidence: "high",
15933
+ file: filePath,
15934
+ line: node.getStartLineNumber(),
15935
+ function: fn?.name,
15936
+ message: `${method}() used \u2014 SQL injection risk via string interpolation/concatenation`,
15937
+ suggestion: "Use $queryRaw with tagged template literals and parameterized queries instead",
15938
+ debtPoints: policy.scoring.performance_risk_critical,
15939
+ gateAction: "block"
15940
+ });
15619
15941
  }
15620
15942
  }
15621
15943
  if (ts_morph_1.Node.isTaggedTemplateExpression(node)) {
@@ -15638,15 +15960,15 @@ var require_perf_pattern_detector = __commonJS({
15638
15960
  const fn2 = getEnclosingFn(node, fns);
15639
15961
  violations.push({
15640
15962
  category: "performance",
15641
- type: shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT,
15642
- ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT,
15963
+ type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15964
+ ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15643
15965
  severity: "critical",
15644
15966
  source: "deterministic",
15645
15967
  confidence: "high",
15646
15968
  file: filePath,
15647
15969
  line,
15648
15970
  function: fn2?.name,
15649
- message: `$queryRawUnsafe() used \u2014 SQL injection risk and unbounded query`,
15971
+ message: `$queryRawUnsafe() used \u2014 SQL injection risk`,
15650
15972
  suggestion: "Use $queryRaw with tagged template literals instead of $queryRawUnsafe",
15651
15973
  debtPoints: policy.scoring.performance_risk_critical,
15652
15974
  gateAction: "block"
@@ -15664,22 +15986,24 @@ var require_perf_pattern_detector = __commonJS({
15664
15986
  const vol = entity ? resolveVolume(entity, policy) : void 0;
15665
15987
  const fn = getEnclosingFn(node, fns);
15666
15988
  if (isUnsafe) {
15667
- const severity = hasLimit ? "warning" : "critical";
15668
15989
  violations.push({
15669
15990
  category: "performance",
15670
- type: shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT,
15671
- ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_NO_LIMIT,
15672
- severity,
15991
+ type: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15992
+ ruleId: shared_1.PERFORMANCE_RULES.RAW_SQL_UNSAFE,
15993
+ severity: "critical",
15673
15994
  source: "deterministic",
15674
15995
  confidence: "high",
15675
15996
  file: filePath,
15676
15997
  line,
15677
15998
  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`,
15999
+ message: `$queryRawUnsafe() used${vol ? ` on '${entity}'` : ""} \u2014 SQL injection risk`,
15679
16000
  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"
16001
+ debtPoints: policy.scoring.performance_risk_critical,
16002
+ gateAction: "block"
15682
16003
  });
16004
+ if (!hasLimit) {
16005
+ 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"));
16006
+ }
15683
16007
  } else {
15684
16008
  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
16009
  }
@@ -15788,7 +16112,7 @@ var require_reliability_detector = __commonJS({
15788
16112
  function isNestJSInjectable(classNode) {
15789
16113
  return classNode.getDecorators().some((d) => {
15790
16114
  const name = d.getName();
15791
- return name === "Injectable" || name === "Controller" || name === "Resolver";
16115
+ return name === "Injectable";
15792
16116
  });
15793
16117
  }
15794
16118
  function isInsideNestJSInjectable(node) {
@@ -16069,6 +16393,12 @@ var require_reliability_detector = __commonJS({
16069
16393
  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
16394
  return;
16071
16395
  }
16396
+ 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;
16397
+ if (ASYNC_METHOD_PATTERNS.test(calledName)) {
16398
+ const fn = getEnclosingFn(node, fns);
16399
+ 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()`));
16400
+ return;
16401
+ }
16072
16402
  } else if (ts_morph_1.Node.isIdentifier(callExpr)) {
16073
16403
  calledName = callExpr.getText();
16074
16404
  const matchFn = fns.find((f) => f.name === calledName && f.isAsync);
@@ -16276,6 +16606,7 @@ var require_reliability_detector = __commonJS({
16276
16606
  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
16607
  });
16278
16608
  }
16609
+ var TRANSACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["$transaction", "transaction"]);
16279
16610
  function detectTransactionNoTimeout(sourceFile, filePath, fns, policy, violations) {
16280
16611
  sourceFile.forEachDescendant((node) => {
16281
16612
  if (!ts_morph_1.Node.isCallExpression(node))
@@ -16283,7 +16614,7 @@ var require_reliability_detector = __commonJS({
16283
16614
  const expr = node.getExpression();
16284
16615
  if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
16285
16616
  return;
16286
- if (expr.getName() !== "$transaction")
16617
+ if (!TRANSACTION_METHOD_NAMES.has(expr.getName()))
16287
16618
  return;
16288
16619
  const args = node.getArguments();
16289
16620
  const hasTimeout = args.some((arg) => {
@@ -16299,6 +16630,8 @@ var require_reliability_detector = __commonJS({
16299
16630
  });
16300
16631
  }
16301
16632
  function detectMissingNullGuard(sourceFile, filePath, fns, policy, violations) {
16633
+ detectNonNullAssertionOnNullable(sourceFile, filePath, fns, policy, violations);
16634
+ detectUnsafeArrayIndexAccess(sourceFile, filePath, fns, policy, violations);
16302
16635
  sourceFile.forEachDescendant((node) => {
16303
16636
  if (!ts_morph_1.Node.isVariableDeclaration(node))
16304
16637
  return;
@@ -16352,6 +16685,62 @@ var require_reliability_detector = __commonJS({
16352
16685
  }
16353
16686
  });
16354
16687
  }
16688
+ function detectNonNullAssertionOnNullable(sourceFile, filePath, fns, policy, violations) {
16689
+ sourceFile.forEachDescendant((node) => {
16690
+ if (!ts_morph_1.Node.isNonNullExpression(node))
16691
+ return;
16692
+ const inner = node.getExpression();
16693
+ if (!ts_morph_1.Node.isIdentifier(inner))
16694
+ return;
16695
+ const varName = inner.getText();
16696
+ const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
16697
+ if (!block)
16698
+ return;
16699
+ const blockText = block.getText();
16700
+ const nodePos = node.getStart() - block.getStart();
16701
+ const textBefore = blockText.substring(0, nodePos);
16702
+ const nullableAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+\\S+\\.(findUnique|findFirst|findOne|findByPk)\\s*\\(`);
16703
+ if (!nullableAssign.test(textBefore))
16704
+ return;
16705
+ const fn = getEnclosingFn(node, fns);
16706
+ 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();`));
16707
+ });
16708
+ }
16709
+ function detectUnsafeArrayIndexAccess(sourceFile, filePath, fns, policy, violations) {
16710
+ sourceFile.forEachDescendant((node) => {
16711
+ if (!ts_morph_1.Node.isElementAccessExpression(node))
16712
+ return;
16713
+ const obj = node.getExpression();
16714
+ if (!ts_morph_1.Node.isIdentifier(obj))
16715
+ return;
16716
+ const varName = obj.getText();
16717
+ const indexArg = node.getArgumentExpression();
16718
+ if (!indexArg || !ts_morph_1.Node.isNumericLiteral(indexArg))
16719
+ return;
16720
+ const parent = node.getParent();
16721
+ if (!parent || !ts_morph_1.Node.isPropertyAccessExpression(parent))
16722
+ return;
16723
+ const block = node.getFirstAncestorByKind(ts_morph_1.SyntaxKind.Block);
16724
+ if (!block)
16725
+ return;
16726
+ const blockText = block.getText();
16727
+ const nodePos = node.getStart() - block.getStart();
16728
+ const textBefore = blockText.substring(0, nodePos);
16729
+ const arrayAssign = new RegExp(`\\b${escapeRegex(varName)}\\s*=\\s*await\\s+\\S+\\.(findMany|find|findAll|getMany|query)\\s*\\(`);
16730
+ if (!arrayAssign.test(textBefore))
16731
+ return;
16732
+ const lengthGuard = new RegExp(`${escapeRegex(varName)}\\s*\\.\\s*length|${escapeRegex(varName)}\\s*\\[\\s*0\\s*\\]\\s*&&|if\\s*\\(\\s*!?${escapeRegex(varName)}`);
16733
+ const assignMatch = textBefore.match(arrayAssign);
16734
+ if (assignMatch) {
16735
+ const assignEnd = textBefore.indexOf(assignMatch[0]) + assignMatch[0].length;
16736
+ const textBetween = textBefore.substring(assignEnd);
16737
+ if (lengthGuard.test(textBetween))
16738
+ return;
16739
+ }
16740
+ const fn = getEnclosingFn(node, fns);
16741
+ 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();`));
16742
+ });
16743
+ }
16355
16744
  function escapeRegex(str) {
16356
16745
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16357
16746
  }
@@ -18054,6 +18443,7 @@ var require_orchestrator = __commonJS({
18054
18443
  crossFileAnalysis = await (0, cross_file_analyzer_1.analyzeCrossFile)(unflaggedPatterns, importGraph, filteredInput, policy);
18055
18444
  }
18056
18445
  const complexityViolations = complexityDeltasToViolations(complexityDeltas, filteredInput);
18446
+ const largeFileViolations = detectLargeFiles(filteredInput);
18057
18447
  const violations = [
18058
18448
  ...boundaryViolations,
18059
18449
  ...circularViolations,
@@ -18061,6 +18451,7 @@ var require_orchestrator = __commonJS({
18061
18451
  ...perfViolations,
18062
18452
  ...reliabilityViolations,
18063
18453
  ...complexityViolations,
18454
+ ...largeFileViolations,
18064
18455
  ...duplicationResult.violations,
18065
18456
  ...missingTestsResult?.violations ?? [],
18066
18457
  ...deadCodeResult.violations,
@@ -18078,6 +18469,34 @@ var require_orchestrator = __commonJS({
18078
18469
  crossFileAnalysis
18079
18470
  };
18080
18471
  }
18472
+ function detectLargeFiles(input) {
18473
+ const threshold = shared_1.DEFAULT_LARGE_FILE_THRESHOLD;
18474
+ const violations = [];
18475
+ for (const file of input.changedFiles) {
18476
+ if (file.status === "deleted")
18477
+ continue;
18478
+ if (!/\.(ts|tsx|js|jsx)$/.test(file.path))
18479
+ continue;
18480
+ const lineCount = file.content.split("\n").length;
18481
+ if (lineCount <= threshold)
18482
+ continue;
18483
+ violations.push({
18484
+ category: "maintainability",
18485
+ type: shared_1.MAINTAINABILITY_RULES.LARGE_FILE,
18486
+ ruleId: shared_1.MAINTAINABILITY_RULES.LARGE_FILE,
18487
+ severity: "warning",
18488
+ source: "deterministic",
18489
+ confidence: "high",
18490
+ file: file.path,
18491
+ line: 1,
18492
+ message: `File has ${lineCount} lines (threshold: ${threshold}) \u2014 consider splitting into smaller modules`,
18493
+ suggestion: "Extract helper functions, types, or constants into separate files",
18494
+ debtPoints: Math.floor((lineCount - threshold) / 50) + 1,
18495
+ gateAction: "warn"
18496
+ });
18497
+ }
18498
+ return violations;
18499
+ }
18081
18500
  function complexityDeltasToViolations(deltas, input) {
18082
18501
  const threshold = shared_1.DEFAULT_COMPLEXITY_THRESHOLD;
18083
18502
  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.4",
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",