New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

impact-scope

Package Overview
Dependencies
Maintainers
1
Versions
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

impact-scope

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

latest
npmnpm
Version
1.0.0
Version published
Maintainers
1
Created
Source

💥 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.

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

impact-scope graph

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

OptionDefaultDescription
--root <path>.Project root directory

impact-scope check <file>

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

OptionDefaultDescription
--format <type>terminalOutput 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:

FactorWeightDescription
Affected file count30%Number of transitively affected files (caps at 20)
Max depth20%Deepest level in the impact chain (caps at 5)
Untested ratio30%Fraction of affected files lacking test coverage
Change size20%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:

  • collectSourceFiles() recursively scans the project root for .ts, .tsx, .js, and .jsx files, skipping node_modules, dist, and .git directories.
  • 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)
  • resolveImportPath() resolves relative import specifiers to actual file paths, trying each extension and index file resolution.

How impact is analyzed:

  • getReverseGraph() inverts the edges so each file maps to its importers.
  • analyzeImpact() runs BFS from each changed file through the reverse graph, recording the depth at which each affected file is reached.
  • 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

FunctionDescription
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

Keywords

typescript

FAQs

Package last updated on 28 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts