renderpilot 1.0.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 (34) hide show
  1. package/README.md +121 -0
  2. package/package.json +65 -0
  3. package/src/cli/commands/scan.js +83 -0
  4. package/src/cli/index.js +49 -0
  5. package/src/cli/options.js +18 -0
  6. package/src/config/ConfigLoader.js +102 -0
  7. package/src/config/DefaultConfig.js +33 -0
  8. package/src/index.js +34 -0
  9. package/src/parser/ASTUtils.js +241 -0
  10. package/src/parser/BabelParser.js +66 -0
  11. package/src/parser/ComponentDetector.js +53 -0
  12. package/src/reporters/HtmlReporter.js +42 -0
  13. package/src/reporters/JsonReporter.js +52 -0
  14. package/src/reporters/TerminalReporter.js +91 -0
  15. package/src/reporters/templates/report.html.js +655 -0
  16. package/src/rules/base/AbstractRule.js +136 -0
  17. package/src/rules/engine/RuleEngine.js +67 -0
  18. package/src/rules/implementations/R001_MissingKeyProp.js +97 -0
  19. package/src/rules/implementations/R002_InfiniteUseEffect.js +86 -0
  20. package/src/rules/implementations/R003_IncorrectDepsArray.js +112 -0
  21. package/src/rules/implementations/R004_LargeComponent.js +47 -0
  22. package/src/rules/implementations/R005_InlineCallback.js +58 -0
  23. package/src/rules/implementations/R006_HeavyRenderCalc.js +86 -0
  24. package/src/rules/implementations/R007_MemoCandidate.js +87 -0
  25. package/src/rules/implementations/R008_NestedComponent.js +65 -0
  26. package/src/rules/implementations/R009_UnusedState.js +62 -0
  27. package/src/rules/index.js +37 -0
  28. package/src/rules/registry/RuleRegistry.js +67 -0
  29. package/src/scanner/FileScanner.js +42 -0
  30. package/src/scanner/ProjectScanner.js +116 -0
  31. package/src/score/CategoryScorer.js +53 -0
  32. package/src/score/ScoreCalculator.js +97 -0
  33. package/src/score/ScoreWeights.js +33 -0
  34. package/src/types/index.js +176 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @file rules/implementations/R007_MemoCandidate.js
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const { AbstractRule } = require('../base/AbstractRule');
8
+ const { ComponentDetector } = require('../../parser/ComponentDetector');
9
+ const traverse = require('@babel/traverse').default;
10
+ const t = require('@babel/types');
11
+
12
+ class R007_MemoCandidate extends AbstractRule {
13
+ constructor() {
14
+ super({
15
+ id: 'R007',
16
+ name: 'React.memo Candidate',
17
+ description: 'Detects pure components (no state, no effects, only props) that might benefit from React.memo.',
18
+ severity: 'info',
19
+ category: 'rendering',
20
+ });
21
+ }
22
+
23
+ detect(context) {
24
+ const violations = [];
25
+ const components = ComponentDetector.extract(context.ast);
26
+
27
+ for (const comp of components) {
28
+ let hasHooks = false;
29
+
30
+ // Traverse inside the component to look for hooks
31
+ comp.path.traverse({
32
+ CallExpression(innerPath) {
33
+ if (t.isIdentifier(innerPath.node.callee)) {
34
+ if (innerPath.node.callee.name.startsWith('use')) {
35
+ hasHooks = true;
36
+ innerPath.stop();
37
+ }
38
+ } else if (t.isMemberExpression(innerPath.node.callee) && t.isIdentifier(innerPath.node.callee.property)) {
39
+ if (innerPath.node.callee.property.name.startsWith('use')) {
40
+ hasHooks = true;
41
+ innerPath.stop();
42
+ }
43
+ }
44
+ }
45
+ });
46
+
47
+ if (!hasHooks) {
48
+ // Pure component!
49
+ // Does it use React.memo already? We can check the parent of the component node.
50
+ let isMemoized = false;
51
+ const parentNode = comp.path.parent;
52
+
53
+ if (t.isCallExpression(parentNode) && t.isIdentifier(parentNode.callee, {name: 'memo'})) {
54
+ isMemoized = true;
55
+ } else if (t.isCallExpression(parentNode) && t.isMemberExpression(parentNode.callee) && t.isIdentifier(parentNode.callee.property, {name: 'memo'})) {
56
+ isMemoized = true;
57
+ } else if (t.isVariableDeclarator(parentNode) && t.isCallExpression(parentNode.init) &&
58
+ (t.isIdentifier(parentNode.init.callee, {name: 'memo'}) ||
59
+ (t.isMemberExpression(parentNode.init.callee) && t.isIdentifier(parentNode.init.callee.property, {name: 'memo'})))) {
60
+ isMemoized = true;
61
+ }
62
+
63
+ if (!isMemoized) {
64
+ // Check if the component actually accepts props
65
+ const hasProps = comp.path.node.params && comp.path.node.params.length > 0;
66
+
67
+ if (hasProps) {
68
+ violations.push(
69
+ this.createViolation({
70
+ filePath: context.filePath,
71
+ line: comp.startLine,
72
+ column: 0,
73
+ message: `This component appears to be pure and receives props.`,
74
+ suggestion: `React.memo may reduce unnecessary re-renders if parent renders frequently.`,
75
+ componentName: comp.name,
76
+ })
77
+ );
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ return violations;
84
+ }
85
+ }
86
+
87
+ module.exports = { R007_MemoCandidate };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @file rules/implementations/R008_NestedComponent.js
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const { AbstractRule } = require('../base/AbstractRule');
8
+ const { ComponentDetector } = require('../../parser/ComponentDetector');
9
+ const { getCodeSnippet } = require('../../parser/ASTUtils');
10
+ const t = require('@babel/types');
11
+
12
+ class R008_NestedComponent extends AbstractRule {
13
+ constructor() {
14
+ super({
15
+ id: 'R008',
16
+ name: 'Nested Component Declarations',
17
+ description: 'Detects components defined inside other components, which forces unmount/remount on every render.',
18
+ severity: 'critical',
19
+ category: 'architecture',
20
+ });
21
+ }
22
+
23
+ detect(context) {
24
+ const violations = [];
25
+ const components = ComponentDetector.extract(context.ast);
26
+
27
+ // Build a map of components by their path for easy lookup
28
+ const compPaths = new Set(components.map(c => c.path));
29
+
30
+ for (const comp of components) {
31
+ // Find components whose parent path is ALSO a component
32
+ let current = comp.path.parentPath;
33
+ let isNested = false;
34
+ let parentCompName = '';
35
+
36
+ while (current) {
37
+ if (compPaths.has(current)) {
38
+ isNested = true;
39
+ const parentComp = components.find(c => c.path === current);
40
+ if (parentComp) parentCompName = parentComp.name;
41
+ break;
42
+ }
43
+ current = current.parentPath;
44
+ }
45
+
46
+ if (isNested) {
47
+ violations.push(
48
+ this.createViolation({
49
+ filePath: context.filePath,
50
+ line: comp.startLine,
51
+ column: 0,
52
+ message: `Component "${comp.name}" is declared inside "${parentCompName}".`,
53
+ suggestion: `Move "${comp.name}" outside of "${parentCompName}". Nested declarations cause React to destroy and recreate the DOM on every render.`,
54
+ codeSnippet: getCodeSnippet(context.lines, comp.startLine),
55
+ componentName: comp.name,
56
+ })
57
+ );
58
+ }
59
+ }
60
+
61
+ return violations;
62
+ }
63
+ }
64
+
65
+ module.exports = { R008_NestedComponent };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @file rules/implementations/R009_UnusedState.js
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const { AbstractRule } = require('../base/AbstractRule');
8
+ const { getCodeSnippet } = require('../../parser/ASTUtils');
9
+ const traverse = require('@babel/traverse').default;
10
+ const t = require('@babel/types');
11
+
12
+ class R009_UnusedState extends AbstractRule {
13
+ constructor() {
14
+ super({
15
+ id: 'R009',
16
+ name: 'Unused React State',
17
+ description: 'Detects useState variables that are never referenced.',
18
+ severity: 'warning',
19
+ category: 'maintainability',
20
+ });
21
+ }
22
+
23
+ detect(context) {
24
+ const violations = [];
25
+
26
+ traverse(context.ast, {
27
+ VariableDeclarator: (path) => {
28
+ if (
29
+ t.isCallExpression(path.node.init) &&
30
+ (t.isIdentifier(path.node.init.callee, { name: 'useState' }) ||
31
+ (t.isMemberExpression(path.node.init.callee) && t.isIdentifier(path.node.init.callee.property, { name: 'useState' }))) &&
32
+ t.isArrayPattern(path.node.id) &&
33
+ path.node.id.elements.length > 0
34
+ ) {
35
+ const stateVar = path.node.id.elements[0];
36
+
37
+ if (t.isIdentifier(stateVar)) {
38
+ const name = stateVar.name;
39
+ const binding = path.scope.getBinding(name);
40
+
41
+ if (binding && !binding.referenced) {
42
+ violations.push(
43
+ this.createViolation({
44
+ filePath: context.filePath,
45
+ line: path.node.loc ? path.node.loc.start.line : 0,
46
+ column: path.node.loc ? path.node.loc.start.column : 0,
47
+ message: `State variable "${name}" is never used.`,
48
+ suggestion: 'Remove unused state to improve maintainability and avoid confusion.',
49
+ codeSnippet: getCodeSnippet(context.lines, path.node.loc ? path.node.loc.start.line : 0),
50
+ })
51
+ );
52
+ }
53
+ }
54
+ }
55
+ }
56
+ });
57
+
58
+ return violations;
59
+ }
60
+ }
61
+
62
+ module.exports = { R009_UnusedState };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @file rules/index.js
3
+ * @description
4
+ * Bootstraps the Rule Registry by loading and registering all built-in rules.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { ruleRegistry } = require('./registry/RuleRegistry');
10
+ const { RuleEngine } = require('./engine/RuleEngine');
11
+
12
+ // Import all rules
13
+ const { R001_MissingKeyProp } = require('./implementations/R001_MissingKeyProp');
14
+ const { R002_InfiniteUseEffect } = require('./implementations/R002_InfiniteUseEffect');
15
+ const { R003_IncorrectDepsArray } = require('./implementations/R003_IncorrectDepsArray');
16
+ const { R004_LargeComponent } = require('./implementations/R004_LargeComponent');
17
+ const { R005_InlineCallback } = require('./implementations/R005_InlineCallback');
18
+ const { R006_HeavyRenderCalc } = require('./implementations/R006_HeavyRenderCalc');
19
+ const { R007_MemoCandidate } = require('./implementations/R007_MemoCandidate');
20
+ const { R008_NestedComponent } = require('./implementations/R008_NestedComponent');
21
+ const { R009_UnusedState } = require('./implementations/R009_UnusedState');
22
+
23
+ // Register them
24
+ ruleRegistry.register(new R001_MissingKeyProp());
25
+ ruleRegistry.register(new R002_InfiniteUseEffect());
26
+ ruleRegistry.register(new R003_IncorrectDepsArray());
27
+ ruleRegistry.register(new R004_LargeComponent());
28
+ ruleRegistry.register(new R005_InlineCallback());
29
+ ruleRegistry.register(new R006_HeavyRenderCalc());
30
+ ruleRegistry.register(new R007_MemoCandidate());
31
+ ruleRegistry.register(new R008_NestedComponent());
32
+ ruleRegistry.register(new R009_UnusedState());
33
+
34
+ module.exports = {
35
+ ruleRegistry,
36
+ RuleEngine,
37
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file rules/registry/RuleRegistry.js
3
+ * @description
4
+ * Manages the registration and lifecycle of all analysis rules.
5
+ * Provides lookup capabilities and handles enable/disable filtering.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ class RuleRegistry {
11
+ constructor() {
12
+ /** @type {Map<string, import('../../base/AbstractRule').AbstractRule>} */
13
+ this.rules = new Map();
14
+ }
15
+
16
+ /**
17
+ * Register a new rule instance.
18
+ * @param {import('../../base/AbstractRule').AbstractRule} ruleInstance
19
+ */
20
+ register(ruleInstance) {
21
+ if (this.rules.has(ruleInstance.id)) {
22
+ throw new Error(`Rule with ID ${ruleInstance.id} is already registered.`);
23
+ }
24
+ this.rules.set(ruleInstance.id, ruleInstance);
25
+ }
26
+
27
+ /**
28
+ * Get all registered rules.
29
+ * @returns {import('../../base/AbstractRule').AbstractRule[]}
30
+ */
31
+ getAllRules() {
32
+ return Array.from(this.rules.values());
33
+ }
34
+
35
+ /**
36
+ * Get rules that should be active for a given configuration.
37
+ * Handles enable-lists and disable-lists.
38
+ * @param {import('../../../types').ResolvedConfig} config
39
+ * @returns {import('../../base/AbstractRule').AbstractRule[]}
40
+ */
41
+ getActiveRules(config) {
42
+ return this.getAllRules().filter((rule) => {
43
+ // If rule is explicitly disabled, skip it
44
+ if (config.disabledRules.has(rule.id)) {
45
+ return false;
46
+ }
47
+ // If an allowlist is provided, only include rules in the allowlist
48
+ if (config.enabledRules !== null && !config.enabledRules.has(rule.id)) {
49
+ return false;
50
+ }
51
+ return true;
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Get a specific rule by ID.
57
+ * @param {string} id
58
+ * @returns {import('../../base/AbstractRule').AbstractRule | undefined}
59
+ */
60
+ getRule(id) {
61
+ return this.rules.get(id);
62
+ }
63
+ }
64
+
65
+ // Export a singleton instance
66
+ const ruleRegistry = new RuleRegistry();
67
+ module.exports = { ruleRegistry, RuleRegistry };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @file scanner/FileScanner.js
3
+ * @description
4
+ * Discovers React source files (.js, .jsx, .ts, .tsx) in the given directory.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fg = require('fast-glob');
10
+ const path = require('path');
11
+
12
+ class FileScanner {
13
+ /**
14
+ * Find all React source files in the project.
15
+ * @param {string} rootDir
16
+ * @param {import('../types').ResolvedConfig} config
17
+ * @returns {Promise<string[]>} List of absolute file paths
18
+ */
19
+ static async discoverFiles(rootDir, config) {
20
+ // Build ignore patterns
21
+ const ignore = config.ignoreDirs.map(dir => `**/${dir}/**`);
22
+
23
+ // We only care about JS/TS/JSX/TSX files
24
+ const patterns = [
25
+ '**/*.js',
26
+ '**/*.jsx',
27
+ '**/*.ts',
28
+ '**/*.tsx',
29
+ ];
30
+
31
+ const files = await fg(patterns, {
32
+ cwd: rootDir,
33
+ ignore,
34
+ absolute: true,
35
+ dot: false,
36
+ });
37
+
38
+ return files;
39
+ }
40
+ }
41
+
42
+ module.exports = { FileScanner };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @file scanner/ProjectScanner.js
3
+ * @description
4
+ * Orchestrates the full project scan: discovers files, parses them,
5
+ * runs the rule engine, and collects the final results.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { FileScanner } = require('./FileScanner');
13
+ const { parseFile } = require('../parser/BabelParser');
14
+ const { RuleEngine } = require('../rules/engine/RuleEngine');
15
+ const { ComponentDetector } = require('../parser/ComponentDetector');
16
+
17
+ class ProjectScanner {
18
+ /**
19
+ * Run a full project scan.
20
+ * @param {string} rootDir
21
+ * @param {import('../types').ResolvedConfig} config
22
+ * @returns {Promise<import('../types').ScanResult>}
23
+ */
24
+ static async scan(rootDir, config) {
25
+ const startedAt = new Date().toISOString();
26
+ const startTime = Date.now();
27
+
28
+ const filePaths = await FileScanner.discoverFiles(rootDir, config);
29
+
30
+ /** @type {import('../types').FileAnalysisResult[]} */
31
+ const files = [];
32
+
33
+ const allViolations = [];
34
+ let parseErrors = 0;
35
+
36
+ for (const filePath of filePaths) {
37
+ const fileStartTime = Date.now();
38
+ const relativePath = path.relative(rootDir, filePath);
39
+ const source = fs.readFileSync(filePath, 'utf-8');
40
+ const lines = source.split(/\r?\n/);
41
+
42
+ const { ast, error } = parseFile(filePath, source);
43
+
44
+ if (error || !ast) {
45
+ parseErrors++;
46
+ files.push({
47
+ filePath,
48
+ relativePath,
49
+ violations: [],
50
+ analysisDurationMs: Date.now() - fileStartTime,
51
+ parsed: false,
52
+ parseError: error ? error.message : 'Unknown parse error',
53
+ lineCount: lines.length,
54
+ componentNames: [],
55
+ });
56
+ continue;
57
+ }
58
+
59
+ // We only care about files that actually have components for some rules,
60
+ // but we run all rules and let them decide. We extract component names just for the report.
61
+ const components = ComponentDetector.extract(ast);
62
+ const componentNames = components.map(c => c.name);
63
+
64
+ const context = {
65
+ ast,
66
+ filePath,
67
+ fileContent: source,
68
+ lines,
69
+ config,
70
+ };
71
+
72
+ const violations = RuleEngine.run(context);
73
+ allViolations.push(...violations);
74
+
75
+ files.push({
76
+ filePath,
77
+ relativePath,
78
+ violations,
79
+ analysisDurationMs: Date.now() - fileStartTime,
80
+ parsed: true,
81
+ lineCount: lines.length,
82
+ componentNames,
83
+ });
84
+ }
85
+
86
+ const durationMs = Date.now() - startTime;
87
+
88
+ // Aggregate stats
89
+ const violationsBySeverity = { critical: 0, warning: 0, info: 0 };
90
+ const violationsByCategory = { rendering: 0, hooks: 0, architecture: 0, maintainability: 0 };
91
+ const violationsByRule = {};
92
+
93
+ for (const v of allViolations) {
94
+ violationsBySeverity[v.severity] = (violationsBySeverity[v.severity] || 0) + 1;
95
+ violationsByCategory[v.category] = (violationsByCategory[v.category] || 0) + 1;
96
+ violationsByRule[v.ruleId] = (violationsByRule[v.ruleId] || 0) + 1;
97
+ }
98
+
99
+ return {
100
+ scanRoot: rootDir,
101
+ startedAt,
102
+ completedAt: new Date().toISOString(),
103
+ durationMs,
104
+ files,
105
+ totalFiles: filePaths.length,
106
+ parseErrors,
107
+ allViolations,
108
+ totalViolations: allViolations.length,
109
+ violationsBySeverity,
110
+ violationsByCategory,
111
+ violationsByRule,
112
+ };
113
+ }
114
+ }
115
+
116
+ module.exports = { ProjectScanner };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @file score/CategoryScorer.js
3
+ * @description
4
+ * Calculates the score for an individual category.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { SEVERITY_PENALTY, CATEGORY_WEIGHT } = require('./ScoreWeights');
10
+
11
+ class CategoryScorer {
12
+ /**
13
+ * Calculate a grade based on a numeric score.
14
+ * @param {number} score
15
+ * @returns {import('../types').Grade}
16
+ */
17
+ static getGrade(score) {
18
+ if (score >= 90) return 'A';
19
+ if (score >= 75) return 'B';
20
+ if (score >= 60) return 'C';
21
+ if (score >= 40) return 'D';
22
+ return 'F';
23
+ }
24
+
25
+ /**
26
+ * Calculate the score for a specific category.
27
+ * @param {import('../types').Category} category
28
+ * @param {import('../types').RuleViolation[]} violations
29
+ * @returns {import('../types').CategoryScore}
30
+ */
31
+ static scoreCategory(category, violations) {
32
+ const categoryViolations = violations.filter(v => v.category === category);
33
+
34
+ let penalty = 0;
35
+ for (const v of categoryViolations) {
36
+ const base = SEVERITY_PENALTY[v.severity] || 0;
37
+ const weight = CATEGORY_WEIGHT[category] || 1;
38
+ penalty += (base * weight);
39
+ }
40
+
41
+ const score = Math.max(0, Math.round(100 - penalty));
42
+
43
+ return {
44
+ category,
45
+ score,
46
+ grade: this.getGrade(score),
47
+ violations: categoryViolations.length,
48
+ penaltyApplied: penalty,
49
+ };
50
+ }
51
+ }
52
+
53
+ module.exports = { CategoryScorer };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @file score/ScoreCalculator.js
3
+ * @description
4
+ * Calculates the overall performance score and aggregates category scores.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { CategoryScorer } = require('./CategoryScorer');
10
+ const { SEVERITY_PENALTY, CATEGORY_WEIGHT } = require('./ScoreWeights');
11
+
12
+ class ScoreCalculator {
13
+ /**
14
+ * Get human-readable label for a score.
15
+ * @param {number} score
16
+ * @returns {string}
17
+ */
18
+ static getLabel(score) {
19
+ if (score >= 90) return 'Excellent';
20
+ if (score >= 75) return 'Good';
21
+ if (score >= 60) return 'Needs Work';
22
+ if (score >= 40) return 'Poor';
23
+ return 'Critical';
24
+ }
25
+
26
+ /**
27
+ * Get top recommendation based on violations.
28
+ * @param {import('../types').RuleViolation[]} violations
29
+ * @returns {string}
30
+ */
31
+ static getTopRecommendation(violations) {
32
+ if (violations.length === 0) return 'Keep up the great work!';
33
+
34
+ const critical = violations.filter(v => v.severity === 'critical');
35
+ if (critical.length > 0) {
36
+ return `Fix ${critical.length} critical issues immediately to prevent infinite loops and broken UI.`;
37
+ }
38
+
39
+ const rendering = violations.filter(v => v.category === 'rendering' && v.severity === 'warning');
40
+ if (rendering.length > 0) {
41
+ return `Focus on reducing unnecessary rerenders by fixing keys and memoizing heavy calculations.`;
42
+ }
43
+
44
+ return 'Review the warnings to further optimize component performance.';
45
+ }
46
+
47
+ /**
48
+ * Compute the full performance score.
49
+ * @param {import('../types').RuleViolation[]} violations
50
+ * @returns {import('../types').PerformanceScore}
51
+ */
52
+ static compute(violations) {
53
+ let totalPenalty = 0;
54
+ let criticalPenalty = 0;
55
+
56
+ const penaltyBySeverity = { critical: 0, warning: 0, info: 0 };
57
+
58
+ for (const v of violations) {
59
+ const base = SEVERITY_PENALTY[v.severity] || 0;
60
+ const weight = CATEGORY_WEIGHT[v.category] || 1;
61
+ const penalty = base * weight;
62
+
63
+ totalPenalty += penalty;
64
+ penaltyBySeverity[v.severity] += penalty;
65
+
66
+ if (v.severity === 'critical') {
67
+ criticalPenalty += penalty;
68
+ }
69
+ }
70
+
71
+ const overallScore = Math.max(0, Math.round(100 - totalPenalty));
72
+
73
+ const categories = [
74
+ CategoryScorer.scoreCategory('rendering', violations),
75
+ CategoryScorer.scoreCategory('hooks', violations),
76
+ CategoryScorer.scoreCategory('architecture', violations),
77
+ CategoryScorer.scoreCategory('maintainability', violations),
78
+ ];
79
+
80
+ const estimatedImprovementOnCriticalFix = Math.min(100 - overallScore, Math.round(criticalPenalty));
81
+ const estimatedImprovementOnAllFix = Math.min(100 - overallScore, Math.round(totalPenalty));
82
+
83
+ return {
84
+ overall: overallScore,
85
+ grade: CategoryScorer.getGrade(overallScore),
86
+ label: this.getLabel(overallScore),
87
+ categories,
88
+ totalPenalty,
89
+ penaltyBySeverity,
90
+ estimatedImprovementOnCriticalFix,
91
+ estimatedImprovementOnAllFix,
92
+ topRecommendation: this.getTopRecommendation(violations),
93
+ };
94
+ }
95
+ }
96
+
97
+ module.exports = { ScoreCalculator };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @file score/ScoreWeights.js
3
+ * @description
4
+ * Defines the constant weights and penalties for the scoring algorithm.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * Base penalty applied per violation based on severity.
11
+ * @type {Record<import('../types').Severity, number>}
12
+ */
13
+ const SEVERITY_PENALTY = {
14
+ critical: 15,
15
+ warning: 5,
16
+ info: 1,
17
+ };
18
+
19
+ /**
20
+ * Multiplier applied to the penalty based on category.
21
+ * @type {Record<import('../types').Category, number>}
22
+ */
23
+ const CATEGORY_WEIGHT = {
24
+ rendering: 1.5,
25
+ hooks: 1.3,
26
+ architecture: 1.0,
27
+ maintainability: 0.7,
28
+ };
29
+
30
+ module.exports = {
31
+ SEVERITY_PENALTY,
32
+ CATEGORY_WEIGHT,
33
+ };