devkit-quality 0.1.0 → 0.1.1
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.
- package/CHANGELOG.md +93 -0
- package/README.md +38 -15
- package/dist/cli.js +71 -49
- package/dist/cliLogic.js +39 -4
- package/dist/discovery.js +175 -17
- package/dist/glob.js +5 -1
- package/dist/ignore.js +14 -5
- package/dist/moduleGraph.js +4 -1
- package/dist/reporters.js +180 -92
- package/dist/rules/deadCode.js +4 -4
- package/dist/rules/dependencies.js +126 -53
- package/dist/rules/duplication.js +24 -9
- package/dist/rules/errorHandling.js +3 -1
- package/dist/rules/hygiene.js +11 -2
- package/dist/rules/security.js +20 -6
- package/dist/scanner.js +90 -27
- package/dist/scoring.js +53 -15
- package/dist/suppressions.js +28 -4
- package/dist/terminal.js +151 -37
- package/package.json +11 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file. The format is
|
|
4
|
+
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
5
|
+
project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.1] - 2026-10-02
|
|
10
|
+
|
|
11
|
+
**Heads up if you ran `devkit scan` on 0.1.0:** file discovery silently
|
|
12
|
+
skipped a meaningful share of most repositories (any `build`/`out`/`vendor`/
|
|
13
|
+
`dist`/`coverage`-named folder anywhere in the tree, any file with a "do not
|
|
14
|
+
edit" comment anywhere in it, and anything outside a correctly-anchored
|
|
15
|
+
`.gitignore` pattern), and the secret scanner only ever looked at `.js`/`.ts`
|
|
16
|
+
files. A clean-looking score or a "no findings" security result from 0.1.0
|
|
17
|
+
may reflect files that were never actually scanned, including `.env`, YAML,
|
|
18
|
+
and JSON config files that can hold real credentials. Re-run `devkit scan` on
|
|
19
|
+
0.1.1 before trusting a prior result.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Discovery no longer silently skips source files: folders named `build`,
|
|
24
|
+
`out`, `dist`, or `coverage` are only skipped when they are build output in
|
|
25
|
+
a package root (a `src/build/` or `src/commands/out/` is scanned normally);
|
|
26
|
+
`vendor/` is never auto-skipped, since vendored/checked-in code is still
|
|
27
|
+
real, committed content; root-anchored `.gitignore` patterns (`/lib`) stay
|
|
28
|
+
anchored instead of matching at every depth; a "do not edit" note is only
|
|
29
|
+
treated as a generated-file marker when it appears in the file's leading
|
|
30
|
+
comment block, not anywhere in the file; `**/x` globs no longer false-match
|
|
31
|
+
`myx`. Discovery now uses `git ls-files` when available, so `.gitignore` is
|
|
32
|
+
applied with exact git semantics.
|
|
33
|
+
- Secret scanning now covers every text file (`.env`, YAML, JSON,
|
|
34
|
+
Dockerfiles, other languages), not only JS/TS. A `.gitignore`d `.env` is no
|
|
35
|
+
longer reported as committed.
|
|
36
|
+
- Dependency rules (`DEP001`/`DEP002`) are now workspace-aware: every
|
|
37
|
+
`package.json` in the repo is checked against its own files, with the
|
|
38
|
+
workspace root tolerated as a hoisting source. Previously, every
|
|
39
|
+
correctly-declared dependency in a non-root workspace/monorepo package was
|
|
40
|
+
flagged as "unlisted."
|
|
41
|
+
- Scoring: security findings are now part of the overall score and cap it at
|
|
42
|
+
6.9 on a credible (high-confidence, non-test) finding, instead of being
|
|
43
|
+
computed and then discarded; architecture is only scored when
|
|
44
|
+
`architecture.layers` is configured, instead of granting free points;
|
|
45
|
+
findings inside test files count at half weight; a scan that analyzes zero
|
|
46
|
+
source files now always fails `--min-score` instead of scoring 10.
|
|
47
|
+
- CLI: `--category` accepts `dead-code`-style names (previously only the
|
|
48
|
+
internal camelCase form matched, so the documented examples didn't work);
|
|
49
|
+
filtering findings with `--category`/`--severity` no longer recomputes a
|
|
50
|
+
fake score for the filtered subset; invalid `--min-score`, `--fail-on`, and
|
|
51
|
+
`--format` values are now rejected instead of silently passing.
|
|
52
|
+
- Duplication detection no longer flags shared `import` headers between
|
|
53
|
+
files, or overlapping windows within the same file, as copy-pasted code.
|
|
54
|
+
- An empty catch block with an explanatory comment is no longer flagged by
|
|
55
|
+
`ERR001`, matching the rule's own suggested fix.
|
|
56
|
+
- Fixed the published CLI command name (`devkit`) to match the `bin` entry in
|
|
57
|
+
`package.json`.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- Every scanning command (`scan`, `report`, `metrics`, `fix`,
|
|
62
|
+
`baseline create`/`compare`) now accepts an optional `[path]` argument to
|
|
63
|
+
scan a directory other than the current one.
|
|
64
|
+
- A `devkit-disable-file` suppression directive, alongside the existing
|
|
65
|
+
`devkit-disable-next-line`, for silencing specific rules (or all rules)
|
|
66
|
+
across a whole file.
|
|
67
|
+
- Scan coverage is now reported: files found vs. analyzed vs. secret-scanned,
|
|
68
|
+
generated/oversized/binary files skipped, and scan duration.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- Terminal output redesigned: live progress bar during the scan (previously a
|
|
73
|
+
spinner that never animated, since the scan is synchronous), a letter
|
|
74
|
+
grade, a scan-coverage section, per-category score bars, a severity
|
|
75
|
+
distribution bar, a file "hotspots" list, and restyled issue cards.
|
|
76
|
+
- `console.*` findings (`HYGIENE001`) in a file that imports a CLI
|
|
77
|
+
argument-parsing library (commander, yargs, cac, meow, sade, clipanion) are
|
|
78
|
+
now reported at LOW confidence instead of CERTAIN, since that output is
|
|
79
|
+
usually the program's actual product, not a debug leftover. Still
|
|
80
|
+
reported, not suppressed.
|
|
81
|
+
- Open-sourced the project: contributing guide, code of conduct, security
|
|
82
|
+
policy, issue/PR templates, and CI.
|
|
83
|
+
|
|
84
|
+
## [0.1.0]
|
|
85
|
+
|
|
86
|
+
- Initial release: deterministic scan pipeline (discovery, TypeScript
|
|
87
|
+
Compiler API parse, module graph, rules, scoring, reporting).
|
|
88
|
+
- Rule categories: dead code, dependencies, complexity, duplication, error
|
|
89
|
+
handling, redundant logic, TypeScript safety, JavaScript hygiene, security,
|
|
90
|
+
architecture, repository hygiene.
|
|
91
|
+
- Terminal, JSON, Markdown, and SARIF output formats.
|
|
92
|
+
- `devkit init`, `scan`, `report`, `metrics`, `rules`, `explain`, `baseline`,
|
|
93
|
+
and `fix` commands.
|
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# DevKit
|
|
2
2
|
|
|
3
|
+
[](https://github.com/Karan071/devkit-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/devkit-quality)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
DevKit is a deterministic, offline code-quality scanner for JavaScript and TypeScript repositories. It walks a project, parses every source file with the TypeScript compiler, and reports a 0-10 "code quality" score backed by concrete, file-and-line findings — dead code, unused dependencies, excessive complexity, duplication, unsafe error handling, unsafe TypeScript, security anti-patterns, architecture violations, and repository hygiene issues.
|
|
4
8
|
|
|
5
9
|
It does not call an LLM, an API, or the network. Every finding is reproducible from the source tree alone.
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
|
-
npx devkit scan
|
|
12
|
+
npx devkit-quality scan
|
|
9
13
|
```
|
|
10
14
|
|
|
11
15
|
```
|
|
@@ -33,13 +37,15 @@ DevKit Repository Scan
|
|
|
33
37
|
- [Output formats](#output-formats)
|
|
34
38
|
- [CI/CD integration](#cicd-integration)
|
|
35
39
|
- [Development](#development)
|
|
40
|
+
- [Contributing](#contributing)
|
|
41
|
+
- [License](#license)
|
|
36
42
|
|
|
37
43
|
## How it works
|
|
38
44
|
|
|
39
45
|
A scan runs in a single pass over the repository:
|
|
40
46
|
|
|
41
|
-
1. **Discovery** — walk
|
|
42
|
-
2. **Parse** — build one `ts.Program` (via the TypeScript Compiler API) covering every discovered file, with `noUnusedLocals`/`noUnusedParameters`/`noImplicitAny` enabled so the compiler itself surfaces dead bindings and implicit `any`.
|
|
47
|
+
1. **Discovery** — list files with `git ls-files` (exact `.gitignore` semantics; falls back to a filesystem walk outside git), apply include/exclude and `.devkitignore`, skip generated/minified files and build output in package roots, and split files into JS/TS sources (`.js`/`.jsx`/`.ts`/`.tsx`/`.mjs`/`.cjs`/`.mts`/`.cts`, fully analyzed) and other text files (configs, `.env`, YAML, other languages — secret-scanned).
|
|
48
|
+
2. **Parse & type-check** — build one `ts.Program` (via the TypeScript Compiler API) covering every discovered file, with `noUnusedLocals`/`noUnusedParameters`/`noImplicitAny` enabled so the compiler itself surfaces dead bindings and implicit `any`.
|
|
43
49
|
3. **Module graph** — resolve imports, `require()`, dynamic `import()`, TypeScript path aliases, and re-exports into an internal dependency graph (edges, reverse edges, entry points, re-export targets).
|
|
44
50
|
4. **Rules** — each rule module receives a shared `RuleContext` (files, program, module graph, config, `package.json`) and returns `Finding[]`.
|
|
45
51
|
5. **Scoring** — findings are weighted by severity × confidence, normalized against repository size, and rolled up into per-category and overall scores.
|
|
@@ -101,7 +107,7 @@ Design principle carried over from the project's PRD (`devkit-slop-scanner-final
|
|
|
101
107
|
| [`src/ast/comments.ts`](src/ast/comments.ts) | Collects and caches comment ranges for hygiene checks and suppressions. |
|
|
102
108
|
| [`src/moduleResolution.ts`](src/moduleResolution.ts) | Resolves modules using TypeScript config, path aliases, and a per-scan cache. |
|
|
103
109
|
| [`src/moduleGraph.ts`](src/moduleGraph.ts) | Resolves imports/exports/re-exports into an internal dependency graph; detects entry points and import cycles. |
|
|
104
|
-
| [`src/suppressions.ts`](src/suppressions.ts) | Applies `devkit-disable-next-line` directives to findings. |
|
|
110
|
+
| [`src/suppressions.ts`](src/suppressions.ts) | Applies `devkit-disable-next-line` and `devkit-disable-file` directives to findings. |
|
|
105
111
|
| [`src/cliLogic.ts`](src/cliLogic.ts) | Pure finding-filter and quality-gate logic used by the CLI. |
|
|
106
112
|
| [`src/config.ts`](src/config.ts) | Loads/writes `.devkitrc.json`; rule enable/disable and threshold overrides. |
|
|
107
113
|
| [`src/context.ts`](src/context.ts) | `RuleContext` type shared by every rule module. |
|
|
@@ -131,7 +137,7 @@ Design principle carried over from the project's PRD (`devkit-slop-scanner-final
|
|
|
131
137
|
|
|
132
138
|
Run `devkit rules` for the live list, or `devkit explain <RULE_ID>` for a rule's full writeup (why it matters, example, fix classification).
|
|
133
139
|
|
|
134
|
-
> **Note:** `devkit-slop-scanner-final-prd.md` in the repo root is the original product-requirements document and describes a larger target surface (incremental caching, monorepo-aware scoring, rule presets, a V2 LLM layer, and more). The table above reflects what is currently implemented in `src/`; treat the PRD as the roadmap, not the current feature set. Minimal `devkit-disable-next-line` suppressions are implemented.
|
|
140
|
+
> **Note:** `devkit-slop-scanner-final-prd.md` in the repo root is the original product-requirements document and describes a larger target surface (incremental caching, monorepo-aware scoring, rule presets, a V2 LLM layer, and more). The table above reflects what is currently implemented in `src/`; treat the PRD as the roadmap, not the current feature set. Minimal `devkit-disable-next-line` and `devkit-disable-file` suppressions are implemented; monorepo-aware dependency scoring (DEP001/DEP002) is implemented.
|
|
135
141
|
|
|
136
142
|
## Project layout
|
|
137
143
|
|
|
@@ -160,7 +166,7 @@ cli-tool/
|
|
|
160
166
|
|
|
161
167
|
## Setup
|
|
162
168
|
|
|
163
|
-
Requires Node.js (
|
|
169
|
+
Requires Node.js `^22.12.0 || ^24.0.0 || >=26.0.0` (the version range the test suite's dependencies require; Node 18 and 20 are not supported).
|
|
164
170
|
|
|
165
171
|
```bash
|
|
166
172
|
# install dependencies
|
|
@@ -202,10 +208,11 @@ Run a full scan (defaults to colored terminal output):
|
|
|
202
208
|
|
|
203
209
|
```bash
|
|
204
210
|
devkit scan
|
|
211
|
+
devkit scan ../other-repo # scan another directory (every scanning command takes an optional path)
|
|
205
212
|
devkit scan --json # machine-readable JSON (ScanSummary)
|
|
206
213
|
devkit scan --format markdown # Markdown report
|
|
207
214
|
devkit scan --format sarif # SARIF, for code-scanning platforms
|
|
208
|
-
devkit scan --category dead-code # only one category
|
|
215
|
+
devkit scan --category dead-code # only one category (dead-code, security, type-safety, ...)
|
|
209
216
|
devkit scan --severity high # only one severity level
|
|
210
217
|
```
|
|
211
218
|
|
|
@@ -262,25 +269,29 @@ Architecture-layer rules (`ARCH001`) are opt-in and only produce findings once c
|
|
|
262
269
|
|
|
263
270
|
## Scoring model
|
|
264
271
|
|
|
265
|
-
Each finding is weighted by `severity × confidence` ([`src/scoring.ts`](src/scoring.ts))
|
|
272
|
+
Each finding is weighted by `severity × confidence` ([`src/scoring.ts`](src/scoring.ts)); findings in test files count at half weight. Quality categories are normalized against lines of code (in 500-line chunks), so the same number of findings costs less in a larger repository. Security is normalized only by the square root of size, capped at 3×, because one leaked key is just as serious in a large repository. Category scores are combined into the overall score using these weights:
|
|
266
273
|
|
|
267
274
|
| Category | Weight |
|
|
268
275
|
| --- | ---: |
|
|
269
|
-
| Dead code |
|
|
276
|
+
| Dead code | 15% |
|
|
270
277
|
| Complexity | 15% |
|
|
278
|
+
| Security | 15% |
|
|
271
279
|
| Dependencies | 10% |
|
|
272
280
|
| Duplication | 10% |
|
|
273
|
-
| Redundant logic | 10% |
|
|
274
281
|
| Error handling | 10% |
|
|
275
282
|
| Type safety | 10% |
|
|
276
|
-
|
|
|
283
|
+
| Redundant logic | 5% |
|
|
284
|
+
| Architecture | 5% |
|
|
277
285
|
| Hygiene | 5% |
|
|
278
286
|
|
|
279
|
-
Security
|
|
287
|
+
- **Security cap:** a HIGH/CRITICAL security finding with HIGH or CERTAIN confidence outside test files caps the overall score at 6.9, however clean the rest of the code is.
|
|
288
|
+
- **Architecture** is only scored when `architecture.layers` is configured; otherwise it is shown as `n/a` and the other weights are rescaled.
|
|
289
|
+
- **Filters** (`--category`, `--severity`) only narrow the findings that are displayed. Scores and `--min-score` always use the whole repository.
|
|
290
|
+
- A scan that analyzes zero JS/TS files prints a warning and always fails `--min-score`.
|
|
280
291
|
|
|
281
292
|
## Output formats
|
|
282
293
|
|
|
283
|
-
- **Terminal** (default) —
|
|
294
|
+
- **Terminal** (default) — live progress on stderr, then a score with letter grade, scan coverage (files found, analyzed, secret-scanned, skipped, duration), per-category score bars, severity distribution, file hotspots, and code frames for the worst finding per category. Respects `NO_COLOR`/`FORCE_COLOR`; set `DEVKIT_ASCII=1` for plain-ASCII glyphs.
|
|
284
295
|
- **JSON** (`--json` / `--format json`) — the full `ScanSummary` object: score, category scores, every finding, repository metrics.
|
|
285
296
|
- **Markdown** (`--format markdown` / `devkit report`) — category table, top deductions, and a flat findings list, suitable for pasting into a PR description.
|
|
286
297
|
- **SARIF** (`--format sarif`) — standard SARIF 2.1.0, for GitHub code scanning and similar tools.
|
|
@@ -288,10 +299,14 @@ Security findings are scored and reported separately rather than folded into the
|
|
|
288
299
|
## Current limitations
|
|
289
300
|
|
|
290
301
|
- Framework entry-point detection is limited to package metadata, tests, and a basic Next.js convention check. Other React setups such as Vite may need entry files specified through package metadata or imports.
|
|
291
|
-
- `.gitignore`
|
|
302
|
+
- Outside a git repository, `.gitignore` is approximated (negation patterns `!pattern` are skipped). `.devkitignore` never supports negation.
|
|
303
|
+
- Non-JS/TS files (Python, Go, YAML, ...) only get the secret scan, not code-quality rules. Script blocks in `.vue`/`.svelte` files are not parsed.
|
|
292
304
|
- `devkit fix` only previews findings marked as safe. It does not modify files.
|
|
293
305
|
- `devkit baseline compare` compares the overall score only; it does not report newly added or resolved findings.
|
|
294
|
-
-
|
|
306
|
+
- The secret scan is scoped by `scan.exclude`, not `scan.include`: a secret outside your configured `include` globs is still reported, by design (security blind spots are worse than noise), but this is easy to miss if you expect `include` to fully sandbox a scan.
|
|
307
|
+
- DEP001/DEP002 are workspace-aware (each `package.json` in the repo is checked against its own files, with the root tolerated as a hoisting source) but do not read `pnpm-workspace.yaml` or Lerna config — only the `package.json` layout itself.
|
|
308
|
+
- The CLI-entry heuristic that discounts `console.*` findings looks for an import of a known argv-parsing library (commander, yargs, cac, meow, sade, clipanion). A hand-rolled CLI without one of these still gets flagged at full confidence.
|
|
309
|
+
- DevKit has no rule presets, React-specific or test-quality rules, configuration analysis, or incremental cache.
|
|
295
310
|
|
|
296
311
|
## CI/CD integration
|
|
297
312
|
|
|
@@ -310,3 +325,11 @@ npm run dev # tsx src/cli.ts (no build step)
|
|
|
310
325
|
```
|
|
311
326
|
|
|
312
327
|
Tests live in [`src/__tests__/`](src/__tests__), one file per rule category plus an end-to-end scan test (`e2e.test.ts`) and a scoring test. Each rule test typically builds a small in-memory `RuleContext` (see [`testUtils.ts`](src/__tests__/testUtils.ts)) and asserts on the findings a rule produces for known-good and known-bad snippets.
|
|
328
|
+
|
|
329
|
+
## Contributing
|
|
330
|
+
|
|
331
|
+
Contributions are welcome — new rules, bug fixes, false-positive reports, and documentation improvements. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup and the process for adding a rule. This project follows the [Code of Conduct](CODE_OF_CONDUCT.md). To report a security issue, see [SECURITY.md](SECURITY.md) rather than opening a public issue.
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
[MIT](LICENSE) © Karan Chourasia
|
package/dist/cli.js
CHANGED
|
@@ -11,18 +11,26 @@ const config_1 = require("./config");
|
|
|
11
11
|
const rules_1 = require("./rules");
|
|
12
12
|
const reporters_1 = require("./reporters");
|
|
13
13
|
const scanner_1 = require("./scanner");
|
|
14
|
-
const scoring_1 = require("./scoring");
|
|
15
14
|
const terminal_1 = require("./terminal");
|
|
16
15
|
const cliLogic_1 = require("./cliLogic");
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
function resolveTarget(target) {
|
|
17
|
+
const root = node_path_1.default.resolve(target ?? process.cwd());
|
|
18
|
+
if (!node_fs_1.default.existsSync(root) || !node_fs_1.default.statSync(root).isDirectory()) {
|
|
19
|
+
throw new Error(`Not a directory: ${root}`);
|
|
20
|
+
}
|
|
21
|
+
return root;
|
|
22
|
+
}
|
|
23
|
+
function scanWithProgress(root) {
|
|
24
|
+
const progress = new terminal_1.ProgressRenderer();
|
|
21
25
|
try {
|
|
22
|
-
|
|
26
|
+
const summary = (0, scanner_1.scanRepository)(root, (update) => progress.update(update));
|
|
27
|
+
const coverage = summary.coverage;
|
|
28
|
+
progress.finish(coverage ? `Scanned ${coverage.analyzedFiles} source files + ${coverage.textFilesScanned} other files` : 'Scan complete');
|
|
29
|
+
return summary;
|
|
23
30
|
}
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
catch (error) {
|
|
32
|
+
progress.fail();
|
|
33
|
+
throw error;
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
const program = new commander_1.Command();
|
|
@@ -34,17 +42,26 @@ program
|
|
|
34
42
|
const configPath = (0, config_1.writeConfig)(process.cwd());
|
|
35
43
|
console.log(`Created config at ${configPath}`);
|
|
36
44
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
findings: filteredFindings,
|
|
45
|
-
...(0, scoring_1.computeScores)(filteredFindings, summary.metrics.sourceLOC)
|
|
46
|
-
};
|
|
45
|
+
const OUTPUT_FORMATS = ['terminal', 'json', 'markdown', 'sarif'];
|
|
46
|
+
function runScan(target, options) {
|
|
47
|
+
const gateError = (0, cliLogic_1.validateGates)(options);
|
|
48
|
+
if (gateError) {
|
|
49
|
+
console.error(gateError);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
47
52
|
const outputType = options.json ? 'json' : (options.format ?? 'terminal');
|
|
53
|
+
if (!OUTPUT_FORMATS.includes(outputType)) {
|
|
54
|
+
console.error(`Unknown format "${outputType}". Use one of: ${OUTPUT_FORMATS.join(', ')}`);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const summary = scanWithProgress(resolveTarget(target));
|
|
58
|
+
// Filters narrow the findings that are displayed; scores always describe the whole repository,
|
|
59
|
+
// so `--category security --min-score 8` still gates on the real overall score.
|
|
60
|
+
const filteredFindings = (0, cliLogic_1.filterFindings)(summary.findings, options);
|
|
61
|
+
const filteredSummary = { ...summary, findings: filteredFindings };
|
|
62
|
+
if ((options.category || options.severity) && filteredFindings.length === 0 && summary.findings.length > 0 && outputType === 'terminal') {
|
|
63
|
+
console.error(terminal_1.color.yellow(`No findings match the filter. Categories: ${[...new Set(summary.findings.map((finding) => finding.category))].join(', ')}`));
|
|
64
|
+
}
|
|
48
65
|
const output = outputType === 'json'
|
|
49
66
|
? (0, reporters_1.formatJson)(filteredSummary)
|
|
50
67
|
: outputType === 'markdown'
|
|
@@ -53,45 +70,50 @@ async function runScan(options) {
|
|
|
53
70
|
? (0, reporters_1.formatSarif)(filteredSummary)
|
|
54
71
|
: (0, reporters_1.formatTerminal)(filteredSummary);
|
|
55
72
|
console.log(output);
|
|
56
|
-
|
|
57
|
-
console.error(`Unknown severity: ${options.failOn}`);
|
|
58
|
-
return (0, cliLogic_1.determineExitFailure)(filteredSummary, options);
|
|
73
|
+
return (0, cliLogic_1.determineExitFailure)(summary, { minScore: options.minScore }) || (0, cliLogic_1.determineExitFailure)(filteredSummary, { failOn: options.failOn });
|
|
59
74
|
}
|
|
60
75
|
program
|
|
61
|
-
.command('scan')
|
|
62
|
-
.description('Scan
|
|
76
|
+
.command('scan [path]')
|
|
77
|
+
.description('Scan a repository (default: current directory) for code-quality issues')
|
|
63
78
|
.option('--json', 'Output JSON instead of terminal text')
|
|
64
79
|
.option('--format <type>', 'Output format: terminal, json, markdown, sarif')
|
|
65
|
-
.option('--category <name>', '
|
|
66
|
-
.option('--severity <level>', '
|
|
67
|
-
.option('--min-score <score>', 'Fail with exit code 1 when score falls below this threshold')
|
|
68
|
-
.option('--fail-on <severity>', 'Fail with exit code 1 when any finding at or above this severity
|
|
69
|
-
.action(
|
|
70
|
-
if (
|
|
80
|
+
.option('--category <name>', 'Show only one category (e.g. security, dead-code, complexity)')
|
|
81
|
+
.option('--severity <level>', 'Show only one severity (critical, high, medium, low, info)')
|
|
82
|
+
.option('--min-score <score>', 'Fail with exit code 1 when the overall score falls below this threshold')
|
|
83
|
+
.option('--fail-on <severity>', 'Fail with exit code 1 when any shown finding is at or above this severity')
|
|
84
|
+
.action((target, options) => {
|
|
85
|
+
if (runScan(target, options)) {
|
|
71
86
|
process.exitCode = 1;
|
|
72
87
|
}
|
|
73
88
|
});
|
|
74
89
|
program
|
|
75
|
-
.command('report')
|
|
76
|
-
.description('Generate a detailed Markdown report of
|
|
77
|
-
.action(
|
|
78
|
-
|
|
79
|
-
console.log((0, reporters_1.formatMarkdown)(summary));
|
|
90
|
+
.command('report [path]')
|
|
91
|
+
.description('Generate a detailed Markdown report of a repository')
|
|
92
|
+
.action((target) => {
|
|
93
|
+
console.log((0, reporters_1.formatMarkdown)(scanWithProgress(resolveTarget(target))));
|
|
80
94
|
});
|
|
81
95
|
program
|
|
82
|
-
.command('metrics')
|
|
83
|
-
.description('Print repository metrics
|
|
84
|
-
.action(
|
|
85
|
-
|
|
86
|
-
console.log((0, reporters_1.formatMetrics)(summary));
|
|
96
|
+
.command('metrics [path]')
|
|
97
|
+
.description('Print repository metrics')
|
|
98
|
+
.action((target) => {
|
|
99
|
+
console.log((0, reporters_1.formatMetrics)(scanWithProgress(resolveTarget(target))));
|
|
87
100
|
});
|
|
88
101
|
program
|
|
89
102
|
.command('rules')
|
|
90
103
|
.description('List the available built-in rules')
|
|
91
104
|
.action(() => {
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
const rules = (0, rules_1.listRules)();
|
|
106
|
+
const idWidth = Math.max(...rules.map((rule) => rule.id.length)) + 2;
|
|
107
|
+
const titleWidth = Math.max(...rules.map((rule) => rule.title.length)) + 2;
|
|
108
|
+
let category = '';
|
|
109
|
+
for (const rule of [...rules].sort((a, b) => a.category.localeCompare(b.category) || a.id.localeCompare(b.id))) {
|
|
110
|
+
if (rule.category !== category) {
|
|
111
|
+
category = rule.category;
|
|
112
|
+
console.log(`\n${terminal_1.color.bold(category)}`);
|
|
113
|
+
}
|
|
114
|
+
console.log(` ${terminal_1.color.cyan((0, terminal_1.pad)(rule.id, idWidth))}${(0, terminal_1.pad)(rule.title, titleWidth)}${(0, terminal_1.severityColor)(rule.severity, rule.severity.toLowerCase())}`);
|
|
94
115
|
}
|
|
116
|
+
console.log(terminal_1.color.dim(`\n${rules.length} rules · devkit explain <RULE_ID> for details`));
|
|
95
117
|
});
|
|
96
118
|
program
|
|
97
119
|
.command('explain <ruleId>')
|
|
@@ -116,17 +138,17 @@ program
|
|
|
116
138
|
}
|
|
117
139
|
});
|
|
118
140
|
program
|
|
119
|
-
.command('baseline <action>')
|
|
141
|
+
.command('baseline <action> [path]')
|
|
120
142
|
.description('Create or compare a baseline score stored under .devkit')
|
|
121
|
-
.action(
|
|
122
|
-
const root =
|
|
143
|
+
.action((action, target) => {
|
|
144
|
+
const root = resolveTarget(target);
|
|
123
145
|
const dir = node_path_1.default.join(root, '.devkit');
|
|
124
146
|
const baselineFile = node_path_1.default.join(dir, 'baseline.json');
|
|
125
147
|
if (!node_fs_1.default.existsSync(dir)) {
|
|
126
148
|
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
127
149
|
}
|
|
128
150
|
if (action === 'create') {
|
|
129
|
-
const summary =
|
|
151
|
+
const summary = scanWithProgress(root);
|
|
130
152
|
node_fs_1.default.writeFileSync(baselineFile, JSON.stringify({ score: summary.score }, null, 2));
|
|
131
153
|
console.log(`Created baseline at ${baselineFile} with score ${summary.score.toFixed(1)}`);
|
|
132
154
|
return;
|
|
@@ -138,7 +160,7 @@ program
|
|
|
138
160
|
return;
|
|
139
161
|
}
|
|
140
162
|
const previousBaseline = JSON.parse(node_fs_1.default.readFileSync(baselineFile, 'utf8'));
|
|
141
|
-
const current =
|
|
163
|
+
const current = scanWithProgress(root);
|
|
142
164
|
const previousScore = Number(previousBaseline.score ?? current.score);
|
|
143
165
|
console.log((0, reporters_1.formatBaselineCompare)(previousScore, current.score));
|
|
144
166
|
return;
|
|
@@ -147,10 +169,10 @@ program
|
|
|
147
169
|
process.exitCode = 1;
|
|
148
170
|
});
|
|
149
171
|
program
|
|
150
|
-
.command('fix')
|
|
172
|
+
.command('fix [path]')
|
|
151
173
|
.description('Preview findings marked as safe to fix')
|
|
152
|
-
.action(
|
|
153
|
-
const summary =
|
|
174
|
+
.action((target) => {
|
|
175
|
+
const summary = scanWithProgress(resolveTarget(target));
|
|
154
176
|
const safeFixes = summary.findings.filter((finding) => finding.fixAvailable);
|
|
155
177
|
console.log('Safe fix preview');
|
|
156
178
|
if (safeFixes.length === 0) {
|
package/dist/cliLogic.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.filterFindings = filterFindings;
|
|
4
4
|
exports.isKnownSeverity = isKnownSeverity;
|
|
5
|
+
exports.validateGates = validateGates;
|
|
5
6
|
exports.determineExitFailure = determineExitFailure;
|
|
6
7
|
const SEVERITY_RANK = {
|
|
7
8
|
INFO: 0,
|
|
@@ -10,9 +11,25 @@ const SEVERITY_RANK = {
|
|
|
10
11
|
HIGH: 3,
|
|
11
12
|
CRITICAL: 4
|
|
12
13
|
};
|
|
14
|
+
/** `dead-code`, `Dead Code`, `dead_code` and `deadCode` all name the same category. */
|
|
15
|
+
function normalizeCategory(value) {
|
|
16
|
+
return value.toLowerCase().replace(/[^a-z]/g, '');
|
|
17
|
+
}
|
|
18
|
+
const CATEGORY_ALIASES = {
|
|
19
|
+
typesafety: 'typescript',
|
|
20
|
+
types: 'typescript',
|
|
21
|
+
redundancy: 'redundantlogic',
|
|
22
|
+
redundant: 'redundantlogic',
|
|
23
|
+
deps: 'dependencies',
|
|
24
|
+
dead: 'deadcode',
|
|
25
|
+
errors: 'errorhandling',
|
|
26
|
+
arch: 'architecture'
|
|
27
|
+
};
|
|
13
28
|
function filterFindings(findings, filters) {
|
|
29
|
+
const category = filters.category ? normalizeCategory(filters.category) : undefined;
|
|
30
|
+
const wantedCategory = category ? (CATEGORY_ALIASES[category] ?? category) : undefined;
|
|
14
31
|
return findings.filter((finding) => {
|
|
15
|
-
if (
|
|
32
|
+
if (wantedCategory && normalizeCategory(finding.category) !== wantedCategory)
|
|
16
33
|
return false;
|
|
17
34
|
if (filters.severity && finding.severity.toLowerCase() !== filters.severity.toLowerCase())
|
|
18
35
|
return false;
|
|
@@ -22,13 +39,31 @@ function filterFindings(findings, filters) {
|
|
|
22
39
|
function isKnownSeverity(value) {
|
|
23
40
|
return Object.prototype.hasOwnProperty.call(SEVERITY_RANK, value.toUpperCase());
|
|
24
41
|
}
|
|
42
|
+
/** Returns an error message for invalid gate options, or null when they are usable. */
|
|
43
|
+
function validateGates(gates) {
|
|
44
|
+
if (gates.minScore !== undefined) {
|
|
45
|
+
const value = Number(gates.minScore);
|
|
46
|
+
if (gates.minScore.trim() === '' || !Number.isFinite(value) || value < 0 || value > 10) {
|
|
47
|
+
return `--min-score must be a number between 0 and 10 (got "${gates.minScore}")`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (gates.failOn !== undefined && !isKnownSeverity(gates.failOn)) {
|
|
51
|
+
return `--fail-on must be one of ${Object.keys(SEVERITY_RANK).join(', ').toLowerCase()} (got "${gates.failOn}")`;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
25
55
|
function determineExitFailure(summary, gates) {
|
|
26
|
-
if (
|
|
56
|
+
if (validateGates(gates))
|
|
27
57
|
return true;
|
|
58
|
+
if (gates.minScore !== undefined) {
|
|
59
|
+
// A score computed from zero analyzed files is meaningless; never let it pass a quality gate.
|
|
60
|
+
if (summary.coverage && summary.coverage.analyzedFiles === 0)
|
|
61
|
+
return true;
|
|
62
|
+
if (summary.score < Number(gates.minScore))
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
28
65
|
if (gates.failOn !== undefined) {
|
|
29
66
|
const threshold = gates.failOn.toUpperCase();
|
|
30
|
-
if (!isKnownSeverity(gates.failOn))
|
|
31
|
-
return true;
|
|
32
67
|
if (summary.findings.some((finding) => SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[threshold]))
|
|
33
68
|
return true;
|
|
34
69
|
}
|