devkit-quality 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.
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.scanRepository = scanRepository;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const typescript_1 = __importDefault(require("typescript"));
10
+ const discovery_1 = require("./discovery");
11
+ const parse_1 = require("./ast/parse");
12
+ const moduleGraph_1 = require("./moduleGraph");
13
+ const config_1 = require("./config");
14
+ const ignore_1 = require("./ignore");
15
+ const complexity_1 = require("./rules/complexity");
16
+ const deadCode_1 = require("./rules/deadCode");
17
+ const dependencies_1 = require("./rules/dependencies");
18
+ const duplication_1 = require("./rules/duplication");
19
+ const errorHandling_1 = require("./rules/errorHandling");
20
+ const redundancy_1 = require("./rules/redundancy");
21
+ const typescript_2 = require("./rules/typescript");
22
+ const javascript_1 = require("./rules/javascript");
23
+ const security_1 = require("./rules/security");
24
+ const architecture_1 = require("./rules/architecture");
25
+ const hygiene_1 = require("./rules/hygiene");
26
+ const scoring_1 = require("./scoring");
27
+ const suppressions_1 = require("./suppressions");
28
+ function countClassDeclarations(program, files) {
29
+ let count = 0;
30
+ for (const file of files) {
31
+ const sourceFile = program.getSourceFile(file);
32
+ if (!sourceFile)
33
+ continue;
34
+ const visit = (node) => {
35
+ if (typescript_1.default.isClassDeclaration(node) || typescript_1.default.isClassExpression(node)) {
36
+ count += 1;
37
+ }
38
+ typescript_1.default.forEachChild(node, visit);
39
+ };
40
+ visit(sourceFile);
41
+ }
42
+ return count;
43
+ }
44
+ function estimateDuplicatedLines(findings) {
45
+ let total = 0;
46
+ for (const finding of findings) {
47
+ if (finding.ruleId !== 'DUP001')
48
+ continue;
49
+ const match = finding.description.match(/^Lines (\d+)-(\d+)/);
50
+ if (match) {
51
+ total += Number(match[2]) - Number(match[1]) + 1;
52
+ }
53
+ }
54
+ return total;
55
+ }
56
+ function scanRepository(projectRoot) {
57
+ const config = (0, config_1.loadConfig)(projectRoot);
58
+ const allFiles = (0, discovery_1.walkDirectory)(projectRoot);
59
+ const { files, generatedFiles } = (0, discovery_1.filterScannedFiles)(allFiles, projectRoot, config, (0, ignore_1.loadIgnorePatterns)(projectRoot));
60
+ const packageJsonPath = node_path_1.default.join(projectRoot, 'package.json');
61
+ const packageJson = node_fs_1.default.existsSync(packageJsonPath) ? JSON.parse((0, discovery_1.readFileSafe)(packageJsonPath)) : null;
62
+ const program = (0, parse_1.createProgram)(files);
63
+ const moduleGraph = (0, moduleGraph_1.buildModuleGraph)(projectRoot, files, packageJson, config);
64
+ const context = { projectRoot, files, allFiles, program, moduleGraph, config, packageJson };
65
+ const functionMetrics = (0, complexity_1.collectFunctionMetrics)(context);
66
+ const allFindings = [
67
+ ...(0, deadCode_1.runDeadCodeRules)(context),
68
+ ...(0, dependencies_1.runDependencyRules)(context),
69
+ ...(0, complexity_1.runComplexityRules)(context, functionMetrics),
70
+ ...(0, duplication_1.runDuplicationRules)(context),
71
+ ...(0, errorHandling_1.runErrorHandlingRules)(context),
72
+ ...(0, redundancy_1.runRedundancyRules)(context),
73
+ ...(0, typescript_2.runTypeScriptRules)(context),
74
+ ...(0, javascript_1.runJavaScriptRules)(context),
75
+ ...(0, security_1.runSecurityRules)(context),
76
+ ...(0, architecture_1.runArchitectureRules)(context),
77
+ ...(0, hygiene_1.runHygieneRules)(context)
78
+ ];
79
+ const findings = allFindings.filter((finding) => {
80
+ const sourceFile = context.program.getSourceFile(node_path_1.default.resolve(projectRoot, finding.file));
81
+ return !sourceFile || !(0, suppressions_1.isSuppressed)(sourceFile, finding.line, finding.ruleId);
82
+ });
83
+ let sourceLOC = 0;
84
+ let testLOC = 0;
85
+ let commentLOC = 0;
86
+ const largestFiles = [];
87
+ for (const file of files) {
88
+ const text = (0, discovery_1.readFileSafe)(file);
89
+ const relativePath = node_path_1.default.relative(projectRoot, file).replace(/\\/g, '/');
90
+ const isTest = (0, discovery_1.isTestFile)(relativePath);
91
+ const loc = (0, discovery_1.countLines)(text);
92
+ if (isTest) {
93
+ testLOC += loc;
94
+ }
95
+ else {
96
+ sourceLOC += loc;
97
+ }
98
+ commentLOC += (text.match(/^\s*(\/\/|\/\*|\*)/gm) ?? []).length;
99
+ if (loc > 0) {
100
+ largestFiles.push({ file: relativePath, loc });
101
+ }
102
+ }
103
+ const sourceFiles = files.filter((file) => !(0, discovery_1.isTestFile)(file)).length;
104
+ const testFiles = files.filter((file) => (0, discovery_1.isTestFile)(file)).length;
105
+ const dependencyCount = [
106
+ ...Object.keys(packageJson?.dependencies ?? {}),
107
+ ...Object.keys(packageJson?.devDependencies ?? {})
108
+ ].length;
109
+ const functionCount = functionMetrics.length;
110
+ const classCount = countClassDeclarations(program, files);
111
+ const deadCodeFindingCount = findings.filter((finding) => finding.category === 'deadCode').length;
112
+ const metrics = {
113
+ totalFiles: files.length,
114
+ sourceFiles,
115
+ testFiles,
116
+ totalLOC: sourceLOC + testLOC,
117
+ sourceLOC,
118
+ testLOC,
119
+ commentLOC,
120
+ functionCount,
121
+ classCount,
122
+ dependencyCount,
123
+ averageFunctionSize: functionCount > 0 ? functionMetrics.reduce((sum, metric) => sum + metric.lines, 0) / functionCount : 0,
124
+ largestFunctions: [...functionMetrics]
125
+ .sort((a, b) => b.lines - a.lines)
126
+ .slice(0, 5)
127
+ .map((metric) => ({ name: metric.name, size: metric.lines, file: metric.file })),
128
+ largestFiles: largestFiles.sort((a, b) => b.loc - a.loc).slice(0, 5),
129
+ complexityDistribution: {
130
+ low: functionMetrics.filter((metric) => metric.cyclomatic <= 10).length,
131
+ medium: functionMetrics.filter((metric) => metric.cyclomatic > 10 && metric.cyclomatic <= 25).length,
132
+ high: functionMetrics.filter((metric) => metric.cyclomatic > 25).length
133
+ },
134
+ duplicationPercentage: sourceLOC > 0 ? Math.min(100, (estimateDuplicatedLines(findings) / sourceLOC) * 100) : 0,
135
+ // Normalized against declared functions/classes rather than LOC, since dead code is a count of
136
+ // unused declarations, not a span of lines.
137
+ deadCodePercentage: functionCount + classCount > 0 ? Math.min(100, (deadCodeFindingCount / (functionCount + classCount)) * 100) : 0,
138
+ testToSourceRatio: sourceLOC > 0 ? testLOC / sourceLOC : 0
139
+ };
140
+ const { overallScore, categoryScores, securityScore } = (0, scoring_1.computeScores)(findings, sourceLOC);
141
+ return {
142
+ repository: projectRoot,
143
+ score: overallScore,
144
+ categoryScores,
145
+ securityScore,
146
+ findings,
147
+ metrics,
148
+ generatedFiles
149
+ };
150
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CATEGORY_WEIGHTS = void 0;
4
+ exports.computeScores = computeScores;
5
+ exports.summarizeTopDeductions = summarizeTopDeductions;
6
+ exports.CATEGORY_WEIGHTS = {
7
+ deadCode: 0.2,
8
+ dependencies: 0.1,
9
+ complexity: 0.15,
10
+ duplication: 0.1,
11
+ redundantLogic: 0.1,
12
+ errorHandling: 0.1,
13
+ typescript: 0.1,
14
+ architecture: 0.1,
15
+ hygiene: 0.05
16
+ };
17
+ const SEVERITY_WEIGHT = {
18
+ CRITICAL: 3,
19
+ HIGH: 2.5,
20
+ MEDIUM: 1.5,
21
+ LOW: 0.8,
22
+ INFO: 0.2
23
+ };
24
+ const CONFIDENCE_WEIGHT = {
25
+ CERTAIN: 1,
26
+ HIGH: 0.8,
27
+ MEDIUM: 0.6,
28
+ LOW: 0.3
29
+ };
30
+ const LOC_CHUNK = 500;
31
+ function findingPenalty(finding) {
32
+ return SEVERITY_WEIGHT[finding.severity] * CONFIDENCE_WEIGHT[finding.confidence];
33
+ }
34
+ function normalizedCategoryScore(findings, sourceLOC) {
35
+ const penalty = findings.reduce((sum, finding) => sum + findingPenalty(finding), 0);
36
+ const normalizer = Math.max(sourceLOC / LOC_CHUNK, 1);
37
+ return Math.max(0, Math.min(10, 10 - penalty / normalizer));
38
+ }
39
+ function computeScores(findings, sourceLOC) {
40
+ const categoryScores = {};
41
+ for (const category of Object.keys(exports.CATEGORY_WEIGHTS)) {
42
+ const categoryFindings = findings.filter((finding) => finding.category === category);
43
+ categoryScores[category] = Number(normalizedCategoryScore(categoryFindings, sourceLOC).toFixed(1));
44
+ }
45
+ const overallScore = Object.entries(exports.CATEGORY_WEIGHTS).reduce((sum, [category, weight]) => sum + weight * categoryScores[category], 0);
46
+ const securityFindings = findings.filter((finding) => finding.category === 'security');
47
+ const securityScore = Number(normalizedCategoryScore(securityFindings, sourceLOC).toFixed(1));
48
+ return {
49
+ overallScore: Number(overallScore.toFixed(1)),
50
+ categoryScores,
51
+ securityScore
52
+ };
53
+ }
54
+ function summarizeTopDeductions(findings, limit = 8) {
55
+ const byRule = new Map();
56
+ for (const finding of findings) {
57
+ const existing = byRule.get(finding.ruleId);
58
+ const penalty = findingPenalty(finding);
59
+ if (existing) {
60
+ existing.count += 1;
61
+ existing.totalPenalty += penalty;
62
+ }
63
+ else {
64
+ byRule.set(finding.ruleId, { ruleId: finding.ruleId, category: finding.category, count: 1, totalPenalty: penalty });
65
+ }
66
+ }
67
+ return [...byRule.values()].sort((a, b) => b.totalPenalty - a.totalPenalty).slice(0, limit);
68
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectSuppressions = collectSuppressions;
4
+ exports.isSuppressed = isSuppressed;
5
+ const comments_1 = require("./ast/comments");
6
+ function collectSuppressions(sourceFile) {
7
+ const directives = [];
8
+ for (const range of (0, comments_1.collectCommentRanges)(sourceFile)) {
9
+ const text = sourceFile.text.slice(range.pos, range.end);
10
+ const match = /devkit-disable-next-line(?:\s+([A-Z]{2,}\d{3}(?:\s*,\s*[A-Z]{2,}\d{3})*))?/i.exec(text);
11
+ if (!match)
12
+ continue;
13
+ const line = sourceFile.getLineAndCharacterOfPosition(range.end).line + 2;
14
+ const ruleIds = match[1] ? new Set(match[1].split(',').map((id) => id.trim().toUpperCase())) : null;
15
+ directives.push({ line, ruleIds });
16
+ }
17
+ return directives;
18
+ }
19
+ function isSuppressed(sourceFile, line, ruleId) {
20
+ return collectSuppressions(sourceFile).some((directive) => directive.line === line && (!directive.ruleIds || directive.ruleIds.has(ruleId.toUpperCase())));
21
+ }
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Spinner = exports.color = void 0;
7
+ exports.severityColor = severityColor;
8
+ exports.scoreColor = scoreColor;
9
+ exports.scoreLabel = scoreLabel;
10
+ exports.renderScoreBar = renderScoreBar;
11
+ exports.pad = pad;
12
+ exports.padStart = padStart;
13
+ exports.renderCodeFrame = renderCodeFrame;
14
+ exports.waitForNextTick = waitForNextTick;
15
+ const node_fs_1 = __importDefault(require("node:fs"));
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ const colorEnabled = Boolean(process.env.FORCE_COLOR) || (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
18
+ function wrap(open, close) {
19
+ return (text) => (colorEnabled ? `\x1b[${open}m${text}\x1b[${close}m` : text);
20
+ }
21
+ exports.color = {
22
+ bold: wrap('1', '22'),
23
+ dim: wrap('2', '22'),
24
+ red: wrap('31', '39'),
25
+ green: wrap('32', '39'),
26
+ yellow: wrap('33', '39'),
27
+ blue: wrap('34', '39'),
28
+ magenta: wrap('35', '39'),
29
+ cyan: wrap('36', '39'),
30
+ gray: wrap('90', '39'),
31
+ white: wrap('37', '39')
32
+ };
33
+ function severityColor(severity, text) {
34
+ switch (severity) {
35
+ case 'CRITICAL':
36
+ case 'HIGH':
37
+ return exports.color.red(text);
38
+ case 'MEDIUM':
39
+ return exports.color.yellow(text);
40
+ case 'LOW':
41
+ return exports.color.cyan(text);
42
+ default:
43
+ return exports.color.gray(text);
44
+ }
45
+ }
46
+ function scoreColor(score, text) {
47
+ if (score >= 8)
48
+ return exports.color.green(text);
49
+ if (score >= 5)
50
+ return exports.color.yellow(text);
51
+ return exports.color.red(text);
52
+ }
53
+ function scoreLabel(score) {
54
+ if (score >= 9)
55
+ return 'Excellent';
56
+ if (score >= 7)
57
+ return 'Good';
58
+ if (score >= 5)
59
+ return 'Needs work';
60
+ return 'Poor';
61
+ }
62
+ function renderScoreBar(score, width = 28) {
63
+ const filled = Math.round((Math.max(0, Math.min(10, score)) / 10) * width);
64
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
65
+ return scoreColor(score, bar);
66
+ }
67
+ function pad(text, width) {
68
+ return text.length >= width ? text : text + ' '.repeat(width - text.length);
69
+ }
70
+ function padStart(text, width) {
71
+ return text.length >= width ? text : ' '.repeat(width - text.length) + text;
72
+ }
73
+ function renderCodeFrame(repository, relativeFile, line, column, severity) {
74
+ const absolutePath = node_path_1.default.join(repository, relativeFile);
75
+ let lines;
76
+ try {
77
+ lines = node_fs_1.default.readFileSync(absolutePath, 'utf8').split(/\r\n|\r|\n/);
78
+ }
79
+ catch {
80
+ return [];
81
+ }
82
+ const startLine = Math.max(1, line - 1);
83
+ const endLine = Math.min(lines.length, line + 1);
84
+ const gutterWidth = String(endLine).length;
85
+ const output = [];
86
+ const maxWidth = 100;
87
+ const truncate = (text) => (text.length > maxWidth ? `${text.slice(0, maxWidth)}…` : text);
88
+ for (let current = startLine; current <= endLine; current += 1) {
89
+ const rawText = lines[current - 1] ?? '';
90
+ const text = truncate(rawText);
91
+ const gutter = padStart(String(current), gutterWidth);
92
+ const marker = current === line ? severityColor(severity, '>') : ' ';
93
+ if (current === line) {
94
+ output.push(` ${marker} ${exports.color.bold(gutter)} ${exports.color.dim('|')} ${text}`);
95
+ if (column > 0 && column - 1 < maxWidth) {
96
+ output.push(` ${' '.repeat(gutterWidth + 2)} ${exports.color.dim('|')} ${' '.repeat(Math.max(0, column - 1))}${severityColor(severity, '^')}`);
97
+ }
98
+ }
99
+ else {
100
+ output.push(` ${marker} ${exports.color.dim(gutter)} ${exports.color.dim('|')} ${exports.color.dim(text)}`);
101
+ }
102
+ }
103
+ return output;
104
+ }
105
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
106
+ class Spinner {
107
+ frameIndex = 0;
108
+ timer;
109
+ text;
110
+ constructor(text) {
111
+ this.text = text;
112
+ }
113
+ start() {
114
+ // Written to stderr so it never pollutes piped/redirected stdout (--format json, etc.).
115
+ if (!process.stderr.isTTY) {
116
+ process.stderr.write(`${this.text}\n`);
117
+ return;
118
+ }
119
+ this.timer = setInterval(() => {
120
+ process.stderr.write(`\r${exports.color.cyan(SPINNER_FRAMES[this.frameIndex])} ${this.text}`);
121
+ this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
122
+ }, 80);
123
+ }
124
+ stop() {
125
+ if (!this.timer)
126
+ return;
127
+ clearInterval(this.timer);
128
+ this.timer = undefined;
129
+ process.stderr.write('\r\x1b[K');
130
+ }
131
+ }
132
+ exports.Spinner = Spinner;
133
+ function waitForNextTick() {
134
+ return new Promise((resolve) => setImmediate(resolve));
135
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "devkit-quality",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic repository quality scanner for JavaScript and TypeScript projects",
5
+ "bin": {
6
+ "devkit-quality": "bin/devkit.js"
7
+ },
8
+ "main": "dist/cli.js",
9
+ "files": [
10
+ "dist",
11
+ "bin",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "start": "node dist/cli.js",
18
+ "dev": "tsx src/cli.ts",
19
+ "scan": "tsx src/cli.ts scan",
20
+ "test": "vitest run",
21
+ "prepublishOnly": "npm run build && npm test"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/Karan071/devkit-cli.git"
26
+ },
27
+ "keywords": [
28
+ "cli",
29
+ "typescript",
30
+ "code-quality",
31
+ "slop-scanner",
32
+ "static-analysis"
33
+ ],
34
+ "license": "MIT",
35
+ "author": "Karan Chourasia",
36
+ "type": "commonjs",
37
+ "dependencies": {
38
+ "commander": "^12.1.0",
39
+ "typescript": "^5.7.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.10.2",
43
+ "tsx": "^4.19.2",
44
+ "vitest": "^5.0.3"
45
+ }
46
+ }