tech-debt-score 0.1.0

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 (135) hide show
  1. package/DEVELOPMENT.md +147 -0
  2. package/LICENSE +21 -0
  3. package/README.md +200 -0
  4. package/SETUP_COMPLETE.md +188 -0
  5. package/TECHNICAL_DESIGN.md +563 -0
  6. package/dist/adapters/input/FileSystemReader.d.ts +11 -0
  7. package/dist/adapters/input/FileSystemReader.d.ts.map +1 -0
  8. package/dist/adapters/input/FileSystemReader.js +41 -0
  9. package/dist/adapters/input/FileSystemReader.js.map +1 -0
  10. package/dist/adapters/input/TypeScriptParser.d.ts +17 -0
  11. package/dist/adapters/input/TypeScriptParser.d.ts.map +1 -0
  12. package/dist/adapters/input/TypeScriptParser.js +305 -0
  13. package/dist/adapters/input/TypeScriptParser.js.map +1 -0
  14. package/dist/adapters/output/JsonExporter.d.ts +11 -0
  15. package/dist/adapters/output/JsonExporter.d.ts.map +1 -0
  16. package/dist/adapters/output/JsonExporter.js +46 -0
  17. package/dist/adapters/output/JsonExporter.js.map +1 -0
  18. package/dist/adapters/output/TerminalReporter.d.ts +13 -0
  19. package/dist/adapters/output/TerminalReporter.d.ts.map +1 -0
  20. package/dist/adapters/output/TerminalReporter.js +82 -0
  21. package/dist/adapters/output/TerminalReporter.js.map +1 -0
  22. package/dist/application/config/AnalysisConfig.d.ts +36 -0
  23. package/dist/application/config/AnalysisConfig.d.ts.map +1 -0
  24. package/dist/application/config/AnalysisConfig.js +19 -0
  25. package/dist/application/config/AnalysisConfig.js.map +1 -0
  26. package/dist/application/ports/IFileReader.d.ts +32 -0
  27. package/dist/application/ports/IFileReader.d.ts.map +1 -0
  28. package/dist/application/ports/IFileReader.js +6 -0
  29. package/dist/application/ports/IFileReader.js.map +1 -0
  30. package/dist/application/ports/IParser.d.ts +35 -0
  31. package/dist/application/ports/IParser.d.ts.map +1 -0
  32. package/dist/application/ports/IParser.js +6 -0
  33. package/dist/application/ports/IParser.js.map +1 -0
  34. package/dist/application/ports/IReporter.d.ts +24 -0
  35. package/dist/application/ports/IReporter.d.ts.map +1 -0
  36. package/dist/application/ports/IReporter.js +6 -0
  37. package/dist/application/ports/IReporter.js.map +1 -0
  38. package/dist/application/services/AnalysisService.d.ts +31 -0
  39. package/dist/application/services/AnalysisService.d.ts.map +1 -0
  40. package/dist/application/services/AnalysisService.js +161 -0
  41. package/dist/application/services/AnalysisService.js.map +1 -0
  42. package/dist/application/services/DependencyAnalyzer.d.ts +29 -0
  43. package/dist/application/services/DependencyAnalyzer.d.ts.map +1 -0
  44. package/dist/application/services/DependencyAnalyzer.js +128 -0
  45. package/dist/application/services/DependencyAnalyzer.js.map +1 -0
  46. package/dist/application/services/DuplicationDetector.d.ts +46 -0
  47. package/dist/application/services/DuplicationDetector.d.ts.map +1 -0
  48. package/dist/application/services/DuplicationDetector.js +165 -0
  49. package/dist/application/services/DuplicationDetector.js.map +1 -0
  50. package/dist/cli/commands/analyze.d.ts +2 -0
  51. package/dist/cli/commands/analyze.d.ts.map +1 -0
  52. package/dist/cli/commands/analyze.js +58 -0
  53. package/dist/cli/commands/analyze.js.map +1 -0
  54. package/dist/cli/index.d.ts +9 -0
  55. package/dist/cli/index.d.ts.map +1 -0
  56. package/dist/cli/index.js +76 -0
  57. package/dist/cli/index.js.map +1 -0
  58. package/dist/domain/entities/Finding.d.ts +42 -0
  59. package/dist/domain/entities/Finding.d.ts.map +1 -0
  60. package/dist/domain/entities/Finding.js +40 -0
  61. package/dist/domain/entities/Finding.js.map +1 -0
  62. package/dist/domain/entities/Metric.d.ts +38 -0
  63. package/dist/domain/entities/Metric.d.ts.map +1 -0
  64. package/dist/domain/entities/Metric.js +36 -0
  65. package/dist/domain/entities/Metric.js.map +1 -0
  66. package/dist/domain/entities/Rule.d.ts +41 -0
  67. package/dist/domain/entities/Rule.d.ts.map +1 -0
  68. package/dist/domain/entities/Rule.js +15 -0
  69. package/dist/domain/entities/Rule.js.map +1 -0
  70. package/dist/domain/entities/Score.d.ts +62 -0
  71. package/dist/domain/entities/Score.d.ts.map +1 -0
  72. package/dist/domain/entities/Score.js +40 -0
  73. package/dist/domain/entities/Score.js.map +1 -0
  74. package/dist/domain/index.d.ts +12 -0
  75. package/dist/domain/index.d.ts.map +1 -0
  76. package/dist/domain/index.js +14 -0
  77. package/dist/domain/index.js.map +1 -0
  78. package/dist/domain/rules/CircularDependencyRule.d.ts +13 -0
  79. package/dist/domain/rules/CircularDependencyRule.d.ts.map +1 -0
  80. package/dist/domain/rules/CircularDependencyRule.js +47 -0
  81. package/dist/domain/rules/CircularDependencyRule.js.map +1 -0
  82. package/dist/domain/rules/ComplexityRule.d.ts +15 -0
  83. package/dist/domain/rules/ComplexityRule.d.ts.map +1 -0
  84. package/dist/domain/rules/ComplexityRule.js +63 -0
  85. package/dist/domain/rules/ComplexityRule.js.map +1 -0
  86. package/dist/domain/rules/DuplicationRule.d.ts +15 -0
  87. package/dist/domain/rules/DuplicationRule.d.ts.map +1 -0
  88. package/dist/domain/rules/DuplicationRule.js +51 -0
  89. package/dist/domain/rules/DuplicationRule.js.map +1 -0
  90. package/dist/domain/rules/SizeRule.d.ts +16 -0
  91. package/dist/domain/rules/SizeRule.d.ts.map +1 -0
  92. package/dist/domain/rules/SizeRule.js +70 -0
  93. package/dist/domain/rules/SizeRule.js.map +1 -0
  94. package/dist/domain/rules/TypeSafetyRule.d.ts +13 -0
  95. package/dist/domain/rules/TypeSafetyRule.d.ts.map +1 -0
  96. package/dist/domain/rules/TypeSafetyRule.js +45 -0
  97. package/dist/domain/rules/TypeSafetyRule.js.map +1 -0
  98. package/dist/index.d.ts +2 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +2 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/shared/types.d.ts +16 -0
  103. package/dist/shared/types.d.ts.map +1 -0
  104. package/dist/shared/types.js +5 -0
  105. package/dist/shared/types.js.map +1 -0
  106. package/package.json +47 -0
  107. package/src/adapters/input/FileSystemReader.ts +51 -0
  108. package/src/adapters/input/TypeScriptParser.ts +367 -0
  109. package/src/adapters/output/JsonExporter.ts +48 -0
  110. package/src/adapters/output/TerminalReporter.ts +88 -0
  111. package/src/application/config/AnalysisConfig.ts +50 -0
  112. package/src/application/ports/IFileReader.ts +36 -0
  113. package/src/application/ports/IParser.ts +40 -0
  114. package/src/application/ports/IReporter.ts +26 -0
  115. package/src/application/services/AnalysisService.ts +199 -0
  116. package/src/application/services/DependencyAnalyzer.ts +158 -0
  117. package/src/application/services/DuplicationDetector.ts +207 -0
  118. package/src/cli/commands/analyze.ts +76 -0
  119. package/src/cli/index.ts +81 -0
  120. package/src/domain/entities/Finding.ts +79 -0
  121. package/src/domain/entities/Metric.ts +70 -0
  122. package/src/domain/entities/Rule.ts +49 -0
  123. package/src/domain/entities/Score.ts +94 -0
  124. package/src/domain/index.ts +15 -0
  125. package/src/domain/rules/CircularDependencyRule.ts +65 -0
  126. package/src/domain/rules/ComplexityRule.ts +88 -0
  127. package/src/domain/rules/DuplicationRule.ts +70 -0
  128. package/src/domain/rules/SizeRule.ts +98 -0
  129. package/src/domain/rules/TypeSafetyRule.ts +63 -0
  130. package/src/index.ts +0 -0
  131. package/src/shared/types.ts +18 -0
  132. package/tests/application/index.test.ts +12 -0
  133. package/tests/domain/index.test.ts +14 -0
  134. package/tests/e2e/index.test.ts +13 -0
  135. package/tsconfig.json +31 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Domain Rule: Complexity Rule
3
+ * Evaluates cyclomatic complexity and nesting depth
4
+ */
5
+ import { BaseRule } from '../entities/Rule.js';
6
+ import { FindingBuilder } from '../entities/Finding.js';
7
+ export class ComplexityRule extends BaseRule {
8
+ constructor() {
9
+ super('complexity', 'Complexity Rule', 'Evaluates cyclomatic complexity and nesting depth to identify overly complex code');
10
+ // Thresholds (configurable in future versions)
11
+ this.COMPLEXITY_THRESHOLD = 10;
12
+ this.NESTING_THRESHOLD = 4;
13
+ }
14
+ evaluate(metrics) {
15
+ const findings = [];
16
+ // Filter for complexity-related metrics
17
+ const complexityMetrics = metrics.filter(m => m.name === 'cyclomatic-complexity' || m.name === 'nesting-depth');
18
+ for (const metric of complexityMetrics) {
19
+ if (metric.name === 'cyclomatic-complexity' && metric.value > this.COMPLEXITY_THRESHOLD) {
20
+ const builder = new FindingBuilder()
21
+ .withRuleId(this.id)
22
+ .withSeverity(metric.value > 20 ? 'high' : 'medium')
23
+ .withMessage(`High cyclomatic complexity (${metric.value}) in ${metric.context || 'function'}`)
24
+ .withFilePath(metric.filePath)
25
+ .withSuggestion('Consider breaking this function into smaller, more focused functions');
26
+ if (metric.location) {
27
+ builder.withLocation(metric.location);
28
+ }
29
+ findings.push(builder.build());
30
+ }
31
+ if (metric.name === 'nesting-depth' && metric.value > this.NESTING_THRESHOLD) {
32
+ const builder = new FindingBuilder()
33
+ .withRuleId(this.id)
34
+ .withSeverity(metric.value > 6 ? 'high' : 'medium')
35
+ .withMessage(`Deep nesting (${metric.value} levels) in ${metric.context || 'function'}`)
36
+ .withFilePath(metric.filePath)
37
+ .withSuggestion('Reduce nesting by using early returns or extracting functions');
38
+ if (metric.location) {
39
+ builder.withLocation(metric.location);
40
+ }
41
+ findings.push(builder.build());
42
+ }
43
+ }
44
+ return findings;
45
+ }
46
+ calculateScore(findings) {
47
+ if (findings.length === 0)
48
+ return 100;
49
+ // Penalty based on severity
50
+ const penalty = findings.reduce((sum, finding) => {
51
+ switch (finding.severity) {
52
+ case 'high': return sum + 10;
53
+ case 'medium': return sum + 5;
54
+ case 'low': return sum + 2;
55
+ default: return sum;
56
+ }
57
+ }, 0);
58
+ // Cap the penalty and invert to get score
59
+ const score = Math.max(0, 100 - penalty);
60
+ return Math.round(score * 100) / 100;
61
+ }
62
+ }
63
+ //# sourceMappingURL=ComplexityRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComplexityRule.js","sourceRoot":"","sources":["../../../src/domain/rules/ComplexityRule.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Domain Rule: Code Duplication Rule
3
+ * Detects duplicate code blocks using token-based similarity
4
+ */
5
+ import { BaseRule } from '../entities/Rule.js';
6
+ import type { Metric } from '../entities/Metric.js';
7
+ import type { Finding } from '../entities/Finding.js';
8
+ export declare class DuplicationRule extends BaseRule {
9
+ private readonly MIN_DUPLICATE_TOKENS;
10
+ private readonly SIMILARITY_THRESHOLD;
11
+ constructor();
12
+ evaluate(metrics: Metric[]): Finding[];
13
+ calculateScore(findings: Finding[]): number;
14
+ }
15
+ //# sourceMappingURL=DuplicationRule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DuplicationRule.d.ts","sourceRoot":"","sources":["../../../src/domain/rules/DuplicationRule.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAGtD,qBAAa,eAAgB,SAAQ,QAAQ;IAE3C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAM;IAC3C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAQ;;IAU7C,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE;IA8BtC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM;CAgB5C"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Domain Rule: Code Duplication Rule
3
+ * Detects duplicate code blocks using token-based similarity
4
+ */
5
+ import { BaseRule } from '../entities/Rule.js';
6
+ import { FindingBuilder } from '../entities/Finding.js';
7
+ export class DuplicationRule extends BaseRule {
8
+ constructor() {
9
+ super('duplication', 'Code Duplication Rule', 'Detects duplicate or highly similar code blocks across the codebase');
10
+ // Thresholds
11
+ this.MIN_DUPLICATE_TOKENS = 50; // Minimum tokens to consider as duplication
12
+ this.SIMILARITY_THRESHOLD = 0.95; // 95% similarity
13
+ }
14
+ evaluate(metrics) {
15
+ const findings = [];
16
+ // Group metrics by type
17
+ const duplicationMetrics = metrics.filter(m => m.name === 'code-duplication');
18
+ for (const metric of duplicationMetrics) {
19
+ if (metric.value > 0) {
20
+ const severity = metric.value > 5 ? 'high' : metric.value > 2 ? 'medium' : 'low';
21
+ const builder = new FindingBuilder()
22
+ .withRuleId(this.id)
23
+ .withSeverity(severity)
24
+ .withMessage(`Found ${metric.value} duplicate code block(s) in ${metric.context || 'file'}`)
25
+ .withFilePath(metric.filePath)
26
+ .withSuggestion('Extract duplicate code into reusable functions or modules');
27
+ if (metric.location) {
28
+ builder.withLocation(metric.location);
29
+ }
30
+ findings.push(builder.build());
31
+ }
32
+ }
33
+ return findings;
34
+ }
35
+ calculateScore(findings) {
36
+ if (findings.length === 0)
37
+ return 100;
38
+ // Each duplication finding reduces score
39
+ const penalty = findings.reduce((sum, finding) => {
40
+ switch (finding.severity) {
41
+ case 'high': return sum + 15;
42
+ case 'medium': return sum + 8;
43
+ case 'low': return sum + 4;
44
+ default: return sum;
45
+ }
46
+ }, 0);
47
+ const score = Math.max(0, 100 - penalty);
48
+ return Math.round(score * 100) / 100;
49
+ }
50
+ }
51
+ //# sourceMappingURL=DuplicationRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DuplicationRule.js","sourceRoot":"","sources":["../../../src/domain/rules/DuplicationRule.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,OAAO,eAAgB,SAAQ,QAAQ;IAK3C;QACE,KAAK,CACH,aAAa,EACb,uBAAuB,EACvB,qEAAqE,CACtE,CAAC;QATJ,aAAa;QACI,yBAAoB,GAAG,EAAE,CAAC,CAAC,4CAA4C;QACvE,yBAAoB,GAAG,IAAI,CAAC,CAAC,iBAAiB;IAQ/D,CAAC;IAED,QAAQ,CAAC,OAAiB;QACxB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,wBAAwB;QACxB,MAAM,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,CAAC,CAAC;QAE9E,KAAK,MAAM,MAAM,IAAI,kBAAkB,EAAE,CAAC;YACxC,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;gBACrB,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;gBAEjF,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE;qBACjC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;qBACnB,YAAY,CAAC,QAAQ,CAAC;qBACtB,WAAW,CACV,SAAS,MAAM,CAAC,KAAK,+BAA+B,MAAM,CAAC,OAAO,IAAI,MAAM,EAAE,CAC/E;qBACA,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC;qBAC7B,cAAc,CAAC,2DAA2D,CAAC,CAAC;gBAE/E,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;oBACpB,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACxC,CAAC;gBAED,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,cAAc,CAAC,QAAmB;QAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,GAAG,CAAC;QAEtC,yCAAyC;QACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE;YAC/C,QAAQ,OAAO,CAAC,QAAQ,EAAE,CAAC;gBACzB,KAAK,MAAM,CAAC,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC;gBAC7B,KAAK,QAAQ,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,CAAC;gBAC9B,KAAK,KAAK,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,CAAC;gBAC3B,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC;YACtB,CAAC;QACH,CAAC,EAAE,CAAC,CAAC,CAAC;QAEN,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;IACvC,CAAC;CACF"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Domain Rule: Size Rule
3
+ * Evaluates file and function size metrics
4
+ */
5
+ import { BaseRule } from '../entities/Rule.js';
6
+ import type { Metric } from '../entities/Metric.js';
7
+ import type { Finding } from '../entities/Finding.js';
8
+ export declare class SizeRule extends BaseRule {
9
+ private readonly MAX_FILE_LINES;
10
+ private readonly MAX_FUNCTION_LINES;
11
+ private readonly MAX_PARAMETERS;
12
+ constructor();
13
+ evaluate(metrics: Metric[]): Finding[];
14
+ calculateScore(findings: Finding[]): number;
15
+ }
16
+ //# sourceMappingURL=SizeRule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SizeRule.d.ts","sourceRoot":"","sources":["../../../src/domain/rules/SizeRule.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAGtD,qBAAa,QAAS,SAAQ,QAAQ;IAEpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAO;IACtC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAM;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAK;;IAUpC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE;IA0DtC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM;CAe5C"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Domain Rule: Size Rule
3
+ * Evaluates file and function size metrics
4
+ */
5
+ import { BaseRule } from '../entities/Rule.js';
6
+ import { FindingBuilder } from '../entities/Finding.js';
7
+ export class SizeRule extends BaseRule {
8
+ constructor() {
9
+ super('size', 'Size Rule', 'Evaluates file and function size to identify overly large code units');
10
+ // Thresholds (configurable in future versions)
11
+ this.MAX_FILE_LINES = 300;
12
+ this.MAX_FUNCTION_LINES = 50;
13
+ this.MAX_PARAMETERS = 5;
14
+ }
15
+ evaluate(metrics) {
16
+ const findings = [];
17
+ const sizeMetrics = metrics.filter(m => m.name === 'file-length' || m.name === 'function-length' || m.name === 'parameter-count');
18
+ for (const metric of sizeMetrics) {
19
+ if (metric.name === 'file-length' && metric.value > this.MAX_FILE_LINES) {
20
+ findings.push(new FindingBuilder()
21
+ .withRuleId(this.id)
22
+ .withSeverity(metric.value > 500 ? 'high' : 'medium')
23
+ .withMessage(`Large file (${metric.value} lines)`)
24
+ .withFilePath(metric.filePath)
25
+ .withSuggestion('Consider splitting this file into smaller, more focused modules')
26
+ .build());
27
+ }
28
+ if (metric.name === 'function-length' && metric.value > this.MAX_FUNCTION_LINES) {
29
+ const builder = new FindingBuilder()
30
+ .withRuleId(this.id)
31
+ .withSeverity(metric.value > 100 ? 'high' : 'medium')
32
+ .withMessage(`Large function (${metric.value} lines) in ${metric.context || 'unknown'}`)
33
+ .withFilePath(metric.filePath)
34
+ .withSuggestion('Break this function into smaller, single-purpose functions');
35
+ if (metric.location) {
36
+ builder.withLocation(metric.location);
37
+ }
38
+ findings.push(builder.build());
39
+ }
40
+ if (metric.name === 'parameter-count' && metric.value > this.MAX_PARAMETERS) {
41
+ const builder = new FindingBuilder()
42
+ .withRuleId(this.id)
43
+ .withSeverity('medium')
44
+ .withMessage(`Too many parameters (${metric.value}) in ${metric.context || 'function'}`)
45
+ .withFilePath(metric.filePath)
46
+ .withSuggestion('Consider using an options object or builder pattern');
47
+ if (metric.location) {
48
+ builder.withLocation(metric.location);
49
+ }
50
+ findings.push(builder.build());
51
+ }
52
+ }
53
+ return findings;
54
+ }
55
+ calculateScore(findings) {
56
+ if (findings.length === 0)
57
+ return 100;
58
+ const penalty = findings.reduce((sum, finding) => {
59
+ switch (finding.severity) {
60
+ case 'high': return sum + 8;
61
+ case 'medium': return sum + 4;
62
+ case 'low': return sum + 2;
63
+ default: return sum;
64
+ }
65
+ }, 0);
66
+ const score = Math.max(0, 100 - penalty);
67
+ return Math.round(score * 100) / 100;
68
+ }
69
+ }
70
+ //# sourceMappingURL=SizeRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SizeRule.js","sourceRoot":"","sources":["../../../src/domain/rules/SizeRule.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Domain Rule: Type Safety Rule
3
+ * Evaluates TypeScript type safety indicators
4
+ */
5
+ import { BaseRule } from '../entities/Rule.js';
6
+ import type { Metric } from '../entities/Metric.js';
7
+ import type { Finding } from '../entities/Finding.js';
8
+ export declare class TypeSafetyRule extends BaseRule {
9
+ constructor();
10
+ evaluate(metrics: Metric[]): Finding[];
11
+ calculateScore(findings: Finding[]): number;
12
+ }
13
+ //# sourceMappingURL=TypeSafetyRule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TypeSafetyRule.d.ts","sourceRoot":"","sources":["../../../src/domain/rules/TypeSafetyRule.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAGtD,qBAAa,cAAe,SAAQ,QAAQ;;IAS1C,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE;IA2BtC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM;CAgB5C"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Domain Rule: Type Safety Rule
3
+ * Evaluates TypeScript type safety indicators
4
+ */
5
+ import { BaseRule } from '../entities/Rule.js';
6
+ import { FindingBuilder } from '../entities/Finding.js';
7
+ export class TypeSafetyRule extends BaseRule {
8
+ constructor() {
9
+ super('type-safety', 'Type Safety Rule', 'Evaluates TypeScript type safety by detecting usage of the `any` type');
10
+ }
11
+ evaluate(metrics) {
12
+ const findings = [];
13
+ const anyUsageMetrics = metrics.filter(m => m.name === 'any-usage');
14
+ for (const metric of anyUsageMetrics) {
15
+ if (metric.value > 0) {
16
+ const builder = new FindingBuilder()
17
+ .withRuleId(this.id)
18
+ .withSeverity(metric.value > 5 ? 'high' : 'medium')
19
+ .withMessage(`Found ${metric.value} usage(s) of 'any' type in ${metric.context || 'code'}`)
20
+ .withFilePath(metric.filePath)
21
+ .withSuggestion('Replace `any` with specific types or use `unknown` for better type safety');
22
+ if (metric.location) {
23
+ builder.withLocation(metric.location);
24
+ }
25
+ findings.push(builder.build());
26
+ }
27
+ }
28
+ return findings;
29
+ }
30
+ calculateScore(findings) {
31
+ if (findings.length === 0)
32
+ return 100;
33
+ // Each 'any' usage reduces the score
34
+ const totalAnyUsages = findings.reduce((sum, finding) => {
35
+ // Extract count from message (hacky, but works for v1)
36
+ const match = finding.message.match(/Found (\d+) usage/);
37
+ return sum + (match ? parseInt(match[1] ?? '0', 10) : 1);
38
+ }, 0);
39
+ // Each 'any' costs 3 points
40
+ const penalty = Math.min(100, totalAnyUsages * 3);
41
+ const score = Math.max(0, 100 - penalty);
42
+ return Math.round(score * 100) / 100;
43
+ }
44
+ }
45
+ //# sourceMappingURL=TypeSafetyRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TypeSafetyRule.js","sourceRoot":"","sources":["../../../src/domain/rules/TypeSafetyRule.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAG/C,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,OAAO,cAAe,SAAQ,QAAQ;IAC1C;QACE,KAAK,CACH,aAAa,EACb,kBAAkB,EAClB,uEAAuE,CACxE,CAAC;IACJ,CAAC;IAED,QAAQ,CAAC,OAAiB;QACxB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,MAAM,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC;QAEpE,KAAK,MAAM,MAAM,IAAI,eAAe,EAAE,CAAC;YACrC,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;gBACrB,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE;qBACjC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;qBACnB,YAAY,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;qBAClD,WAAW,CACV,SAAS,MAAM,CAAC,KAAK,8BAA8B,MAAM,CAAC,OAAO,IAAI,MAAM,EAAE,CAC9E;qBACA,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC;qBAC7B,cAAc,CAAC,2EAA2E,CAAC,CAAC;gBAE/F,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;oBACpB,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACxC,CAAC;gBAED,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,cAAc,CAAC,QAAmB;QAChC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,GAAG,CAAC;QAEtC,qCAAqC;QACrC,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE;YACtD,uDAAuD;YACvD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;YACzD,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,CAAC,EAAE,CAAC,CAAC,CAAC;QAEN,4BAA4B;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,GAAG,CAAC,CAAC,CAAC;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;QAEzC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;IACvC,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared type definitions used across the application
3
+ */
4
+ export interface SourceLocation {
5
+ startLine: number;
6
+ endLine: number;
7
+ startColumn: number;
8
+ endColumn: number;
9
+ }
10
+ export type Severity = 'low' | 'medium' | 'high';
11
+ export interface FileMetadata {
12
+ path: string;
13
+ size: number;
14
+ linesOfCode: number;
15
+ }
16
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared type definitions used across the application
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "tech-debt-score",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/cli/index.js",
6
+ "bin": {
7
+ "tech-debt-score": "./dist/cli/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc && node dist/cli/index.js",
12
+ "analyze": "tsc && node dist/cli/index.js",
13
+ "scan": "npm run build && node dist/cli/index.js .",
14
+ "test": "echo \"Error: no test specified\" && exit 1"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/panduken/tech-debt-score.git"
19
+ },
20
+ "keywords": [
21
+ "technical-debt",
22
+ "code-quality",
23
+ "static-analysis",
24
+ "typescript",
25
+ "javascript",
26
+ "ast",
27
+ "metrics",
28
+ "complexity",
29
+ "code-analysis",
30
+ "developer-tools"
31
+ ],
32
+ "author": "@panduken",
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/panduken/tech-debt-score/issues"
36
+ },
37
+ "homepage": "https://github.com/panduken/tech-debt-score#readme",
38
+ "description": "Quantify technical debt you can actually control. Built by developers, for developers.",
39
+ "devDependencies": {
40
+ "@types/node": "^25.0.9",
41
+ "ts-node": "^10.9.2",
42
+ "typescript": "^5.9.3"
43
+ },
44
+ "dependencies": {
45
+ "fast-glob": "^3.3.3"
46
+ }
47
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Input Adapter: File System Reader
3
+ * Implements IFileReader using Node.js filesystem
4
+ */
5
+
6
+ import { readFile } from 'node:fs/promises';
7
+ import { stat } from 'node:fs/promises';
8
+ import { resolve, join } from 'node:path';
9
+ import fg from 'fast-glob';
10
+ import type { IFileReader, FileReadResult } from '../../application/ports/IFileReader.js';
11
+
12
+ export class FileSystemReader implements IFileReader {
13
+ async scan(rootPath: string, patterns: string[], ignore: string[]): Promise<string[]> {
14
+ // Resolve the root path to absolute
15
+ const absoluteRoot = resolve(rootPath);
16
+
17
+ // Convert patterns to absolute paths relative to root
18
+ const absolutePatterns = patterns.map(pattern =>
19
+ join(absoluteRoot, pattern)
20
+ );
21
+
22
+ // Scan for files matching patterns
23
+ const files = await fg(absolutePatterns, {
24
+ ignore,
25
+ absolute: true,
26
+ onlyFiles: true,
27
+ dot: false, // Don't include hidden files by default
28
+ });
29
+
30
+ return files;
31
+ }
32
+
33
+ async read(filePath: string): Promise<FileReadResult> {
34
+ const content = await readFile(filePath, 'utf-8');
35
+ const stats = await stat(filePath);
36
+ const lines = content.split('\n');
37
+
38
+ return {
39
+ content,
40
+ metadata: {
41
+ path: filePath,
42
+ size: stats.size,
43
+ linesOfCode: lines.length,
44
+ },
45
+ };
46
+ }
47
+
48
+ async readBatch(filePaths: string[]): Promise<FileReadResult[]> {
49
+ return Promise.all(filePaths.map(path => this.read(path)));
50
+ }
51
+ }