projscan 0.2.0 → 0.3.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/README.md CHANGED
@@ -60,16 +60,18 @@ npx projscan
60
60
  Run inside any repository:
61
61
 
62
62
  ```bash
63
- projscan # Full project analysis
64
- projscan doctor # Health check
65
- projscan hotspots # Rank files by risk (churn × complexity × issues × ownership)
66
- projscan file <path> # Drill into a file — purpose, risk, ownership, issues
67
- projscan fix # Auto-fix detected issues
68
- projscan ci # CI health gate (exits 1 on low score)
69
- projscan diff # Compare health + hotspot trends against a baseline
70
- projscan diagram # Architecture visualization
71
- projscan structure # Directory tree
72
- projscan mcp # Run as an MCP server for AI coding agents
63
+ projscan # Full project analysis
64
+ projscan doctor # Health check
65
+ projscan hotspots # Rank files by risk (churn × complexity × issues × ownership)
66
+ projscan file <path> # Drill into a file — purpose, risk, ownership, issues
67
+ projscan fix # Auto-fix detected issues
68
+ projscan ci # CI health gate (exits 1 on low score)
69
+ projscan ci --changed-only # Gate only on this PR's diff
70
+ projscan ci --format sarif # SARIF 2.1.0 for GitHub Code Scanning
71
+ projscan diff # Compare health + hotspot trends against a baseline
72
+ projscan diagram # Architecture visualization
73
+ projscan structure # Directory tree
74
+ projscan mcp # Run as an MCP server for AI coding agents
73
75
  ```
74
76
 
75
77
  <img src="docs/npx%20projscan%20--help.png" alt="npx projscan --help" width="700">
@@ -85,7 +87,7 @@ For a comprehensive walkthrough, see the **[Full Guide](docs/GUIDE.md)**.
85
87
  | `projscan hotspots` | Rank files by risk — churn × complexity × issues × ownership |
86
88
  | `projscan file <path>` | Drill into a file — purpose, risk, ownership, related issues |
87
89
  | `projscan fix` | Auto-fix issues (ESLint, Prettier, Vitest, .editorconfig) |
88
- | `projscan ci` | CI pipeline health gate — exits 1 if score below threshold |
90
+ | `projscan ci` | CI health gate — SARIF output, `--changed-only` PR-diff mode, exits 1 if score below threshold |
89
91
  | `projscan diff` | Compare current health **and hotspot trends** against a baseline |
90
92
  | `projscan explain <file>` | Explain a file's purpose, imports, exports, and issues |
91
93
  | `projscan diagram` | ASCII architecture diagram of your project |
@@ -139,15 +141,19 @@ All commands support `--format` for different output targets:
139
141
  ```bash
140
142
  projscan analyze --format json # Machine-readable JSON
141
143
  projscan doctor --format markdown # Markdown for docs/PRs
144
+ projscan ci --format sarif # SARIF 2.1.0 for GitHub Code Scanning
142
145
  ```
143
146
 
144
- Formats: `console` (default), `json`, `markdown`
147
+ Formats: `console` (default), `json`, `markdown`, `sarif`
145
148
 
146
149
  ### Options
147
150
 
148
151
  | Flag | Description |
149
152
  |------|-------------|
150
- | `--format <type>` | Output format: console, json, markdown |
153
+ | `--format <type>` | Output format: console, json, markdown, sarif |
154
+ | `--config <path>` | Path to a `.projscanrc` config file |
155
+ | `--changed-only` | Scope to files changed vs base ref (ci/analyze/doctor) |
156
+ | `--base-ref <ref>` | Git base ref for `--changed-only` (default: origin/main) |
151
157
  | `--verbose` | Enable debug output |
152
158
  | `--quiet` | Suppress non-essential output |
153
159
  | `-V, --version` | Show version |
@@ -204,21 +210,79 @@ This outputs a [shields.io](https://shields.io) badge URL and markdown snippet y
204
210
  Use `projscan ci` to gate your pipelines:
205
211
 
206
212
  ```bash
207
- projscan ci --min-score 70 # Exits 1 if score < 70
208
- projscan ci --min-score 80 --format json # JSON output for parsing
213
+ projscan ci --min-score 70 # Exits 1 if score < 70
214
+ projscan ci --min-score 80 --format json # JSON output for parsing
215
+ projscan ci --changed-only # Gate only on this PR's diff
216
+ projscan ci --format sarif > projscan.sarif # SARIF for Code Scanning
209
217
  ```
210
218
 
211
219
  <img src="docs/npx%20projscan%20ci%20--min-score%2070.png" alt="npx projscan ci --min-score 70" width="700">
212
220
 
213
- ### GitHub Actions
221
+ ### GitHub Action (recommended)
222
+
223
+ projscan ships a first-party GitHub Action that installs, runs, and uploads SARIF to **GitHub Code Scanning** in one step:
224
+
225
+ ```yaml
226
+ # .github/workflows/projscan.yml
227
+ name: ProjScan
228
+ on:
229
+ push: { branches: [main] }
230
+ pull_request: { branches: [main] }
231
+
232
+ permissions:
233
+ contents: read
234
+ security-events: write # required for SARIF upload
235
+
236
+ jobs:
237
+ scan:
238
+ runs-on: ubuntu-latest
239
+ steps:
240
+ - uses: actions/checkout@v4
241
+ with: { fetch-depth: 0 } # needed for --changed-only
242
+ - uses: actions/setup-node@v4
243
+ with: { node-version: 20 }
244
+ - uses: abhiyoheswaran1/projscan@v0.3.0
245
+ with:
246
+ min-score: '70'
247
+ changed-only: 'true'
248
+ ```
214
249
 
215
- Copy the included workflow template to your project:
250
+ Inputs: `min-score`, `changed-only`, `base-ref`, `config`, `sarif-file`, `upload-sarif`, `working-directory`, `version`. Outputs: `score`, `grade`.
216
251
 
217
- ```bash
218
- cp .github/projscan-ci.yml .github/workflows/projscan.yml
252
+ Findings appear in the **Security → Code scanning** tab, annotated on files and lines. PRs get inline annotations on changed lines.
253
+
254
+ ### Plain workflow (no SARIF upload)
255
+
256
+ If you'd rather not upload SARIF, [`.github/projscan-ci.yml`](.github/projscan-ci.yml) is a drop-in workflow that runs projscan and posts a markdown health report as a PR comment.
257
+
258
+ ## Configuration (`.projscanrc`)
259
+
260
+ Drop a `.projscanrc.json` at your repo root to set defaults — CLI flags always win over config. A `"projscan"` key in `package.json` and plain `.projscanrc` are also supported.
261
+
262
+ ```json
263
+ {
264
+ "minScore": 80,
265
+ "baseRef": "origin/main",
266
+ "ignore": ["**/fixtures/**", "**/generated/**"],
267
+ "disableRules": ["missing-editorconfig", "large-*"],
268
+ "severityOverrides": {
269
+ "missing-prettier": "info"
270
+ },
271
+ "hotspots": {
272
+ "limit": 20,
273
+ "since": "6 months ago"
274
+ }
275
+ }
219
276
  ```
220
277
 
221
- This runs health checks on every push/PR and posts a markdown health report as a PR comment. See [`.github/projscan-ci.yml`](.github/projscan-ci.yml) for the full workflow.
278
+ Fields:
279
+
280
+ - `minScore` — default `ci` threshold (0–100)
281
+ - `baseRef` — default base ref for `--changed-only`
282
+ - `ignore` — extra glob patterns added to the built-in ignore list
283
+ - `disableRules` — silence rules by id; supports wildcard `prefix-*`
284
+ - `severityOverrides` — remap a rule's severity (`info` / `warning` / `error`)
285
+ - `hotspots.limit` / `hotspots.since` — defaults for the `hotspots` command
222
286
 
223
287
  ## Tracking Health Over Time
224
288
 
@@ -24,6 +24,7 @@ export async function check(rootPath, files) {
24
24
  severity: 'warning',
25
25
  category: 'architecture',
26
26
  fixAvailable: false,
27
+ locations: [{ file: dir }],
27
28
  });
28
29
  }
29
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"architectureCheck.js","sourceRoot":"","sources":["../../src/analyzers/architectureCheck.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"architectureCheck.js","sourceRoot":"","sources":["../../src/analyzers/architectureCheck.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAC/B,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAE3D,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,QAAgB,EAAE,KAAkB;IAC9D,MAAM,MAAM,GAAY,EAAE,CAAC;IAE3B,sCAAsC;IACtC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;gBAC3B,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,aAAa,EAAE,CAAC;QACzC,IAAI,KAAK,GAAG,mBAAmB,EAAE,CAAC;YAChC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,CAAC,IAAI,CAAC;gBACV,EAAE,EAAE,SAAS,OAAO,MAAM;gBAC1B,KAAK,EAAE,SAAS,OAAO,gBAAgB,KAAK,SAAS;gBACrD,WAAW,EAAE,OAAO,GAAG,wBAAwB,KAAK,kFAAkF;gBACtI,QAAQ,EAAE,SAAS;gBACnB,QAAQ,EAAE,cAAc;gBACxB,YAAY,EAAE,KAAK;gBACnB,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;aAC3B,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,kDAAkD;IAClD,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QACpC,MAAM,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CACpC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CACnF,CAAC;IAEF,IAAI,YAAY,IAAI,CAAC,YAAY,EAAE,CAAC;QAClC,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CAChC,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,KAAK,GAAG,CAAC;YACrC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CACvD,CAAC;QACF,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC;gBACV,EAAE,EAAE,eAAe;gBACnB,KAAK,EAAE,kCAAkC;gBACzC,WAAW,EACT,+HAA+H;gBACjI,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,cAAc;gBACxB,YAAY,EAAE,KAAK;aACpB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,kCAAkC;IAClC,MAAM,eAAe,GAAG,KAAK,CAAC,IAAI,CAChC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,eAAe,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,KAAK,GAAG,CAAC,CAClG,CAAC;IACF,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,sBAAsB;YAC1B,KAAK,EAAE,uBAAuB;YAC9B,WAAW,EACT,6GAA6G;YAC/G,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,cAAc;YACxB,YAAY,EAAE,IAAI;YAClB,KAAK,EAAE,kBAAkB;SAC1B,CAAC,CAAC;IACL,CAAC;IAED,oCAAoC;IACpC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC;QACzD,OAAO,CACL,CAAC,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,YAAY,CAAC;YACpE,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,KAAK,GAAG,CAAC,CACtC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,gBAAgB;YACpB,KAAK,EAAE,gBAAgB;YACvB,WAAW,EACT,uFAAuF;YACzF,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,cAAc;YACxB,YAAY,EAAE,KAAK;SACpB,CAAC,CAAC;IACL,CAAC;SAAM,IAAI,UAAU,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,cAAc;YAClB,KAAK,EAAE,wBAAwB;YAC/B,WAAW,EACT,4HAA4H;YAC9H,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,cAAc;YACxB,YAAY,EAAE,KAAK;SACpB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -38,6 +38,7 @@ export async function check(rootPath, files) {
38
38
  severity: 'warning',
39
39
  category: 'security',
40
40
  fixAvailable: false,
41
+ locations: [{ file: file.relativePath, line: 1 }],
41
42
  });
42
43
  }
43
44
  }
@@ -55,6 +56,7 @@ export async function check(rootPath, files) {
55
56
  severity: 'error',
56
57
  category: 'security',
57
58
  fixAvailable: false,
59
+ locations: [{ file: file.relativePath, line: 1 }],
58
60
  });
59
61
  }
60
62
  }
@@ -101,7 +103,9 @@ async function scanFileForSecrets(file) {
101
103
  try {
102
104
  const content = await fs.readFile(file.absolutePath, 'utf-8');
103
105
  for (const { name, pattern } of SECRET_PATTERNS) {
104
- if (pattern.test(content)) {
106
+ const match = pattern.exec(content);
107
+ if (match) {
108
+ const line = lineNumberFor(content, match.index);
105
109
  return {
106
110
  id: 'hardcoded-secret',
107
111
  title: `Potential ${name} detected in ${file.relativePath}`,
@@ -109,6 +113,7 @@ async function scanFileForSecrets(file) {
109
113
  severity: 'error',
110
114
  category: 'security',
111
115
  fixAvailable: false,
116
+ locations: [{ file: file.relativePath, line }],
112
117
  };
113
118
  }
114
119
  }
@@ -118,4 +123,14 @@ async function scanFileForSecrets(file) {
118
123
  }
119
124
  return null;
120
125
  }
126
+ function lineNumberFor(content, index) {
127
+ if (index <= 0)
128
+ return 1;
129
+ let line = 1;
130
+ for (let i = 0; i < index && i < content.length; i++) {
131
+ if (content.charCodeAt(i) === 10)
132
+ line++;
133
+ }
134
+ return line;
135
+ }
121
136
  //# sourceMappingURL=securityCheck.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"securityCheck.js","sourceRoot":"","sources":["../../src/analyzers/securityCheck.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"securityCheck.js","sourceRoot":"","sources":["../../src/analyzers/securityCheck.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -21,29 +21,67 @@ import { setLogLevel } from '../utils/logger.js';
21
21
  import { calculateScore, badgeUrl, badgeMarkdown } from '../utils/scoreCalculator.js';
22
22
  import { showBanner, showCompactBanner, showHelp } from '../utils/banner.js';
23
23
  import { saveBaseline, loadBaseline, computeDiff } from '../utils/baseline.js';
24
+ import { loadConfig, applyConfigToIssues } from '../utils/config.js';
25
+ import { getChangedFiles } from '../utils/changedFiles.js';
24
26
  import { runMcpServer } from '../mcp/server.js';
25
27
  import { reportAnalysis, reportHealth, reportCi, reportDiff, reportDetectedIssues, reportExplanation, reportDiagram, reportStructure, reportDependencies, reportHotspots, reportFileInspection, } from '../reporters/consoleReporter.js';
26
28
  import { reportAnalysisJson, reportHealthJson, reportCiJson, reportDiffJson, reportExplanationJson, reportDiagramJson, reportStructureJson, reportDependenciesJson, reportHotspotsJson, reportFileJson, } from '../reporters/jsonReporter.js';
27
29
  import { reportAnalysisMarkdown, reportHealthMarkdown, reportCiMarkdown, reportDiffMarkdown, reportExplanationMarkdown, reportDiagramMarkdown, reportStructureMarkdown, reportDependenciesMarkdown, reportHotspotsMarkdown, reportFileMarkdown, } from '../reporters/markdownReporter.js';
30
+ import { reportAnalysisSarif, reportHealthSarif, reportCiSarif, } from '../reporters/sarifReporter.js';
28
31
  // ── CLI Setup ─────────────────────────────────────────────
29
32
  const program = new Command();
30
33
  program
31
34
  .name('projscan')
32
35
  .description('Instant codebase insights — doctor, x-ray, and architecture map for any repository')
33
36
  .version(pkg.version)
34
- .option('--format <type>', 'output format: console, json, markdown', 'console')
37
+ .option('--format <type>', 'output format: console, json, markdown, sarif', 'console')
38
+ .option('--config <path>', 'path to .projscanrc config file')
35
39
  .option('--verbose', 'enable verbose output')
36
40
  .option('--quiet', 'suppress non-essential output');
37
41
  function getFormat() {
38
42
  const opts = program.opts();
39
43
  const f = opts.format;
40
- if (f === 'json' || f === 'markdown')
44
+ if (f === 'json' || f === 'markdown' || f === 'sarif')
41
45
  return f;
42
46
  return 'console';
43
47
  }
44
48
  function getRootPath() {
45
49
  return process.cwd();
46
50
  }
51
+ async function loadProjectConfig() {
52
+ const opts = program.opts();
53
+ const explicit = typeof opts.config === 'string' ? opts.config : undefined;
54
+ try {
55
+ const { config, source } = await loadConfig(getRootPath(), explicit);
56
+ if (source && !opts.quiet && getFormat() === 'console') {
57
+ console.error(chalk.dim(` [config: ${path.relative(getRootPath(), source) || source}]`));
58
+ }
59
+ return config;
60
+ }
61
+ catch (err) {
62
+ const msg = err instanceof Error ? err.message : String(err);
63
+ console.error(chalk.red(` Config error: ${msg}`));
64
+ process.exit(1);
65
+ }
66
+ }
67
+ async function filterIssuesByChangedFiles(issues, rootPath, baseRef) {
68
+ const result = await getChangedFiles(rootPath, baseRef);
69
+ if (!result.available) {
70
+ if (getFormat() === 'console' && !program.opts().quiet) {
71
+ console.error(chalk.yellow(` [--changed-only: ${result.reason ?? 'unavailable'} — reporting all issues]`));
72
+ }
73
+ return issues;
74
+ }
75
+ if (getFormat() === 'console' && !program.opts().quiet) {
76
+ console.error(chalk.dim(` [--changed-only: base=${result.baseRef}, ${result.files.length} file(s)]`));
77
+ }
78
+ const set = new Set(result.files);
79
+ return issues.filter((issue) => {
80
+ if (!issue.locations || issue.locations.length === 0)
81
+ return false;
82
+ return issue.locations.some((loc) => set.has(loc.file));
83
+ });
84
+ }
47
85
  function setupLogLevel() {
48
86
  const opts = program.opts();
49
87
  if (opts.verbose)
@@ -77,14 +115,17 @@ function maybeCompactBanner() {
77
115
  program
78
116
  .command('analyze', { isDefault: true })
79
117
  .description('Analyze repository and show project report')
80
- .action(async () => {
118
+ .option('--changed-only', 'only report issues on files changed vs base ref')
119
+ .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
120
+ .action(async (cmdOpts) => {
81
121
  setupLogLevel();
82
122
  maybeBanner();
83
123
  const rootPath = getRootPath();
84
124
  const format = getFormat();
125
+ const config = await loadProjectConfig();
85
126
  const spinner = format === 'console' ? ora('Scanning repository...').start() : null;
86
127
  try {
87
- const scan = await scanRepository(rootPath);
128
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
88
129
  if (spinner)
89
130
  spinner.text = 'Detecting languages...';
90
131
  const languages = detectLanguages(scan.files);
@@ -96,7 +137,11 @@ program
96
137
  const dependencies = await analyzeDependencies(rootPath);
97
138
  if (spinner)
98
139
  spinner.text = 'Checking for issues...';
99
- const issues = await collectIssues(rootPath, scan.files);
140
+ let issues = await collectIssues(rootPath, scan.files);
141
+ issues = applyConfigToIssues(issues, config);
142
+ if (cmdOpts.changedOnly) {
143
+ issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
144
+ }
100
145
  if (spinner)
101
146
  spinner.stop();
102
147
  const report = {
@@ -116,6 +161,9 @@ program
116
161
  case 'markdown':
117
162
  reportAnalysisMarkdown(report);
118
163
  break;
164
+ case 'sarif':
165
+ reportAnalysisSarif(issues, pkg.version);
166
+ break;
119
167
  default:
120
168
  reportAnalysis(report);
121
169
  }
@@ -131,15 +179,22 @@ program
131
179
  program
132
180
  .command('doctor')
133
181
  .description('Evaluate project health and detect issues')
134
- .action(async () => {
182
+ .option('--changed-only', 'only report issues on files changed vs base ref')
183
+ .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
184
+ .action(async (cmdOpts) => {
135
185
  setupLogLevel();
136
186
  maybeCompactBanner();
137
187
  const rootPath = getRootPath();
138
188
  const format = getFormat();
189
+ const config = await loadProjectConfig();
139
190
  const spinner = format === 'console' ? ora('Running health checks...').start() : null;
140
191
  try {
141
- const scan = await scanRepository(rootPath);
142
- const issues = await collectIssues(rootPath, scan.files);
192
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
193
+ let issues = await collectIssues(rootPath, scan.files);
194
+ issues = applyConfigToIssues(issues, config);
195
+ if (cmdOpts.changedOnly) {
196
+ issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
197
+ }
143
198
  if (spinner)
144
199
  spinner.stop();
145
200
  switch (format) {
@@ -149,6 +204,9 @@ program
149
204
  case 'markdown':
150
205
  reportHealthMarkdown(issues);
151
206
  break;
207
+ case 'sarif':
208
+ reportHealthSarif(issues, pkg.version);
209
+ break;
152
210
  default:
153
211
  reportHealth(issues, scan.scanDurationMs);
154
212
  }
@@ -164,16 +222,24 @@ program
164
222
  program
165
223
  .command('ci')
166
224
  .description('Run health check for CI pipelines (exits 1 if score below threshold)')
167
- .option('--min-score <score>', 'minimum passing score (0-100)', '70')
225
+ .option('--min-score <score>', 'minimum passing score (0-100)')
226
+ .option('--changed-only', 'gate only on issues in files changed vs base ref')
227
+ .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
168
228
  .action(async (cmdOpts) => {
169
229
  setupLogLevel();
170
230
  maybeCompactBanner();
171
231
  const rootPath = getRootPath();
172
232
  const format = getFormat();
233
+ const config = await loadProjectConfig();
173
234
  try {
174
- const scan = await scanRepository(rootPath);
175
- const issues = await collectIssues(rootPath, scan.files);
176
- const threshold = Math.max(0, Math.min(100, parseInt(cmdOpts.minScore, 10) || 70));
235
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
236
+ let issues = await collectIssues(rootPath, scan.files);
237
+ issues = applyConfigToIssues(issues, config);
238
+ if (cmdOpts.changedOnly) {
239
+ issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
240
+ }
241
+ const rawThreshold = cmdOpts.minScore ?? config.minScore ?? 70;
242
+ const threshold = Math.max(0, Math.min(100, typeof rawThreshold === 'string' ? parseInt(rawThreshold, 10) || 70 : rawThreshold));
177
243
  const { score } = calculateScore(issues);
178
244
  switch (format) {
179
245
  case 'json':
@@ -182,6 +248,9 @@ program
182
248
  case 'markdown':
183
249
  reportCiMarkdown(issues, threshold);
184
250
  break;
251
+ case 'sarif':
252
+ reportCiSarif(issues, pkg.version);
253
+ break;
185
254
  default:
186
255
  reportCi(issues, threshold);
187
256
  }
@@ -205,9 +274,11 @@ program
205
274
  maybeCompactBanner();
206
275
  const rootPath = getRootPath();
207
276
  const format = getFormat();
277
+ const config = await loadProjectConfig();
208
278
  try {
209
- const scan = await scanRepository(rootPath);
210
- const issues = await collectIssues(rootPath, scan.files);
279
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
280
+ let issues = await collectIssues(rootPath, scan.files);
281
+ issues = applyConfigToIssues(issues, config);
211
282
  const hotspotReport = await analyzeHotspots(rootPath, scan.files, issues, { limit: 20 });
212
283
  if (cmdOpts.saveBaseline) {
213
284
  const filePath = await saveBaseline(rootPath, issues, hotspotReport);
@@ -259,9 +330,11 @@ program
259
330
  maybeCompactBanner();
260
331
  const rootPath = getRootPath();
261
332
  const spinner = ora('Detecting issues...').start();
333
+ const config = await loadProjectConfig();
262
334
  try {
263
- const scan = await scanRepository(rootPath);
264
- const issues = await collectIssues(rootPath, scan.files);
335
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
336
+ let issues = await collectIssues(rootPath, scan.files);
337
+ issues = applyConfigToIssues(issues, config);
265
338
  const fixes = getAllAvailableFixes(issues);
266
339
  spinner.stop();
267
340
  if (fixes.length === 0) {
@@ -387,9 +460,10 @@ program
387
460
  maybeCompactBanner();
388
461
  const rootPath = getRootPath();
389
462
  const format = getFormat();
463
+ const config = await loadProjectConfig();
390
464
  const spinner = format === 'console' ? ora('Analyzing architecture...').start() : null;
391
465
  try {
392
- const scan = await scanRepository(rootPath);
466
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
393
467
  const frameworks = await detectFrameworks(rootPath, scan.files);
394
468
  const layers = buildArchitectureLayers(scan.files, frameworks.frameworks.map((f) => f.name));
395
469
  if (spinner)
@@ -421,9 +495,10 @@ program
421
495
  maybeCompactBanner();
422
496
  const rootPath = getRootPath();
423
497
  const format = getFormat();
498
+ const config = await loadProjectConfig();
424
499
  const spinner = format === 'console' ? ora('Scanning...').start() : null;
425
500
  try {
426
- const scan = await scanRepository(rootPath);
501
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
427
502
  if (spinner)
428
503
  spinner.stop();
429
504
  switch (format) {
@@ -484,20 +559,24 @@ program
484
559
  program
485
560
  .command('hotspots')
486
561
  .description('Rank files by risk (git churn × complexity × open issues)')
487
- .option('--limit <n>', 'number of hotspots to show', '10')
488
- .option('--since <when>', 'git history window (e.g. "6 months ago", "2024-01-01")', '12 months ago')
562
+ .option('--limit <n>', 'number of hotspots to show')
563
+ .option('--since <when>', 'git history window (e.g. "6 months ago", "2024-01-01")')
489
564
  .action(async (cmdOpts) => {
490
565
  setupLogLevel();
491
566
  maybeCompactBanner();
492
567
  const rootPath = getRootPath();
493
568
  const format = getFormat();
569
+ const config = await loadProjectConfig();
494
570
  const spinner = format === 'console' ? ora('Analyzing hotspots...').start() : null;
495
571
  try {
496
- const scan = await scanRepository(rootPath);
497
- const issues = await collectIssues(rootPath, scan.files);
498
- const limit = Math.max(1, Math.min(100, parseInt(cmdOpts.limit, 10) || 10));
572
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
573
+ let issues = await collectIssues(rootPath, scan.files);
574
+ issues = applyConfigToIssues(issues, config);
575
+ const limitRaw = cmdOpts.limit ?? config.hotspots?.limit ?? 10;
576
+ const limit = Math.max(1, Math.min(100, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 10 : limitRaw));
577
+ const since = cmdOpts.since ?? config.hotspots?.since ?? '12 months ago';
499
578
  const report = await analyzeHotspots(rootPath, scan.files, issues, {
500
- since: cmdOpts.since,
579
+ since,
501
580
  limit,
502
581
  });
503
582
  if (spinner)
@@ -545,9 +624,11 @@ program
545
624
  maybeCompactBanner();
546
625
  const rootPath = getRootPath();
547
626
  const spinner = ora('Calculating health score...').start();
627
+ const config = await loadProjectConfig();
548
628
  try {
549
- const scan = await scanRepository(rootPath);
550
- const issues = await collectIssues(rootPath, scan.files);
629
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
630
+ let issues = await collectIssues(rootPath, scan.files);
631
+ issues = applyConfigToIssues(issues, config);
551
632
  const { score, grade } = calculateScore(issues);
552
633
  spinner.stop();
553
634
  const gradeColor = grade === 'A' || grade === 'B' ? chalk.green : grade === 'C' ? chalk.yellow : chalk.red;