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 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
+ [![CI](https://github.com/Karan071/devkit-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Karan071/devkit-cli/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/devkit-quality.svg)](https://www.npmjs.com/package/devkit-quality)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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 the directory tree, apply configured include/exclude and ignore patterns, skip generated files, and select `.js`/`.jsx`/`.ts`/`.tsx`/`.mjs`/`.cjs` files.
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 (for the TypeScript compiler API and `fs`/`path` usage — any reasonably current LTS version works).
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)) and normalized against source LOC (in 500-line chunks), so a single low-confidence finding in a large repository barely moves the score. Category scores are combined into the overall score using fixed weights:
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 | 20% |
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
- | Architecture | 10% |
283
+ | Redundant logic | 5% |
284
+ | Architecture | 5% |
277
285
  | Hygiene | 5% |
278
286
 
279
- Security findings are scored and reported separately rather than folded into the overall score.
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) — colored, with a score bar, per-category breakdown, and code frames for the top finding per category.
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` and `.devkitignore` negation patterns (`!pattern`) are skipped; full Git ignore semantics are not implemented.
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
- - DevKit has no rule presets, React-specific or test-quality rules, configuration analysis, incremental cache, or monorepo-aware scoring.
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
- async function runWithSpinner(text, task) {
18
- const spinner = new terminal_1.Spinner(text);
19
- spinner.start();
20
- await (0, terminal_1.waitForNextTick)();
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
- return task();
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
- finally {
25
- spinner.stop();
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
- async function runScan(options) {
38
- const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
39
- const filteredFindings = (0, cliLogic_1.filterFindings)(summary.findings, options);
40
- const filteredSummary = filteredFindings.length === summary.findings.length
41
- ? summary
42
- : {
43
- ...summary,
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
- if (options.failOn && !(0, cliLogic_1.isKnownSeverity)(options.failOn))
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 the current repository for code-quality issues')
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>', 'Filter by category')
66
- .option('--severity <level>', 'Filter by severity')
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 exists')
69
- .action(async (options) => {
70
- if (await runScan(options)) {
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 the current repository')
77
- .action(async () => {
78
- const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
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 for the current project')
84
- .action(async () => {
85
- const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
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
- for (const rule of (0, rules_1.listRules)()) {
93
- console.log(`${rule.id} - ${rule.title} (${rule.category})`);
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(async (action) => {
122
- const root = process.cwd();
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 = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(root));
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 = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(root));
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(async () => {
153
- const summary = await runWithSpinner('Scanning repository...', () => (0, scanner_1.scanRepository)(process.cwd()));
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 (filters.category && finding.category.toLowerCase() !== filters.category.toLowerCase())
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 (gates.minScore !== undefined && summary.score < Number(gates.minScore))
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
  }