Skip to main content

Language-agnostic code graph: query symbols, entrypoints, and source-to-sink call paths from a SQLite index

Project description

entrygraph

Query your codebase like a graph. entrygraph indexes a repository into a SQLite database (through the SQLAlchemy ORM) and answers questions about symbols, classes/methods, entrypoints (HTTP routes, CLI commands, main functions, tasks, lambda handlers), and source → sink call-graph reachability ("can any HTTP route reach subprocess.run?").

Language-agnostic via tree-sitter; first-class support for Python, JavaScript/TypeScript, Go, Java, Ruby, C#, PHP, and Rust, with language and framework detection.

Reachability is a heuristic taint tier, not just call-edge closure: paths are risk-ranked, sanitizers prune or discount them, class-hierarchy analysis recovers virtual dispatch, and confidence flags trade recall for precision. Entrypoints include decorator/attribute routes, call-based route registration, middleware, and config-file handlers (serverless, SAM, Procfile, Dockerfile).

Install

pip install entrygraph        # or: uv pip install entrygraph

Requires Python ≥ 3.11. Depends on sqlalchemy, tree-sitter, tree-sitter-language-pack, and pathspec.

Python API

from entrygraph import CodeGraph

# Index a repo (creates <repo>/.entrygraph.db by default)
graph = CodeGraph.index("/path/to/repo")

# ...or open an existing index
graph = CodeGraph.open("graph.db")

# Symbols — glob on name or qualified name, filter by kind or file
graph.symbols(kind="class", name="User*")
graph.symbol("app.services.Runner.execute")        # exact; raises if missing

# Detection
report = graph.detect()
report.languages      # -> [DetectedLanguage(name="python", percent=96.7, ...), ...]
report.frameworks     # -> [DetectedFramework(name="flask", confidence=0.94, ...), ...]

# Entrypoints
graph.entrypoints(framework="flask")
graph.entrypoints(kind="http_route", route="/api/*")

# Call graph
graph.callers("app.services.run_report")            # who calls it
graph.callees("app.services.run_report", depth=3)   # what it (transitively) calls
graph.references("app.models.CONST")                # inbound edges of any kind

# Source -> sink reachability (paths are risk-ranked, highest first)
paths = graph.paths(source="app.routes.*", sink_category="command_exec")
for p in paths:
    print(p.risk_score, p.render(), "(+may continue)" if p.may_continue else "")
    # 0.72 app.routes.create_report -> app.services.run_report (line 20)
    #   -> ...ReportRunner.render_and_execute (line 17) -> py:subprocess.run (line 22)

graph.reachable(source="app.routes.upload", sink="py:subprocess.run")   # -> bool

# Precision/recall dial. By default only EXACT/IMPORT and unique-name FUZZY
# edges are traversed. Opt into wider (noisier) traversal:
graph.paths(source="app.routes.*", sink_category="sql",
            include_unresolved=True)   # follow py:*.execute wildcard-sink guesses
graph.paths(source="app.routes.*", sink_category="command_exec",
            include_fuzzy=True)        # follow speculative class-hierarchy (CHA) edges
graph.paths(source="app.routes.*", sink_category="command_exec",
            prune_sanitized=True)      # drop paths neutralized by a shlex.quote etc.

# Incremental re-index (only changed/added/deleted files are reparsed)
graph.refresh()

# Escape hatches
graph.session()               # raw SQLAlchemy Session
graph.sql("SELECT ...")       # textual query -> list[dict]

Every result is a frozen, immutable dataclass detached from the DB session, so results are safe to hold and trivial to serialize.

CLI

Human-readable output is rendered with Rich — colored tables, language/confidence bars, a progress spinner while indexing, and risk-ranked reachability rendered as call trees (sink nodes highlighted, per-hop confidence, sanitizer/const-arg badges). Piped or --json output stays plain.

entrygraph index PATH [--full] [--paranoid]     # incremental by default
entrygraph detect
entrygraph symbols --kind class --name 'User*'
entrygraph entrypoints --framework flask
entrygraph callers  app.services.run_report --depth 2
entrygraph callees  app.services.run_report
entrygraph paths --source 'app.routes.*' --sink-category command_exec
entrygraph stats

Add --json to any query command for machine-readable output. entrygraph paths exits 0 when a path is found and 1 when none is — handy in CI:

entrygraph paths --source '*' --sink-category command_exec && echo "reachable!"

How it works

  1. Walkos.scandir with hard-pruned junk dirs (node_modules, .venv, …), .gitignore rules, and size/binary/minified gates. Every skip is recorded with a reason.
  2. Extract — tree-sitter .scm queries harvest definitions/imports/calls; small per-language "shaper" modules build qualified names, import maps, and receiver info. Parsing runs across a process pool for large repos.
  3. Resolve — a two-pass resolver binds references to symbols with a confidence level (exact / import / fuzzy / unresolved). External callees (subprocess.run, child_process.exec, …) become placeholder nodes so sinks are real graph terminals.
  4. Detect — frameworks are scored from manifest dependencies plus code signals (noisy-or); entrypoint rules map framework patterns to route/command records.
  5. Store — everything persists to SQLite via the SQLAlchemy 2.0 ORM with bulk inserts and app-assigned keys. Re-indexing is incremental and content-hash driven.
  6. Query — reachability runs over an in-memory adjacency cache (BFS/DFS with cycle handling); a recursive-CTE SQL engine is available as a fallback (engine="sql").

Extending

  • Custom sinks/sources/sanitizers — drop an entrygraph.toml in the repo root with [[sink]] / [[source]] / [[sanitizer]] tables (same schema as the built-in data/sinks/*.toml), or call entrygraph.detect.taint.register_sink(...) / register_sanitizer(...). A [[sanitizer]] with effect = "neutralizes" prunes a path for its category; effect = "reduces" only discounts the risk score. Third-party wrapper libraries that reach a sink internally are covered by data/sinks/lib_*.toml "library summaries" (same schema, with a library = "..." tag).
  • New frameworks / entrypoints — register a FrameworkSpec and an EntrypointRule; adding a framework is usually a few lines.
  • New languages — add a <lang>/{definitions,imports,calls}.scm query set and a shaper implementing the LanguageExtractor protocol.

Releasing

Merging to main auto-bumps the patch version (via a git tag) and publishes to PyPI through Trusted Publishing — see RELEASING.md. The package version is derived from git tags by hatch-vcs, so it's never hand-edited.

License

MIT

Project details


Release history Release notifications | RSS feed

This version

0.1.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

entrygraph-0.1.0.tar.gz (166.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

entrygraph-0.1.0-py3-none-any.whl (140.6 kB view details)

Uploaded Python 3

File details

Details for the file entrygraph-0.1.0.tar.gz.

File metadata

  • Download URL: entrygraph-0.1.0.tar.gz
  • Upload date:
  • Size: 166.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for entrygraph-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0314c7febaa864aa27865f38ecf445d16278fbdbc46c634031c6f795f7de3019
MD5 c14109c7ec236435190a5b08747631d2
BLAKE2b-256 0b617d709d1e82404faf76f70c009f25fa4edc8a673f1d99300bbed522da14c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for entrygraph-0.1.0.tar.gz:

Publisher: release.yml on brettbergin/entrygraph

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file entrygraph-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: entrygraph-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 140.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for entrygraph-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4438c42c68cf4a36b66db6893ca512b469692f6068c329e21ccd295c9677970d
MD5 f91798badee755bd0278fc75191e7e0e
BLAKE2b-256 7241c14fcb24c1d625585e8cd22e1eac4664082354765d20c3a44f585c910b40

See more details on using hashes here.

Provenance

The following attestation bundles were made for entrygraph-0.1.0-py3-none-any.whl:

Publisher: release.yml on brettbergin/entrygraph

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page