npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

impact-scope

v1.0.0

Published

See the blast radius of your code changes. Import graph analysis + risk scoring for PRs.

Readme

💥 impact-scope

See the blast radius of your code changes

GitHub Stars License TypeScript Tests

Import graph analysis and risk scoring for code reviews and PRs.

Import Graph + Risk Scoring + CI Gates

Quick Start | Features


Why This Exists

Code review tools show you what changed, but not what those changes will break. impact-scope scans your TypeScript/JavaScript project, builds an import dependency graph, and shows exactly which files are transitively affected when you touch a file. It assigns a risk score from 0 to 100 based on impact depth, affected file count, test coverage gaps, and change size -- so reviewers can prioritize attention on the changes that actually matter.


Requirements

  • Node.js >= 18.0.0
  • Git installed and available in PATH
  • A TypeScript or JavaScript project with relative imports

Quick Start

As a CLI tool

# Install globally
npm install -g impact-scope

# Analyze the impact of your latest commit
impact-scope analyze

# Analyze changes against a specific base ref
impact-scope analyze --base main

# Check impact of changing a specific file
impact-scope check src/utils/math.ts

# View import graph statistics
impact-scope graph

As a library

npm install impact-scope
import {
  parseDiff,
  buildImportGraph,
  analyzeImpact,
  buildRiskReport,
  formatTerminal,
} from 'impact-scope';
import { execSync } from 'child_process';

// Get diff from git
const diffOutput = execSync('git diff HEAD~1', { encoding: 'utf-8' });

const graph = buildImportGraph('.');
const changedFiles = parseDiff(diffOutput);
const affected = analyzeImpact(changedFiles, graph, '.');
const report = buildRiskReport(changedFiles, affected);
console.log(formatTerminal(report));

CLI Commands

impact-scope analyze

Analyze the impact of code changes from a git diff.

| Option | Default | Description | |--------|---------|-------------| | --base <ref> | HEAD~1 | Base git ref for diff | | --threshold <n> | 50 | Risk score threshold for CI mode | | --format <type> | terminal | Output format: terminal, json, ci | | --root <path> | . | Project root directory |

impact-scope graph

Show import graph statistics (file count, edge count, most-imported files).

| Option | Default | Description | |--------|---------|-------------| | --root <path> | . | Project root directory |

impact-scope check <file>

Check the blast radius of changing a specific file without needing a diff.

| Option | Default | Description | |--------|---------|-------------| | --format <type> | terminal | Output format: terminal, json | | --root <path> | . | Project root directory |


Example Output

Terminal format

============================================================
  IMPACT SCOPE ANALYSIS
============================================================

  Risk Score: 42/100 (MEDIUM)
  [#########################---------------]

  Changed files:  1
  Affected files: 3
  Untested:       1

  Changed Files:
    +2 / -1  src/utils/math.ts

  Affected Files (by depth):
      depth 1: src/components/calculator.ts (add, multiply) [tested]
      depth 1: src/utils/index.ts (*) [tested]
      depth 2: src/app.ts [UNTESTED]

  Untested Affected Files:
    ! src/app.ts

============================================================

JSON format

{
  "score": 42,
  "level": "medium",
  "changedFiles": [
    { "path": "src/utils/math.ts", "additions": 2, "deletions": 1, "hunks": [{ "startLine": 1, "endLine": 6 }] }
  ],
  "affectedFiles": [
    { "filePath": "src/components/calculator.ts", "depth": 1, "affectedSymbols": ["add", "multiply"], "hasTests": true },
    { "filePath": "src/utils/index.ts", "depth": 1, "affectedSymbols": ["*"], "hasTests": true },
    { "filePath": "src/app.ts", "depth": 2, "affectedSymbols": [], "hasTests": false }
  ],
  "untestedAffected": ["src/app.ts"],
  "summary": "Risk Score: 42/100 (MEDIUM) | 1 changed file(s), 3 affected file(s), 1 untested, 3 line(s) changed",
  "details": [
    "--- Changed Files ---",
    "  src/utils/math.ts (+2/-1)",
    "--- Affected Files ---",
    "  depth=1 src/components/calculator.ts [tested]",
    "  depth=1 src/utils/index.ts [tested]",
    "    depth=2 src/app.ts [UNTESTED]",
    "--- Untested Affected Files ---",
    "  ! src/app.ts"
  ]
}

CI format

impact-scope: PASS
score: 42/100 (medium)
threshold: 50
changed: 1 files
affected: 3 files
untested: 1 files

How It Works

git diff --> parseDiff --> changedFiles
                              |
project root --> buildImportGraph --> importGraph
                                          |
changedFiles + importGraph --> analyzeImpact --> affectedFiles
                                                     |
changedFiles + affectedFiles --> buildRiskReport --> RiskReport
                                                        |
                                              formatTerminal / formatJSON / formatCI

Risk Scoring

The risk score (0-100) is computed from four weighted factors:

| Factor | Weight | Description | |--------|--------|-------------| | Affected file count | 30% | Number of transitively affected files (caps at 20) | | Max depth | 20% | Deepest level in the impact chain (caps at 5) | | Untested ratio | 30% | Fraction of affected files lacking test coverage | | Change size | 20% | Total lines added + deleted (caps at 500) |

Risk levels: low (0-25), medium (26-50), high (51-75), critical (76-100).

How Risk Scoring Works

The risk score quantifies how dangerous a set of code changes is to your project. Here is exactly how each factor is computed:

1. Affected File Count (30% weight) impact-scope walks the reverse import graph using BFS. Starting from each changed file, it finds every file that transitively depends on it. The raw count is divided by a cap of 20 files. If a utility module is imported by 15 files, changing it yields 15/20 = 0.75 for this factor.

2. Max Depth (20% weight) Depth measures how far the impact ripples through the dependency chain. A change affecting only direct importers has depth 1. If those importers are themselves imported, depth grows. The cap is 5 levels. Depth 3 yields 3/5 = 0.6.

3. Untested Ratio (30% weight) For each affected file, impact-scope checks whether a corresponding test file exists (e.g., src/foo.ts -> tests/foo.test.ts) or whether any test file in the import graph imports it. The ratio of untested affected files drives this factor. If 4 of 10 affected files lack tests: 4/10 = 0.4.

4. Change Size (20% weight) Total lines added + deleted across all changed files, capped at 500. A 60-line change yields 60/500 = 0.12.

Final score: (factor1 * 0.3 + factor2 * 0.2 + factor3 * 0.3 + factor4 * 0.2) * 100, rounded and clamped to [0, 100].

You can override the default weights by passing a custom ScoringWeights object to computeRiskScore() or buildRiskReport().

Understanding the Impact Graph

The import graph is a directed graph where each node is a source file and each edge represents an import statement:

math.ts  <--  calculator.ts  <--  app.ts
   |               ^
   v               |
format.ts --> index.ts

How the graph is built:

  1. collectSourceFiles() recursively scans the project root for .ts, .tsx, .js, and .jsx files, skipping node_modules, dist, and .git directories.
  2. extractImports() reads each file and uses regex patterns to extract five types of import/export statements:
    • import { x } from './path' (named imports)
    • import x from './path' (default imports)
    • import * as x from './path' (namespace imports)
    • export { x } from './path' (re-exports)
    • export * from './path' (star re-exports)
  3. resolveImportPath() resolves relative import specifiers to actual file paths, trying each extension and index file resolution.

How impact is analyzed:

  1. getReverseGraph() inverts the edges so each file maps to its importers.
  2. analyzeImpact() runs BFS from each changed file through the reverse graph, recording the depth at which each affected file is reached.
  3. Files at depth 0 (the changed files themselves) are excluded from the results.

Limitations:

  • Only relative imports are resolved. Path aliases (@/utils/math) and bare module specifiers (lodash) are not tracked.
  • Dynamic import() expressions are not detected.
  • TypeScript paths configuration in tsconfig.json is not read.

GitHub Actions Integration

Add this workflow to .github/workflows/impact-scope.yml to run impact analysis on every PR:

name: Impact Scope Analysis
on:
  pull_request:
    branches: [main]
jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - name: Run impact analysis
        run: npx impact-scope analyze --base origin/main --format ci --threshold 50

Integration Patterns

Danger.js

// dangerfile.ts
import { execSync } from 'child_process';

const output = execSync(
  'npx impact-scope analyze --base origin/main --format json',
  { encoding: 'utf-8' }
);
const report = JSON.parse(output);

if (report.score > 50) {
  warn(`Impact scope risk score: ${report.score}/100 (${report.level})`);
}
if (report.untestedAffected.length > 0) {
  const files = report.untestedAffected.map((f: string) => `- \`${f}\``).join('\n');
  warn(`Untested affected files:\n${files}`);
}
message(`**Impact Analysis:** ${report.summary}`);

GitHub PR Comment via Actions

- name: Run impact analysis
  id: impact
  run: |
    OUTPUT=$(npx impact-scope analyze --base origin/main --format json)
    echo "report<<EOF" >> $GITHUB_OUTPUT
    echo "$OUTPUT" >> $GITHUB_OUTPUT
    echo "EOF" >> $GITHUB_OUTPUT
  continue-on-error: true

- name: Comment PR
  uses: actions/github-script@v7
  with:
    script: |
      const report = JSON.parse(`${{ steps.impact.outputs.report }}`);
      const body = [
        '## Impact Scope Analysis',
        `**Risk Score:** ${report.score}/100 (${report.level.toUpperCase()})`,
        `**Changed:** ${report.changedFiles.length} files | **Affected:** ${report.affectedFiles.length} files | **Untested:** ${report.untestedAffected.length}`,
        '',
        report.untestedAffected.length > 0
          ? '### Untested Affected Files\n' + report.untestedAffected.map(f => `- \`${f}\``).join('\n')
          : '',
      ].filter(Boolean).join('\n');
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body
      });

Programmatic API

import {
  parseDiff,
  buildImportGraph,
  analyzeImpact,
  buildRiskReport,
  formatTerminal,
  formatJSON,
  formatCI,
} from 'impact-scope';
import type { RiskReport } from 'impact-scope';

function analyzeChanges(diffOutput: string, projectRoot: string): RiskReport {
  const changed = parseDiff(diffOutput);
  const graph = buildImportGraph(projectRoot);
  const affected = analyzeImpact(changed, graph, projectRoot);
  return buildRiskReport(changed, affected);
}

// Render the report
const report = analyzeChanges(diffOutput, '.');
console.log(formatTerminal(report));            // colored terminal output
console.log(formatJSON(report));                // JSON string
const { output, exitCode } = formatCI(report, 50); // CI with threshold

API Reference

| Function | Description | |----------|-------------| | parseDiff(diffOutput) | Parse unified diff string into ChangedFile[] | | buildImportGraph(rootDir) | Scan project and build ImportGraph | | analyzeImpact(changed, graph, root) | Find transitively affected files via BFS | | analyzeFileImpact(filePath, graph, root) | Shorthand for single-file impact check | | computeRiskScore(changed, affected, weights?) | Compute 0-100 risk score | | getRiskLevel(score) | Map score to 'low'/'medium'/'high'/'critical' | | buildRiskReport(changed, affected, weights?) | Build complete RiskReport | | formatTerminal(report) | Render colored terminal output | | formatJSON(report) | Render as formatted JSON string | | formatCI(report, threshold) | Render CI output with { output, exitCode } | | checkTestCoverage(file, root, graph) | Check if a file has test coverage | | isTestFile(filePath) | Check if a path looks like a test file | | findTestFiles(graph) | Find all test files in the graph | | getReverseGraph(graph) | Get reverse dependency map |

Error classes: ImpactScopeError, DiffParseError, GraphBuildError, AnalysisError

Types: ChangedFile, ImportEdge, ImportGraph, ImpactNode, RiskReport, ScoringWeights


FAQ / Troubleshooting

"Error: Command failed: git diff HEAD~1"

No previous commit to diff against. Use --base to specify a valid ref:

impact-scope analyze --base main

"File not found in project"

The check command only works with files in the import graph (.ts, .tsx, .js, .jsx). Ensure:

  • The file path is relative to the project root
  • The file has a supported extension
  • Use --root if running from outside the project directory

No affected files are found

This can happen when:

  • The changed file is not imported by any other file (leaf/entry point)
  • The project uses non-relative imports (path aliases) which are not yet resolved
  • The changed file is not a .ts/.tsx/.js/.jsx file

License

MIT