tech-debt-score 0.1.5 → 0.1.6

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 (48) hide show
  1. package/dist/adapters/input/FileSystemReader.d.ts.map +1 -1
  2. package/dist/adapters/input/FileSystemReader.js +4 -5
  3. package/dist/adapters/input/FileSystemReader.js.map +1 -1
  4. package/dist/adapters/output/TerminalReporter.d.ts.map +1 -1
  5. package/dist/adapters/output/TerminalReporter.js +8 -0
  6. package/dist/adapters/output/TerminalReporter.js.map +1 -1
  7. package/dist/application/config/AnalysisConfig.d.ts.map +1 -1
  8. package/dist/application/config/AnalysisConfig.js +10 -2
  9. package/dist/application/config/AnalysisConfig.js.map +1 -1
  10. package/dist/application/services/AnalysisService.d.ts.map +1 -1
  11. package/dist/application/services/AnalysisService.js +29 -11
  12. package/dist/application/services/AnalysisService.js.map +1 -1
  13. package/dist/cli/commands/analyze.d.ts.map +1 -1
  14. package/dist/cli/commands/analyze.js +8 -7
  15. package/dist/cli/commands/analyze.js.map +1 -1
  16. package/package.json +8 -2
  17. package/DEVELOPMENT.md +0 -147
  18. package/SETUP_COMPLETE.md +0 -188
  19. package/TECHNICAL_DESIGN.md +0 -563
  20. package/src/adapters/input/FileSystemReader.ts +0 -47
  21. package/src/adapters/input/TypeScriptParser.ts +0 -367
  22. package/src/adapters/output/JsonExporter.ts +0 -48
  23. package/src/adapters/output/TerminalReporter.ts +0 -94
  24. package/src/application/config/AnalysisConfig.ts +0 -58
  25. package/src/application/ports/IFileReader.ts +0 -36
  26. package/src/application/ports/IParser.ts +0 -40
  27. package/src/application/ports/IReporter.ts +0 -26
  28. package/src/application/services/AnalysisService.ts +0 -218
  29. package/src/application/services/DependencyAnalyzer.ts +0 -158
  30. package/src/application/services/DuplicationDetector.ts +0 -207
  31. package/src/cli/commands/analyze.ts +0 -77
  32. package/src/cli/index.ts +0 -81
  33. package/src/domain/entities/Finding.ts +0 -79
  34. package/src/domain/entities/Metric.ts +0 -70
  35. package/src/domain/entities/Rule.ts +0 -49
  36. package/src/domain/entities/Score.ts +0 -94
  37. package/src/domain/index.ts +0 -15
  38. package/src/domain/rules/CircularDependencyRule.ts +0 -65
  39. package/src/domain/rules/ComplexityRule.ts +0 -88
  40. package/src/domain/rules/DuplicationRule.ts +0 -70
  41. package/src/domain/rules/SizeRule.ts +0 -98
  42. package/src/domain/rules/TypeSafetyRule.ts +0 -63
  43. package/src/index.ts +0 -0
  44. package/src/shared/types.ts +0 -18
  45. package/tests/application/index.test.ts +0 -12
  46. package/tests/domain/index.test.ts +0 -14
  47. package/tests/e2e/index.test.ts +0 -13
  48. package/tsconfig.json +0 -31
package/src/cli/index.ts DELETED
@@ -1,81 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * CLI Entry Point
4
- * Command-line interface for tech-debt-score
5
- *
6
- * NO BUSINESS LOGIC HERE - just argument parsing and delegation
7
- */
8
-
9
- import { analyzeCommand } from './commands/analyze.js';
10
-
11
- async function main() {
12
- const args = process.argv.slice(2);
13
-
14
- if (args.includes('--help') || args.includes('-h')) {
15
- printHelp();
16
- process.exit(0);
17
- }
18
-
19
- // Parse arguments
20
- let rootPath = process.cwd();
21
- let jsonOutputPath: string | undefined;
22
-
23
- for (let i = 0; i < args.length; i++) {
24
- const arg = args[i];
25
- if (!arg) continue;
26
-
27
- if (arg === '--json' || arg === '-j') {
28
- const nextArg = args[i + 1];
29
- if (nextArg && !nextArg.startsWith('-')) {
30
- jsonOutputPath = nextArg;
31
- i++; // Skip next arg
32
- }
33
- } else if (!arg.startsWith('-')) {
34
- rootPath = arg;
35
- }
36
- }
37
-
38
- try {
39
- await analyzeCommand(rootPath, jsonOutputPath);
40
- process.exit(0);
41
- } catch (error) {
42
- console.error('❌ Analysis failed:', error instanceof Error ? error.message : error);
43
- process.exit(1);
44
- }
45
- }
46
-
47
- function printHelp() {
48
- console.log(`
49
- ┌─────────────────────────────────────────────┐
50
- │ tech-debt-score - Quantify Technical Debt │
51
- │ Built by developers, for developers │
52
- └─────────────────────────────────────────────┘
53
-
54
- Usage:
55
- tech-debt-score [path]
56
-
57
- Arguments:
58
- path Path to analyze (default: current directory)
59
-
60
- Options:
61
- -h, --help Show this help message
62
-
63
- Examples:
64
- tech-debt-score # Analyze current directory
65
- tech-debt-score ./my-project # Analyze specific directory
66
- tech-debt-score /path/to/code # Analyze absolute path
67
-
68
- Options:
69
- -h, --help Show this help message
70
- -j, --json <file> Output JSON report to specific file
71
-
72
-
73
- For more information, visit:
74
- https://github.com/panduken/tech-debt-score
75
- `);
76
- }
77
-
78
- // Run if executed directly
79
- if (import.meta.url === `file://${process.argv[1]}`) {
80
- main();
81
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * Domain Entity: Finding
3
- * Represents an identified issue or code smell in the codebase
4
- */
5
-
6
- import type { SourceLocation, Severity } from '../../shared/types.js';
7
-
8
- export interface Finding {
9
- /**
10
- * ID of the rule that generated this finding
11
- */
12
- ruleId: string;
13
-
14
- /**
15
- * Severity level of the issue
16
- */
17
- severity: Severity;
18
-
19
- /**
20
- * Human-readable message describing the issue
21
- */
22
- message: string;
23
-
24
- /**
25
- * File path where the issue was found
26
- */
27
- filePath: string;
28
-
29
- /**
30
- * Optional exact location in the source code
31
- */
32
- location?: SourceLocation;
33
-
34
- /**
35
- * Optional suggestion for fixing the issue
36
- */
37
- suggestion?: string;
38
- }
39
-
40
- export class FindingBuilder {
41
- private finding: Partial<Finding> = {};
42
-
43
- withRuleId(ruleId: string): this {
44
- this.finding.ruleId = ruleId;
45
- return this;
46
- }
47
-
48
- withSeverity(severity: Severity): this {
49
- this.finding.severity = severity;
50
- return this;
51
- }
52
-
53
- withMessage(message: string): this {
54
- this.finding.message = message;
55
- return this;
56
- }
57
-
58
- withFilePath(filePath: string): this {
59
- this.finding.filePath = filePath;
60
- return this;
61
- }
62
-
63
- withLocation(location: SourceLocation): this {
64
- this.finding.location = location;
65
- return this;
66
- }
67
-
68
- withSuggestion(suggestion: string): this {
69
- this.finding.suggestion = suggestion;
70
- return this;
71
- }
72
-
73
- build(): Finding {
74
- if (!this.finding.ruleId || !this.finding.severity || !this.finding.message || !this.finding.filePath) {
75
- throw new Error('Finding requires ruleId, severity, message, and filePath');
76
- }
77
- return this.finding as Finding;
78
- }
79
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * Domain Entity: Metric
3
- * Represents a single measurement extracted from code analysis
4
- */
5
-
6
- import type { SourceLocation } from '../../shared/types.js';
7
-
8
- export interface Metric {
9
- /**
10
- * Unique identifier for the metric type
11
- * e.g., 'cyclomatic-complexity', 'function-length', 'any-usage'
12
- */
13
- name: string;
14
-
15
- /**
16
- * The measured value
17
- */
18
- value: number;
19
-
20
- /**
21
- * File path where this metric was measured
22
- */
23
- filePath: string;
24
-
25
- /**
26
- * Optional location in the source code
27
- */
28
- location?: SourceLocation;
29
-
30
- /**
31
- * Optional context (e.g., function name, class name)
32
- */
33
- context?: string;
34
- }
35
-
36
- export class MetricBuilder {
37
- private metric: Partial<Metric> = {};
38
-
39
- withName(name: string): this {
40
- this.metric.name = name;
41
- return this;
42
- }
43
-
44
- withValue(value: number): this {
45
- this.metric.value = value;
46
- return this;
47
- }
48
-
49
- withFilePath(filePath: string): this {
50
- this.metric.filePath = filePath;
51
- return this;
52
- }
53
-
54
- withLocation(location: SourceLocation): this {
55
- this.metric.location = location;
56
- return this;
57
- }
58
-
59
- withContext(context: string): this {
60
- this.metric.context = context;
61
- return this;
62
- }
63
-
64
- build(): Metric {
65
- if (!this.metric.name || this.metric.value === undefined || !this.metric.filePath) {
66
- throw new Error('Metric requires name, value, and filePath');
67
- }
68
- return this.metric as Metric;
69
- }
70
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Domain Entity: Rule
3
- * Represents an evaluation rule that processes metrics and generates findings
4
- */
5
-
6
- import type { Metric } from './Metric.js';
7
- import type { Finding } from './Finding.js';
8
-
9
- export interface Rule {
10
- /**
11
- * Unique identifier for this rule
12
- */
13
- id: string;
14
-
15
- /**
16
- * Human-readable name
17
- */
18
- name: string;
19
-
20
- /**
21
- * Description of what this rule evaluates
22
- */
23
- description: string;
24
-
25
- /**
26
- * Evaluate metrics and produce findings
27
- */
28
- evaluate(metrics: Metric[]): Finding[];
29
-
30
- /**
31
- * Calculate a score (0-100) based on findings
32
- * where 100 = no issues, 0 = maximum issues
33
- */
34
- calculateScore(findings: Finding[]): number;
35
- }
36
-
37
- /**
38
- * Abstract base class for rules
39
- */
40
- export abstract class BaseRule implements Rule {
41
- constructor(
42
- public readonly id: string,
43
- public readonly name: string,
44
- public readonly description: string
45
- ) {}
46
-
47
- abstract evaluate(metrics: Metric[]): Finding[];
48
- abstract calculateScore(findings: Finding[]): number;
49
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * Domain Entity: Score
3
- * Represents the technical debt score and category breakdowns
4
- */
5
-
6
- export interface CategoryScore {
7
- /**
8
- * Category name (e.g., 'Complexity', 'Size', 'Type Safety')
9
- */
10
- name: string;
11
-
12
- /**
13
- * Score for this category (0-100, where 100 = no debt)
14
- */
15
- score: number;
16
-
17
- /**
18
- * Weight of this category in the overall score (0-1)
19
- */
20
- weight: number;
21
-
22
- /**
23
- * Optional description of what this category measures
24
- */
25
- description?: string;
26
- }
27
-
28
- export interface Score {
29
- /**
30
- * Overall technical debt score (0-100, where 100 = no debt)
31
- */
32
- overall: number;
33
-
34
- /**
35
- * Breakdown by category
36
- */
37
- categories: CategoryScore[];
38
-
39
- /**
40
- * When this score was calculated
41
- */
42
- timestamp: Date;
43
-
44
- /**
45
- * Optional metadata about the analysis
46
- */
47
- metadata?: {
48
- filesAnalyzed: number;
49
- totalMetrics: number;
50
- totalFindings: number;
51
- };
52
- }
53
-
54
- export class ScoreCalculator {
55
- /**
56
- * Calculate overall score from category scores
57
- *
58
- * Formula: Σ(category_score × category_weight)
59
- */
60
- static calculateOverall(categories: CategoryScore[]): number {
61
- // Validate weights sum to 1.0 (or close to it)
62
- const totalWeight = categories.reduce((sum, cat) => sum + cat.weight, 0);
63
- if (Math.abs(totalWeight - 1.0) > 0.01) {
64
- throw new Error(`Category weights must sum to 1.0, got ${totalWeight}`);
65
- }
66
-
67
- // Calculate weighted average
68
- const weightedSum = categories.reduce(
69
- (sum, cat) => sum + (cat.score * cat.weight),
70
- 0
71
- );
72
-
73
- // Round to 2 decimal places
74
- return Math.round(weightedSum * 100) / 100;
75
- }
76
-
77
- /**
78
- * Normalize a raw value to 0-100 scale
79
- *
80
- * @param value - The raw value to normalize
81
- * @param min - Minimum value (maps to 100)
82
- * @param max - Maximum value (maps to 0)
83
- * @param invert - If true, higher values = better score
84
- */
85
- static normalize(value: number, min: number, max: number, invert = false): number {
86
- if (value <= min) return invert ? 0 : 100;
87
- if (value >= max) return invert ? 100 : 0;
88
-
89
- const normalized = ((value - min) / (max - min)) * 100;
90
- const score = invert ? normalized : 100 - normalized;
91
-
92
- return Math.round(score * 100) / 100;
93
- }
94
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Domain Layer - Public API
3
- * Exports all domain entities and services
4
- */
5
-
6
- // Entities
7
- export * from './entities/Metric.js';
8
- export * from './entities/Finding.js';
9
- export * from './entities/Score.js';
10
- export * from './entities/Rule.js';
11
-
12
- // Rules
13
- export * from './rules/ComplexityRule.js';
14
- export * from './rules/SizeRule.js';
15
- export * from './rules/TypeSafetyRule.js';
@@ -1,65 +0,0 @@
1
- /**
2
- * Domain Rule: Circular Dependency Rule
3
- * Detects circular dependencies between modules
4
- */
5
-
6
- import { BaseRule } from '../entities/Rule.js';
7
- import type { Metric } from '../entities/Metric.js';
8
- import type { Finding } from '../entities/Finding.js';
9
- import { FindingBuilder } from '../entities/Finding.js';
10
-
11
- export class CircularDependencyRule extends BaseRule {
12
- constructor() {
13
- super(
14
- 'circular-dependency',
15
- 'Circular Dependency Rule',
16
- 'Detects circular dependencies between modules that can cause maintenance issues'
17
- );
18
- }
19
-
20
- evaluate(metrics: Metric[]): Finding[] {
21
- const findings: Finding[] = [];
22
-
23
- // Find circular dependency metrics
24
- const circularDepMetrics = metrics.filter(m => m.name === 'circular-dependency');
25
-
26
- for (const metric of circularDepMetrics) {
27
- if (metric.value > 0) {
28
- // Extract cycle information from context
29
- const cycleLength = metric.value;
30
- const severity = cycleLength >= 4 ? 'high' : cycleLength >= 3 ? 'medium' : 'low';
31
-
32
- findings.push(
33
- new FindingBuilder()
34
- .withRuleId(this.id)
35
- .withSeverity(severity)
36
- .withMessage(
37
- `Circular dependency detected: ${metric.context || 'module cycle'} (${cycleLength} files in cycle)`
38
- )
39
- .withFilePath(metric.filePath)
40
- .withSuggestion('Refactor to break the circular dependency by introducing interfaces or dependency injection')
41
- .build()
42
- );
43
- }
44
- }
45
-
46
- return findings;
47
- }
48
-
49
- calculateScore(findings: Finding[]): number {
50
- if (findings.length === 0) return 100;
51
-
52
- // Circular dependencies are serious - heavy penalty
53
- const penalty = findings.reduce((sum, finding) => {
54
- switch (finding.severity) {
55
- case 'high': return sum + 20; // Very bad
56
- case 'medium': return sum + 12;
57
- case 'low': return sum + 6;
58
- default: return sum;
59
- }
60
- }, 0);
61
-
62
- const score = Math.max(0, 100 - penalty);
63
- return Math.round(score * 100) / 100;
64
- }
65
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * Domain Rule: Complexity Rule
3
- * Evaluates cyclomatic complexity and nesting depth
4
- */
5
-
6
- import { BaseRule } from '../entities/Rule.js';
7
- import type { Metric } from '../entities/Metric.js';
8
- import type { Finding } from '../entities/Finding.js';
9
- import { FindingBuilder } from '../entities/Finding.js';
10
-
11
- export class ComplexityRule extends BaseRule {
12
- // Thresholds (configurable in future versions)
13
- private readonly COMPLEXITY_THRESHOLD = 10;
14
- private readonly NESTING_THRESHOLD = 4;
15
-
16
- constructor() {
17
- super(
18
- 'complexity',
19
- 'Complexity Rule',
20
- 'Evaluates cyclomatic complexity and nesting depth to identify overly complex code'
21
- );
22
- }
23
-
24
- evaluate(metrics: Metric[]): Finding[] {
25
- const findings: Finding[] = [];
26
-
27
- // Filter for complexity-related metrics
28
- const complexityMetrics = metrics.filter(
29
- m => m.name === 'cyclomatic-complexity' || m.name === 'nesting-depth'
30
- );
31
-
32
- for (const metric of complexityMetrics) {
33
- if (metric.name === 'cyclomatic-complexity' && metric.value > this.COMPLEXITY_THRESHOLD) {
34
- const builder = new FindingBuilder()
35
- .withRuleId(this.id)
36
- .withSeverity(metric.value > 20 ? 'high' : 'medium')
37
- .withMessage(
38
- `High cyclomatic complexity (${metric.value}) in ${metric.context || 'function'}`
39
- )
40
- .withFilePath(metric.filePath)
41
- .withSuggestion('Consider breaking this function into smaller, more focused functions');
42
-
43
- if (metric.location) {
44
- builder.withLocation(metric.location);
45
- }
46
-
47
- findings.push(builder.build());
48
- }
49
-
50
- if (metric.name === 'nesting-depth' && metric.value > this.NESTING_THRESHOLD) {
51
- const builder = new FindingBuilder()
52
- .withRuleId(this.id)
53
- .withSeverity(metric.value > 6 ? 'high' : 'medium')
54
- .withMessage(
55
- `Deep nesting (${metric.value} levels) in ${metric.context || 'function'}`
56
- )
57
- .withFilePath(metric.filePath)
58
- .withSuggestion('Reduce nesting by using early returns or extracting functions');
59
-
60
- if (metric.location) {
61
- builder.withLocation(metric.location);
62
- }
63
-
64
- findings.push(builder.build());
65
- }
66
- }
67
-
68
- return findings;
69
- }
70
-
71
- calculateScore(findings: Finding[]): number {
72
- if (findings.length === 0) return 100;
73
-
74
- // Penalty based on severity
75
- const penalty = findings.reduce((sum, finding) => {
76
- switch (finding.severity) {
77
- case 'high': return sum + 10;
78
- case 'medium': return sum + 5;
79
- case 'low': return sum + 2;
80
- default: return sum;
81
- }
82
- }, 0);
83
-
84
- // Cap the penalty and invert to get score
85
- const score = Math.max(0, 100 - penalty);
86
- return Math.round(score * 100) / 100;
87
- }
88
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * Domain Rule: Code Duplication Rule
3
- * Detects duplicate code blocks using token-based similarity
4
- */
5
-
6
- import { BaseRule } from '../entities/Rule.js';
7
- import type { Metric } from '../entities/Metric.js';
8
- import type { Finding } from '../entities/Finding.js';
9
- import { FindingBuilder } from '../entities/Finding.js';
10
-
11
- export class DuplicationRule extends BaseRule {
12
- // Thresholds
13
- private readonly MIN_DUPLICATE_TOKENS = 50; // Minimum tokens to consider as duplication
14
- private readonly SIMILARITY_THRESHOLD = 0.95; // 95% similarity
15
-
16
- constructor() {
17
- super(
18
- 'duplication',
19
- 'Code Duplication Rule',
20
- 'Detects duplicate or highly similar code blocks across the codebase'
21
- );
22
- }
23
-
24
- evaluate(metrics: Metric[]): Finding[] {
25
- const findings: Finding[] = [];
26
-
27
- // Group metrics by type
28
- const duplicationMetrics = metrics.filter(m => m.name === 'code-duplication');
29
-
30
- for (const metric of duplicationMetrics) {
31
- if (metric.value > 0) {
32
- const severity = metric.value > 5 ? 'high' : metric.value > 2 ? 'medium' : 'low';
33
-
34
- const builder = new FindingBuilder()
35
- .withRuleId(this.id)
36
- .withSeverity(severity)
37
- .withMessage(
38
- `Found ${metric.value} duplicate code block(s) in ${metric.context || 'file'}`
39
- )
40
- .withFilePath(metric.filePath)
41
- .withSuggestion('Extract duplicate code into reusable functions or modules');
42
-
43
- if (metric.location) {
44
- builder.withLocation(metric.location);
45
- }
46
-
47
- findings.push(builder.build());
48
- }
49
- }
50
-
51
- return findings;
52
- }
53
-
54
- calculateScore(findings: Finding[]): number {
55
- if (findings.length === 0) return 100;
56
-
57
- // Each duplication finding reduces score
58
- const penalty = findings.reduce((sum, finding) => {
59
- switch (finding.severity) {
60
- case 'high': return sum + 15;
61
- case 'medium': return sum + 8;
62
- case 'low': return sum + 4;
63
- default: return sum;
64
- }
65
- }, 0);
66
-
67
- const score = Math.max(0, 100 - penalty);
68
- return Math.round(score * 100) / 100;
69
- }
70
- }