Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/impact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Impact

# Comments on each pull request with what its changes can reach. This repository uses its own action.
on:
pull_request:
branches: [main]

permissions:
contents: read
pull-requests: write

jobs:
impact:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ All notable changes to [`codegraph-engine`](https://www.npmjs.com/package/codegr

## Unreleased

### Fixed
- **Diffs no longer inflate the blast radius.** An edit on a line that declares a local variable is a change to its function (it used to be attributed to the variable, which has no callers, so the function's blast radius came out empty), and a module-level edit such as an import line no longer counts every symbol in the file as changed. Symbols a diff adds in full are reported as new instead of as changes. The pull request comment now counts only callers outside the files the change edits, says plainly when a change only adds code, and gives a test command per ecosystem (`go test ./pkg`, `pytest`, `vitest`/`jest`).
- **Python function-local imports** (`def f(): from x import y`) are scoped to that function instead of replacing the file's import of the same name, and shadow a same-named function outside it. Removes one `resolved` error each in Flask and Rich in the Jedi check.

### Added
- **Go support.** `.go` files are indexed (CLI, web app, MCP, pull request comments): packages resolved through `go.mod`, methods attached to their types, struct embedding and promoted methods, interfaces, typed variables through constructors, results, ranges and fields, and `//go:build` for linux/amd64. Checked against the Go type checker on Cobra, Gin, Logrus and gorilla/mux: 98.5% of the calls it resolves are found, and every one of 7,550 edges is correct (first run, before any fix: 96.8% and 7,336 of 7,339). Details and limits in `docs/ORACLE-EVAL.md`.
- **Private repositories in the web app**: paste a GitHub personal access token on the home page. It stays in the browser tab, is sent only to `api.github.com`, and also raises GitHub's limit from 60 to 5,000 requests an hour.
- **`codegraph mcp`**: an MCP server (stdio) so AI agents can ask what a change touches: impact of a symbol or of the current diff, callers and callees, symbol search, an overview of the riskiest code.
- **A GitHub Action** (`uses: harsharajkumar-273/CodeGraph@main`) that comments on pull requests with the symbols, files and tests a change can reach, with an optional `fail-above` limit. `codegraph impact --format markdown` prints the same text.
- **A proper website around the app.** A new homepage with an animated pipeline (get the code, parse it, link it, label every edge, show the blast radius), a product preview, features, the accuracy results with their caveats, and the CLI commands. Analyzing now shows the same pipeline live, stage by stage, with real progress. Fonts are bundled (no third-party requests), the page works on phones, respects reduced-motion settings and passes an automated accessibility check.
- **The web app analyzes any repository, in the browser.** Paste a public GitHub repository (or `owner/repo@branch`, or
a `github.com` link) or drop a project folder; the parser (WebAssembly) runs in a Web Worker and nothing is uploaded.
Expand Down
61 changes: 49 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![npm downloads](https://img.shields.io/npm/dm/codegraph-engine.svg)](https://www.npmjs.com/package/codegraph-engine)
[![license](https://img.shields.io/npm/l/codegraph-engine.svg)](LICENSE)

**See what breaks before you change it.** An AST dependency, call-graph and blast-radius engine for TypeScript, JavaScript and Python repositories, with a web app that runs entirely in your browser.
**See what breaks before you change it.** An AST dependency, call-graph and blast-radius engine for TypeScript, JavaScript, Python and Go repositories, with a web app that runs entirely in your browser.

![CodeGraph's blast radius for url_for() in Flask: 40 symbols in 19 files, with the affected test files folded together](docs/screenshots/workspace.png)

Expand All @@ -23,21 +23,22 @@ Impact 3 symbols in 6 files (3 resolved · 0 probable · 0 ambiguous)
d3 resolved src/flask/app.py#Flask.__call__
```

Every edge is labelled **resolved / probable / ambiguous**, so you can see *how much* of a blast radius is proven vs. guessed, and filter accordingly. Checked against independent oracles (the TypeScript compiler, and Jedi for Python) on every call site of six open-source repositories, not a sample. The repositories were chosen before measuring, but fixes the checks prompted are included, so a fresh repository may score a little lower:
Every edge is labelled **resolved / probable / ambiguous**, so you can see *how much* of a blast radius is proven vs. guessed, and filter accordingly. Checked against independent oracles (the TypeScript compiler, Jedi for Python, and the Go type checker) on every call site of ten open-source repositories, not a sample. The repositories were chosen before measuring, but fixes the checks prompted are included, so a fresh repository may score a little lower:

| | `resolved` | `probable` | `ambiguous` |
|---|---|---|---|
| TypeScript: Hono, Immer, NestJS (vs. the TypeScript compiler) | 98% | 69% | 35% |
| Python: Requests, HTTPX, Rich (vs. Jedi) | 99.8% | 80% | 16% |
| Go: Cobra, Gin, Logrus, mux (vs. the Go type checker) | 100% (7,550 of 7,550) | none emitted | none emitted |

Trust `resolved`; treat `probable` as a lead to check and `ambiguous` as a list of name matches. See [`docs/ORACLE-EVAL.md`](docs/ORACLE-EVAL.md) for the numbers and their limits (no JavaScript-only repos yet, no adoption or independent-user evidence yet). The earlier LLM-labeled samples are in [`docs/PRECISION.md`](docs/PRECISION.md) and [`docs/EVALUATION.md`](docs/EVALUATION.md).

## Web app

Open the app, then paste `owner/repo` (or `owner/repo@branch`, or any `github.com` link), or drop a folder, some `.ts`/`.js`/`.py` files or a `.zip` (such as GitHub's "Download ZIP") from your machine. You land on a map of the repository's folders and a short list of the code that is riskiest to change. Pick any function, class or file to see its blast radius: what could break, how sure CodeGraph is about each link, and which tests to run. Share an analysis with `?repo=owner/repo` on the app's URL.

- **Public GitHub repositories, or anything on your own machine** (a folder, loose files, a `.zip`), in TypeScript, JavaScript and Python. Private repositories: download or clone them and use the folder or the CLI. Local files are read in the page and never uploaded.
- **Anonymous GitHub API limits** apply: about 30 analyses an hour per visitor. Local files, folders and zips don't use GitHub. Dragging a folder in is faster than choosing it, because `node_modules` and `.git` are skipped before anything is read.
- **Public GitHub repositories, or anything on your own machine** (a folder, loose files, a `.zip`), in TypeScript, JavaScript, Python and Go. Private repositories: paste a GitHub token (fine-grained, read-only "Contents"). It stays in the tab, goes only to api.github.com, and is forgotten when the tab closes. Or clone the repository and drop the folder. Local files are read in the page and never uploaded.
- **Anonymous GitHub API limits** apply: about 30 analyses an hour per visitor (a token raises it to 5,000 requests an hour). Local files, folders and zips don't use GitHub. Dragging a folder in is faster than choosing it, because `node_modules` and `.git` are skipped before anything is read.
- **Large repositories** are trimmed to 3,000 source files, tests and examples first, and the page says so.
- Host it yourself as a static site, for free on Vercel: see [`docs/DEPLOY.md`](docs/DEPLOY.md).

Expand Down Expand Up @@ -87,15 +88,51 @@ To host the web app yourself (Vercel, Docker, Google Cloud Run or AWS), see [`do

Targets accepted by `impact` / `slice`: `src/a.ts`, `src/a.ts#Foo.bar`, `src/a.ts:42`, or a bare `Foo.bar`.

## Use it with AI agents (MCP)

`codegraph mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio. Agents such as Claude
Code can ask what a change touches before they make it. Tools: `codegraph_impact`, `codegraph_diff_impact`,
`codegraph_find`, `codegraph_callers_callees`, `codegraph_overview`, `codegraph_reindex`.

```bash
claude mcp add codegraph -- npx -y codegraph-engine mcp .
```

Other clients take the same command in their MCP settings (`"command": "npx", "args": ["-y", "codegraph-engine", "mcp", "."]`).
The graph is built on the first question and held in memory; `codegraph_reindex` refreshes it after files change.

## On pull requests

The repository root is a GitHub Action that comments on each pull request with the symbols, files and tests its
changes can reach, and updates that comment as the pull request changes.

```yaml
# .github/workflows/impact.yml
on: pull_request
permissions: { contents: read, pull-requests: write }
jobs:
impact:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # the base commit must be present
- uses: harsharajkumar-273/CodeGraph@main
# with: { min-confidence: probable, fail-above: 200 }
```

`fail-above` makes the check fail when a change reaches more symbols than that. Pull requests from forks have a
read-only token, so the result goes to the job summary instead of a comment. The same text is available locally with
`codegraph impact --diff --range main...HEAD --format markdown`.

## What it understands

| | TypeScript / JavaScript | Python |
|---|---|---|
| Imports | ESM, `require()`, dynamic `import()`, barrels (`export *`), `tsconfig` paths, workspace packages | absolute/relative imports, `__init__.py` re-exports, `from pkg import submodule` |
| Symbols | functions, arrows, classes, methods, `Foo.prototype.x =`, `module.exports` | functions, classes, methods, nested functions |
| Calls | plain, member, `this`, `new`, JSX components, callbacks passed by name | plain, member, `self`/`super()`, decorators, callbacks |
| Type-aware | annotations, `new Foo()`, constructor parameter properties | annotations, `x = Foo()`, `self.x = Foo()` |
| Variables | `const`/`let`/`var`, params, `for`/`for-of`, `catch`, `+=` | assignment, params, `for`, `+=` |
| | TypeScript / JavaScript | Python | Go |
|---|---|---|---|
| Imports | ESM, `require()`, dynamic `import()`, barrels (`export *`), `tsconfig` paths, workspace packages | absolute/relative imports, `__init__.py` re-exports, `from pkg import submodule` | import paths resolved through `go.mod`; a package is its directory, so calls inside it need no import |
| Symbols | functions, arrows, classes, methods, `Foo.prototype.x =`, `module.exports` | functions, classes, methods, nested functions | functions, methods (attached to their receiver type wherever it is declared), structs, interfaces and interface methods |
| Calls | plain, member, `this`, `new`, JSX components, callbacks passed by name | plain, member, `self`/`super()`, decorators, callbacks | plain, `pkg.F()`, `x.M()` including promoted methods of embedded types, calls on results (`New().Run()`), `defer`/`go` |
| Type-aware | annotations, `new Foo()`, constructor parameter properties | annotations, `x = Foo()`, `self.x = Foo()` | parameters, `var x T`, `x := T{}` / `&T{}` / `new(T)` / `F()` results, `for _, v := range`, fields and field chains, typed constants, `//go:build` for linux/amd64 |
| Variables | `const`/`let`/`var`, params, `for`/`for-of`, `catch`, `+=` | assignment, params, `for`, `+=` | not tracked yet |

Variable lineage is **intra-procedural** (stage 1): it tracks a variable's value only within its own function, not across closures or object fields (`this.x`, `self.x`) — see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md#6-variable-lineage--stage-1-intra-procedural) for exactly what is and isn't tracked yet.

Expand All @@ -108,7 +145,7 @@ packages/extractors per-file fact extraction (incl. variable def/use) + cross-f
packages/analysis blast radius, slice, variable lineage, diff→symbols, import cycles (pure TS: Node or browser)
packages/cli codegraph index | impact | slice | vars | cycles | serve
packages/web the web app: GitHub / folder loaders, an indexing Web Worker, and the React Flow UI
fixtures/ tiny TS/JS and Python repos with hand-verified expected edges
fixtures/ tiny TS/JS, Python and Go repos with hand-verified expected edges
docs/ARCHITECTURE.md design, schema, resolution algorithm, decisions, roadmap
```

Expand Down
80 changes: 80 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: CodeGraph impact comment
description: Comments on a pull request with the functions, files and tests that its changes can reach.
author: harsharajkumar-273
branding:
icon: git-pull-request
color: blue

inputs:
github-token:
description: "Token used to post the comment. The default needs `pull-requests: write` on the workflow."
default: ${{ github.token }}
min-confidence:
description: "Weakest link to count (resolved, probable or ambiguous)."
default: probable
fail-above:
description: "Fail the check when more than this many symbols are affected. Leave empty to only comment."
default: ''

runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install CodeGraph
shell: bash
working-directory: ${{ github.action_path }}
run: npm ci --no-audit --no-fund
- name: Work out what the pull request can reach
id: impact
shell: bash
working-directory: ${{ github.action_path }}
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
MIN: ${{ inputs.min-confidence }}
WORKSPACE: ${{ github.workspace }}
OUT: ${{ runner.temp }}/codegraph-comment.md
run: |
if [ -z "$BASE_SHA" ]; then echo "::error::Run this action on pull_request events."; exit 1; fi
if ! git -C "$WORKSPACE" cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then
echo "::error::The base commit is not in the checkout. Use actions/checkout with fetch-depth: 0."; exit 1
fi
npx tsx packages/cli/src/bin.ts impact --diff --range "$BASE_SHA...HEAD" --repo "$WORKSPACE" \
--min-confidence "$MIN" --format markdown > "$OUT"
# The headline number in the comment is "**N symbols in M files**"; no match means nothing was affected.
N=$(grep -oE '\*\*[0-9]+ symbols' "$OUT" | grep -oE '[0-9]+' | head -1 || true)
echo "symbols=${N:-0}" >> "$GITHUB_OUTPUT"
echo "file=$OUT" >> "$GITHUB_OUTPUT"
- name: Post or update the comment
if: steps.impact.outputs.file != ''
uses: actions/github-script@v7
env:
FILE: ${{ steps.impact.outputs.file }}
with:
github-token: ${{ inputs.github-token }}
script: |
const fs = require('fs');
const body = fs.readFileSync(process.env.FILE, 'utf8').trim();
if (!body) { core.info('No changed source symbols, nothing to say.'); return; }
const marker = '<!-- codegraph-impact -->';
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;
try {
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 });
const mine = comments.find((c) => c.body && c.body.startsWith(marker));
if (mine) await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
else await github.rest.issues.createComment({ owner, repo, issue_number, body });
} catch (e) {
// Pull requests from forks get a read-only token. Show the result in the run summary instead.
core.warning(`Could not post the comment (${e.message}). Writing it to the job summary instead.`);
await core.summary.addRaw(body).write();
}
- name: Enforce the limit
if: inputs.fail-above != '' && steps.impact.outputs.symbols != ''
shell: bash
env:
SYMBOLS: ${{ steps.impact.outputs.symbols }}
LIMIT: ${{ inputs.fail-above }}
run: |
if [ "$SYMBOLS" -gt "$LIMIT" ]; then echo "::error::This change can reach $SYMBOLS symbols, over the limit of $LIMIT."; exit 1; fi
2 changes: 1 addition & 1 deletion docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Share a ready-made analysis with a link such as `https://<project>.vercel.app/?r
**Limits to know about**
- GitHub allows 60 anonymous API requests per hour per visitor IP. One analysis uses 2 (1 when the branch is given),
so a visitor can run about 30 analyses an hour. The page explains it when a visitor hits the limit.
- Only public repositories can be read from GitHub by a browser. For a private one, download it (or clone it) and drop the folder or the .zip onto the page, or use the CLI.
- Visitors can read a private repository by pasting a GitHub personal access token on the home page. The token stays in their tab (`sessionStorage`) and is sent only to `api.github.com`, never to your host. Files are then fetched through the contents API, which counts against the token's 5,000 requests an hour. Without a token, only public repositories are readable; for a private one, drop a clone or a .zip onto the page, or use the CLI.
- Very large repositories are trimmed to 3,000 source files (tests and examples first), and files over 1 MB are
skipped; the page says when it does this.

Expand Down
Loading
Loading