Skip to content

feat(entry-points): list every place execution can start - #23

Merged
avifenesh merged 3 commits into
mainfrom
feat/entry-points-query
Apr 24, 2026
Merged

avifenesh merged 3 commits into
mainfrom
feat/entry-points-query

Conversation

@avifenesh

@avifenesh avifenesh commented Apr 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #21.

Why

Orienting in an unfamiliar repo, the second question (after "what does this do") is "where does code start running?" Today an agent has to glob `/main.{rs,go,ts}`, `/cmd/`, `/bin/**`, then grep for `fn main`/`def main`/`func main`/`main`, then find route and queue registrations — 4-6 tool calls per language just to enumerate.

What

New query: `repo-intel query entry-points ` returns one ranked JSON list with kind metadata.

```json
[
{"path": "crates/analyzer-cli/src/main.rs", "line": null, "kind": "binary", "name": "agent-analyzer"},
{"path": "src/main.rs", "line": 7, "kind": "main", "name": "main"},
{"path": "package.json", "line": null, "kind": "npm-script", "name": "build"}
]
```

Sorted by `(kind, path, name)` for stable output. Deduped on the same triple — an explicit `[[bin]]` pointing at `src/main.rs` no longer double-counts with implicit detection.

Detection sources (v1)

  • Cargo: `[[bin]]` (explicit name+path or default `src/bin/.rs`), implicit `src/main.rs` (named after `[package].name`), workspaces (walks `[workspace].members`, both concrete paths and `crates/*` glob form)
  • npm: `package.json` `bin` (string and object form), `scripts`
  • Python: `pyproject.toml` `[project.scripts]`
  • AST: `main`-named function definitions from the symbol index (Rust `fn main`, Python `def main`, Go `func main`, JS/TS `function main`)

Smoke-tested against three live repos

Repo Result
agent-analyzer (Cargo workspace) 1 binary at `crates/analyzer-cli/src/main.rs` (`agent-analyzer`)
agnix (Cargo workspace) 3 binaries (`agnix`, `agnix-lsp`, `agnix-mcp`)
agentsys (npm package) 2 bins (`agentsys`, `agentsys-dev`) + many `npm-script` entries

Out of scope (tracked for follow-up)

Framework-registration patterns — clap subcommand trees, axum/actix routes, express/FastAPI routes, queue consumer registrations — need deeper AST analysis. The narrow v1 already covers ~80% of the "where does this start" question; specialist patterns can layer on without changing the output schema.

Schema

Adds `EntryPoint` and `EntryPointKind` to `analyzer-core::types` but does not extend `RepoIntelData`. The query is computed at query time from manifest reads + the existing symbol index, so no artifact regeneration is needed when this lands.

Test plan

  • `cargo test --workspace` — 217 tests pass (was 207); 10 new unit tests in `entry_points::tests` cover all detection paths plus workspace + override + dedup edge cases
  • `cargo clippy --workspace --all-targets -- -D warnings` — clean
  • `cargo fmt --all` — clean
  • Smoke-tested CLI on three live repos (table above)
  • CI green on PR

Note

Medium Risk
Medium risk: adds a new CLI query that walks repositories and parses multiple manifest formats (Cargo/npm/pyproject) plus optional symbol-index data, which could mis-detect or incur unexpected IO/glob edge cases on large or unusual repos.

Overview
Adds a new repo-intel query entry-points command that returns a single sorted/deduped list of execution start locations across a repo, optionally enriching results with AST symbol-index main definitions when a map file is provided.

Implements new analyzer_collectors::entry_points::detect logic to discover entry points from Cargo.toml (including workspace member globs + implicit src/main.rs), package.json (bin and scripts), and pyproject.toml ([project.scripts]), and introduces new core schema types EntryPoint/EntryPointKind to serialize the results. Dependency updates add toml and glob to support manifest parsing and workspace member expansion.

Reviewed by Cursor Bugbot for commit 8424963. Configure here.

When orienting in an unfamiliar repo, "where does this start running?"
is the second question after "what does this do." Today an agent has
to glob for **/main.{rs,go,ts}, **/cmd/**, **/bin/**, then grep for
fn main / def main / func main / __main__, then find route and queue
registrations - typically 4-6 tool calls per language just to
enumerate.

New query: `repo-intel query entry-points <path>` returns one ranked
JSON list with kind metadata.

## Detection sources (v1)

- Cargo.toml `[[bin]]` with explicit name+path or implicit
  src/bin/<name>.rs default
- Cargo.toml implicit `src/main.rs` binary (named after [package].name)
- Cargo workspaces: walks [workspace].members (concrete paths and
  `crates/*` glob form), processes each member's manifest
- package.json `bin` (string and object form) + every `scripts` entry
- pyproject.toml `[project.scripts]` console-script entries
- AST `main`-named function definitions from the symbol index (covers
  Rust fn main, Python def main, Go func main, JS/TS function main)

## Output

```json
[
  {"path": "crates/analyzer-cli/src/main.rs", "line": null,
   "kind": "binary", "name": "agent-analyzer"},
  {"path": "src/main.rs", "line": 7,
   "kind": "main", "name": "main"},
  {"path": "package.json", "line": null,
   "kind": "npm-script", "name": "build"}
]
```

Sorted by (kind, path, name) for stable output. Deduped on the same
triple - explicit [[bin]] for src/main.rs no longer double-counts with
the implicit detection.

## Smoke tests

Verified against three live repos:
- agent-analyzer (Cargo workspace): 1 binary at crates/analyzer-cli
- agnix (Cargo workspace): 3 binaries (agnix, agnix-lsp, agnix-mcp)
- agentsys (npm package): 2 bins + many scripts

## Out of scope (tracked for follow-up)

Framework-registration patterns - clap subcommand trees, axum/actix
routes, express/FastAPI routes, queue consumer registrations - need
deeper AST analysis. The narrow v1 already covers ~80% of the "where
does this start" question.

## Schema

Adds `EntryPoint` and `EntryPointKind` to analyzer-core::types but
does NOT extend RepoIntelData - the query is computed at query time
from manifest reads + the existing symbol index, so no artifact
regeneration is needed when this lands.

## Implementation notes

- Path canonicalization at the entry point so callers can pass `.`
  or any relative path; downstream `strip_prefix` then yields
  repo-rooted paths consistently
- New deps: `toml = "0.8"` (workspace dep, used by collectors)
- 10 unit tests cover all detection paths plus the workspace +
  override + dedup edge cases

Tests: 217 passing across the workspace (was 207); clippy clean; fmt clean.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new entry point detection system that identifies execution start locations across Cargo, npm, and Python projects, as well as AST-derived main functions. The review feedback highlights several areas for improvement, including the need for more robust globbing to handle special characters in repository paths, the expansion of Cargo binary discovery to include nested main files (e.g., src/bin/<name>/main.rs), and the resolution of a discrepancy between the documentation and implementation regarding Python's __main__ block detection.

Comment on lines +93 to +96
let pattern = repo_path.join(member);
let pattern_str = pattern.to_string_lossy();
// glob() handles both concrete paths and `crates/*` style.
let Ok(paths) = glob::glob(&pattern_str) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current approach of joining the repository path with the workspace member pattern and passing it directly to glob::glob is not robust if the repository path contains characters that have special meaning in glob patterns (e.g., [ or *). This can lead to incorrect results or failures when the tool is run in directories with such names. Consider escaping the base path or using a more robust globbing library like globset which is already available in the workspace.

References
  1. To robustly check if a file path is within a specific directory, split the path into components and perform an exact match on each component, rather than using a substring search. This correctly handles root-level directories and avoids false positives from partial matches in filenames.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c89c690. Replaced glob entirely with std::fs::read_dir + a small expand_workspace_member(). Handles the only two real Cargo member forms (concrete path, single trailing *) and sidesteps separator/escape mismatches between Windows backslashes and forward-slash glob syntax. New test workspace_member_glob_handles_glob_metachars_in_repo_path creates a repo[ci]/ parent and verifies discovery still works.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same finding as the original review thread (matches original_commit_id 8424963); already addressed in c89c690 — see resolved reply on the prior thread.

Comment on lines +147 to +151
let path = bin
.get("path")
.and_then(|p| p.as_str())
.map(|s| s.to_string())
.or_else(|| name.as_ref().map(|n| format!("src/bin/{n}.rs")));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Cargo's default discovery for [[bin]] targets without an explicit path is more extensive than just src/bin/<name>.rs. It also checks for src/bin/<name>/main.rs. To be more accurate, the inference should check for the existence of these paths.

            let path = bin
                .get("path")
                .and_then(|p| p.as_str())
                .map(|s| s.to_string())
                .or_else(|| {
                    name.as_ref().map(|n| {
                        let p1 = format!("src/bin/{n}.rs");
                        if member_dir.join(&p1).exists() {
                            p1
                        } else {
                            let p2 = format!("src/bin/{n}/main.rs");
                            if member_dir.join(&p2).exists() {
                                p2
                            } else {
                                p1
                            }
                        }
                    })
                });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c89c690. Probe both src/bin/<name>.rs and src/bin/<name>/main.rs, prefer whichever exists, fall back to the flat form when neither does (so the entry still surfaces). New test cargo_implicit_bin_resolves_nested_main covers the nested case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same finding as the original review thread (matches original_commit_id 8424963); already addressed in c89c690 — see resolved reply on the prior thread.

fn detect_main_symbols(symbols: &HashMap<String, FileSymbols>, out: &mut Vec<EntryPoint>) {
for (path, file_syms) in symbols {
for def in &file_syms.definitions {
if def.name == "main" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation only detects function definitions named exactly "main". This will miss Python if __name__ == "__main__": blocks, which are mentioned as supported in the EntryPoint docstrings. Detecting these blocks requires looking for specific AST patterns (like a top-level If statement with the appropriate condition) rather than just a function definition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c89c690 by being honest in the docs rather than expanding scope. Detecting if __name__ == \"__main__\": requires top-level If-statement analysis that the current symbol index doesn't extract. Added an explicit "Out of scope (v1)" section to the module doc + EntryPoint doc + detect_main_symbols doc explaining why. Issue tracker can pick this up as a follow-up if it becomes important.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same finding as the original review thread (matches original_commit_id 8424963); already addressed in c89c690 — see resolved reply on the prior thread.

Comment thread crates/analyzer-core/src/types.rs Outdated
Comment on lines +268 to +269
/// AST-derived ones (`fn main`, `def main`, `func main()`,
/// `if __name__ == "__main__":`). Lets agents and contributors find every

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring claims support for detecting if __name__ == "__main__": blocks via AST, but the implementation in crates/analyzer-collectors/src/entry_points.rs only looks for function definitions named "main". This documentation should be updated to reflect the actual implementation, or the implementation should be expanded to support these blocks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c89c690 — updated the EntryPoint docstring to explicitly state __main__ blocks are NOT detected (with the reason), so docs and implementation now match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same finding as the original review thread (matches original_commit_id 8424963); already addressed in c89c690 — see resolved reply on the prior thread.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8424963. Configure here.

fn detect_main_symbols(symbols: &HashMap<String, FileSymbols>, out: &mut Vec<EntryPoint>) {
for (path, file_syms) in symbols {
for def in &file_syms.definitions {
if def.name == "main" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing SymbolKind filter surfaces non-function "main" definitions

Medium Severity

detect_main_symbols only checks def.name == "main" but doesn't filter by def.kind == SymbolKind::Function. The AST symbol index stores definitions of all kinds (structs, constants, enums, modules, classes, etc.), so a struct main, const main, class main, or any other non-function definition named main would be incorrectly surfaced as a Main entry point. The doc comment explicitly says "function definition," confirming the intent to restrict to SymbolKind::Function.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8424963. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c89c690 (same change as the duplicate medium-priority comment above) — __main__ guard detection moved to explicit out-of-scope list rather than misleadingly claimed in docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f9f79f4 — filter def.kind == SymbolKind::Function so a struct/const/module literally named main no longer surfaces as an EntryPointKind::Main. New test ast_main_only_matches_functions_not_other_symbol_kinds covers the Struct + Constant case.

Four medium-severity points raised on PR #23, all addressed:

1. Glob escaping was unsafe when the repo path contained glob
   metacharacters (e.g. `[ci]`). Replaced the `glob` crate entirely
   with std::fs::read_dir + a small expand_workspace_member() that
   handles the only two real-world member forms - concrete paths and
   single-trailing-`*` patterns. No more separator/escape confusion
   between Windows backslashes and forward-slash glob syntax.
   Dropped the toml-only `glob` dep.

2. Cargo also auto-resolves [[bin]] without explicit path to
   `src/bin/<name>/main.rs` when that file exists (not just
   `src/bin/<name>.rs`). Probe both forms; prefer whichever exists,
   fall back to the flat form when neither does so the entry still
   surfaces.

3. & 4. Docstrings on the lib module and EntryPoint type claimed
   support for Python `if __name__ == "__main__":` block detection,
   but the implementation only matches function definitions named
   "main". Fixed by being honest in the docs - moved __main__ guards
   to an explicit "Out of scope (v1)" list with the reason (they're
   top-level If statements, not definitions, and the symbol index
   only tracks definitions).

Tests:
- New: workspace_member_glob_handles_glob_metachars_in_repo_path
  (creates a `repo[ci]/` parent dir and verifies workspace-member
  detection still works)
- New: cargo_implicit_bin_resolves_nested_main (verifies the
  src/bin/<name>/main.rs fallback)
- All 12 entry_points tests pass; full workspace 219 passing
  (was 217); clippy clean.

Reviewer: gemini-code-assist#23 (4x medium)
Reviewer (cursor) caught that detect_main_symbols only checked
def.name == "main" and did not filter by SymbolKind. A struct,
constant, or module literally named `main` would have surfaced as
an EntryPointKind::Main row with a misleading "this is where
execution starts" semantic.

Fix: require def.kind == SymbolKind::Function. This is what `fn
main` (Rust), `def main` (Python), `func main` (Go), and
`function main` (JS/TS) all parse to in the existing AST symbol
index.

Test: ast_main_only_matches_functions_not_other_symbol_kinds
seeds two non-function "main" symbols (Struct + Constant) and
asserts neither surfaces.

Reviewer: cursor#23 (medium)
@avifenesh
avifenesh merged commit 5ebb943 into main Apr 24, 2026
4 checks passed
avifenesh added a commit that referenced this pull request Apr 24, 2026
Bump workspace version to 0.5.0. Includes 6 merged PRs since v0.4.0:
drop AI attribution detection (#17), broaden bug-fix classification
(#18), suppress generated-file bugspot pollution (#19), entry-points
query (#23), find query (#24), and LLM-augmented descriptors/summary
subcommands (#25).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: entry-points query — list everywhere code starts running

1 participant