diff --git a/AGENTS.md b/AGENTS.md index 914826e6..2e762202 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,24 @@ extracts symbols and their relationships, persists them to a per-project SQLite (with an FTS5 search index), and exposes the result through a CLI and an MCP (Model Context Protocol) stdio server. No AI / vector / LLM anywhere in the binary — output is byte-stable. +## CodeGraph command contract + +Before code research, run `codegraph status . --json`. Lifecycle commands +(`status`, `init`, `sync`, `index`) accept an optional positional project path. +Research commands take one positional query/target and select the project with +`-p/--path`: + +```bash +codegraph explore "" -p . +codegraph search "" -p . +codegraph node "" -p . +``` + +Do not append `.` as a second positional argument to a research command. If an +argument is rejected, run `codegraph --help` and correct the syntax +before falling back to grep. `node` accepts a symbol name, qualified name, exact +`node.id` from `search --json`, or an indexed file path. + ## Hard invariants (never break) - **Golden `.schema` byte-stability** — verified by `crates/codegraph-bench/tests/equivalence.rs` diff --git a/README.md b/README.md index d5ebf190..442de43b 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,12 @@ codegraph serve --mcp --path /path/to/project # MCP server (--path optional, codegraph install --yes --init # wire detected agents + initialize cwd ``` +Project paths are intentionally parsed differently by command family: +lifecycle commands use an optional positional path (`codegraph status .`), +while research commands use `-p/--path` +(`codegraph search "" -p .`). If a command rejects an argument, check +`codegraph --help` before falling back to text search. + --- ## Installation diff --git a/crates/codegraph-cli/src/installer/shared.rs b/crates/codegraph-cli/src/installer/shared.rs index 440c315b..598e4cf1 100644 --- a/crates/codegraph-cli/src/installer/shared.rs +++ b/crates/codegraph-cli/src/installer/shared.rs @@ -35,6 +35,13 @@ Before the first query in a repository, verify that the index is usable: Do not infer index readiness from the directory alone. Follow the recovery guidance reported by `status`; do not initialize or rebuild an index unless the user requests it or `status` explicitly requires it. +Shell project-path syntax differs by command family: + +- Lifecycle commands accept an optional positional path: `codegraph status . --json`, `codegraph init .`, `codegraph sync .`, `codegraph index .`. +- Research commands take exactly one positional query/target and use `-p/--path` for the project: `codegraph explore \"\" -p .`, `codegraph search \"\" -p .`, `codegraph node \"\" -p .`. + +Do not append the project as a second positional argument to a research command. If the shell rejects an argument, run `codegraph --help` and correct the syntax before falling back to grep/find. + For code research: - Start with `codegraph_explore` for an area, feature, or call flow. @@ -1075,6 +1082,8 @@ mod tests { assert!(text.contains("`codegraph_status`")); assert!(text.contains("`codegraph status . --json`")); assert!(text.contains("`codegraph_search`")); + assert!(text.contains("`codegraph search \"\" -p .`")); + assert!(text.contains("`codegraph --help`")); assert!(text.contains("caller/callee trail")); assert!(text.contains("do not create one unless the user asks")); assert!(!text.contains("deferred")); diff --git a/crates/codegraph-cli/src/main.rs b/crates/codegraph-cli/src/main.rs index 8988cf09..a14556c5 100644 --- a/crates/codegraph-cli/src/main.rs +++ b/crates/codegraph-cli/src/main.rs @@ -145,8 +145,29 @@ pub(crate) mod test_env { } const VERSION: &str = env!("CARGO_PKG_VERSION"); +// MSVC executables start with a 1 MiB main-thread stack. Keep CLI growth and +// indexing work independent of that platform default. +const CLI_MAIN_STACK_BYTES: usize = 8 * 1024 * 1024; fn main() { + let main_thread = match std::thread::Builder::new() + .name("codegraph-main".to_string()) + .stack_size(CLI_MAIN_STACK_BYTES) + .spawn(cli_main) + { + Ok(handle) => handle, + Err(err) => { + eprintln!("CodeGraph startup error: failed to create the CLI thread: {err}"); + std::process::exit(1); + } + }; + + if let Err(payload) = main_thread.join() { + std::panic::resume_unwind(payload); + } +} + +fn cli_main() { let cli = Cli::parse(); // Process bootstrap has no addressed project yet, so this config is // `APP_CONFIG`-or-defaults ONLY and may configure NOTHING but the logger @@ -203,6 +224,8 @@ struct Cli { enum Command { // Upstream flags/output: upstream bin/codegraph.ts:420-424, 431-470. Init { + /// Project root. Lifecycle commands accept this as an optional + /// positional path. path: Option, /// Also write project-level MCP config for these agents (csv ids, /// `auto`, `all`, `none`). Defaults to `none` (index only). Editors that @@ -217,12 +240,16 @@ enum Command { }, // Upstream flags/output: upstream bin/codegraph.ts:482-485, 489-527. Uninit { + /// Project root. Lifecycle commands accept this as an optional + /// positional path. path: Option, #[arg(short, long)] force: bool, }, // Upstream flags/output: upstream bin/codegraph.ts:536-540, 545-596. Index { + /// Project root. Lifecycle commands accept this as an optional + /// positional path. path: Option, #[arg(short, long)] force: bool, @@ -233,12 +260,16 @@ enum Command { }, // Upstream flags/output: upstream bin/codegraph.ts:605-608, 612-657. Sync { + /// Project root. Lifecycle commands accept this as an optional + /// positional path. path: Option, #[arg(short, long)] quiet: bool, }, // Upstream flags/output shape: upstream bin/codegraph.ts:667-670, 679-738, 743-820. Status { + /// Project root. Lifecycle commands accept this as an optional + /// positional path. path: Option, #[arg(short = 'j', long = "json")] json: bool, @@ -249,6 +280,8 @@ enum Command { #[command(visible_alias = "query")] Search { search: String, + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, #[arg(short, long, default_value_t = 10)] @@ -263,6 +296,8 @@ enum Command { }, // Upstream flags/output shape: upstream bin/codegraph.ts:903-911, 939-1013. Files { + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, /// Filter to files under this directory (path prefix). @@ -282,6 +317,8 @@ enum Command { }, // Upstream flags/output: upstream bin/codegraph.ts:1110-1115, 1124-1156. Serve { + /// Project root to pin. Use `-p/--path`; this command does not accept a + /// positional project path. #[arg(short, long)] path: Option, #[arg(long)] @@ -313,11 +350,15 @@ enum Command { }, // Upstream flags/output: upstream bin/codegraph.ts:1167-1169, 1173-1186. Unlock { + /// Project root. Lifecycle commands accept this as an optional + /// positional path. path: Option, }, // Upstream flags/output shape: upstream bin/codegraph.ts:1201-1205, 1219-1267. Callers { symbol: String, + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, #[arg(short, long, default_value_t = 20)] @@ -335,6 +376,8 @@ enum Command { // Upstream flags/output shape: upstream bin/codegraph.ts:1280-1284, 1298-1345. Callees { symbol: String, + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, #[arg(short, long, default_value_t = 20)] @@ -352,6 +395,8 @@ enum Command { // Upstream flags/output shape: upstream bin/codegraph.ts:1358-1362, 1374-1439. Impact { symbol: String, + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, #[arg(short, long, default_value_t = 2)] @@ -369,6 +414,8 @@ enum Command { // Upstream flags/output shape: upstream bin/codegraph.ts:1462-1469, 1479-1582. Affected { files: Vec, + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, #[arg(short, long, default_value_t = 5)] @@ -381,6 +428,8 @@ enum Command { // cycle detection. Ports `findCircularDependencies` // (upstream graph/queries.ts:225-263). Check { + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, #[arg(short = 'j', long = "json")] @@ -390,7 +439,8 @@ enum Command { /// and reverse-dependency impact. Computed from the existing graph + disk /// checks; adds no extraction and is separate from `check`. Audit { - /// Project root (NOT a result filter; use --include/--exclude to narrow). + /// Project root (use `-p/--path`; NOT a result filter — use + /// `--include`/`--exclude` to narrow). #[arg(short, long)] path: Option, /// Report `.tres`/`.tscn` resources nothing references. @@ -415,6 +465,8 @@ enum Command { json: bool, }, Export { + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, #[arg(short = 'o', long = "out")] @@ -429,6 +481,8 @@ enum Command { Explore { /// Symbol names or a natural-language question to explore. query: String, + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, /// Max number of files to include source from (clamped 1..=20). @@ -442,8 +496,11 @@ enum Command { /// caller/callee trail; for a file it prints the line-numbered source plus /// which files depend on it. NO LLM. Node { - /// A symbol name, or a file path/basename to read. + /// A symbol name, exact node ID (for example from `search --json`), or + /// a file path/basename to read. target: String, + /// Project root. Query commands require `-p/--path`; do not append the + /// project as another positional argument. #[arg(short, long)] path: Option, /// Pin an overloaded SYMBOL to the definition in this file (path or diff --git a/crates/codegraph-cli/tests/explore_node_cli.rs b/crates/codegraph-cli/tests/explore_node_cli.rs index ef4b560b..59da2d1d 100644 --- a/crates/codegraph-cli/tests/explore_node_cli.rs +++ b/crates/codegraph-cli/tests/explore_node_cli.rs @@ -126,6 +126,57 @@ fn help_lists_explore_and_node_subcommands() { "node must be a real subcommand: {}", nh.stderr ); + assert!( + nh.stdout.contains("exact node ID"), + "node help must advertise search-result IDs: {}", + nh.stdout + ); + + let sh = run_in(dir.path(), &["search", "--help"]); + assert!(sh.ok, "search --help must succeed: {}", sh.stderr); + assert!( + sh.stdout.contains("-p/--path") && sh.stdout.contains("positional"), + "query help must distinguish the project path option: {}", + sh.stdout + ); + + let status_help = run_in(dir.path(), &["status", "--help"]); + assert!( + status_help.ok, + "status --help must succeed: {}", + status_help.stderr + ); + assert!( + status_help.stdout.contains("optional positional"), + "lifecycle help must identify its positional path: {}", + status_help.stdout + ); +} + +#[cfg(unix)] +#[test] +fn init_survives_a_one_mib_launcher_stack() { + let dir = TestDir::new("small-launcher-stack"); + let project = dir.path().join("mini"); + copy_tree(&mini_fixture(), &project); + + let output = Command::new("sh") + .arg("-c") + .arg("ulimit -s 1024; exec \"$1\" init \"$2\"") + .arg("codegraph-small-stack") + .arg(bin()) + .arg(&project) + .current_dir(dir.path()) + .env("CODEGRAPH_NO_DAEMON", "1") + .output() + .expect("run codegraph with a one MiB launcher stack"); + + assert!( + output.status.success(), + "init must not depend on the platform main-thread stack size: stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); } #[test] @@ -186,6 +237,41 @@ fn node_symbol_returns_source_and_trail() { ); } +#[test] +fn search_json_node_id_round_trips_through_node() { + let dir = TestDir::new("node-id"); + let project = indexed_project(&dir); + let p = project.to_str().unwrap(); + + let search = run_in( + dir.path(), + &["search", "Counter", "-p", p, "--json", "--strict"], + ); + assert!(search.ok, "search --json must succeed: {}", search.stderr); + let results: serde_json::Value = + serde_json::from_str(&search.stdout).expect("search emits JSON"); + let id = results[0]["node"]["id"] + .as_str() + .expect("search result carries node.id"); + + let node = run_in(dir.path(), &["node", id, "-p", p, "--strict"]); + assert!( + node.ok, + "node must succeed: {}", + node.stderr + ); + assert!( + node.stdout.contains("## Counter (class)"), + "node ID must render the named target, not the opaque ID: {}", + node.stdout + ); + assert!( + node.stdout.contains("src/math.ts"), + "node ID must preserve the target location: {}", + node.stdout + ); +} + #[test] fn node_file_returns_numbered_source() { let dir = TestDir::new("node-file"); diff --git a/crates/codegraph-mcp/src/engine.rs b/crates/codegraph-mcp/src/engine.rs index 1e412130..47044167 100644 --- a/crates/codegraph-mcp/src/engine.rs +++ b/crates/codegraph-mcp/src/engine.rs @@ -566,25 +566,48 @@ impl CodeGraphEngine { .and_then(Value::as_bool) .unwrap_or(false); - let matches = self.find_symbol_matches(symbol)?; + // Exact node IDs are authoritative. This makes the public + // `codegraph search --json` -> `codegraph node ` flow round-trip + // without routing the opaque ID through fuzzy/qualified-name search. + let matches = match self.store.node_by_id(symbol)? { + Some(node) => vec![node], + None => self.find_symbol_matches(symbol)?, + }; if matches.is_empty() { return Ok(ToolResult::not_found_text(format!( "Symbol \"{symbol}\" not found in the codebase" ))); } - // `symbol` + `file` (#1314): the schema promises `file` PINS an - // overloaded name to the definition in that file. A pin that matches - // nothing reports not-found rather than falling back to an arbitrary - // overload — a silent miss beats a wrong answer. - if let Some(file_hint) = file_hint { + let line_hint = args.get("line").and_then(Value::as_i64); + // `symbol` + `file`/`line` (#1314): the schema promises these fields + // PIN an overloaded name to one definition. A pin that matches nothing + // reports not-found rather than falling back to an arbitrary overload + // — a silent miss beats a wrong answer. + if file_hint.is_some() || line_hint.is_some() { let pinned: Vec = matches .iter() - .filter(|n| file_path_matches_hint(&n.file_path, file_hint)) + .filter(|node| { + let file_matches = file_hint + .map(|file| file_path_matches_hint(&node.file_path, file)) + .unwrap_or(true); + let line_matches = line_hint + .map(|line| { + node.start_line <= line && line <= node.end_line.max(node.start_line) + }) + .unwrap_or(true); + file_matches && line_matches + }) .cloned() .collect(); if pinned.is_empty() { + let scope = match (file_hint, line_hint) { + (Some(file), Some(line)) => format!(" in \"{file}\" at line {line}"), + (Some(file), None) => format!(" in \"{file}\""), + (None, Some(line)) => format!(" at line {line}"), + (None, None) => String::new(), + }; return Ok(ToolResult::not_found_text(format!( - "Symbol \"{symbol}\" not found in \"{file_hint}\"" + "Symbol \"{symbol}\" not found{scope}" ))); } if pinned.len() == 1 { @@ -4719,8 +4742,8 @@ mod tests { "setState", "setState", "src/beta.ts", - 1, - 4, + 10, + 13, NodeKind::Function, Language::TypeScript, ); @@ -4741,6 +4764,32 @@ mod tests { "a resolved pin renders one definition, not the ambiguity list: {txt}" ); + let line_pinned = engine.execute( + "codegraph_node", + &serde_json::json!({"symbol": "setState", "line": 10}), + ); + let txt = text_of(&line_pinned); + assert!(txt.contains("src/beta.ts"), "got: {txt}"); + assert!( + !txt.contains("src/alpha.ts"), + "the line pin must exclude the other overload: {txt}" + ); + + let file_and_line_pinned = engine.execute( + "codegraph_node", + &serde_json::json!({ + "symbol": "setState", + "file": "src/beta.ts", + "line": 11 + }), + ); + let txt = text_of(&file_and_line_pinned); + assert!(txt.contains("src/beta.ts"), "got: {txt}"); + assert!( + !txt.contains("src/alpha.ts"), + "the combined pin must exclude the other overload: {txt}" + ); + let bad = engine.execute( "codegraph_node", &serde_json::json!({"symbol": "setState", "file": "src/nowhere.ts"}), @@ -4751,6 +4800,17 @@ mod tests { "got: {}", text_of(&bad) ); + + let bad_line = engine.execute( + "codegraph_node", + &serde_json::json!({"symbol": "setState", "line": 99}), + ); + assert_eq!(bad_line.not_found, Some(true)); + assert!( + text_of(&bad_line).contains("at line 99"), + "got: {}", + text_of(&bad_line) + ); } #[test] @@ -4768,6 +4828,37 @@ mod tests { assert!(text_of(&tr).contains("not found in the codebase")); } + #[test] + fn ext_node_exact_id_round_trips_without_name_search() { + let mut engine = test_engine(); + put_indexed_source( + &engine, + "svc.rs", + &(1..=20).map(|i| format!("line {i}\n")).collect::(), + Language::Rust, + 1, + ); + let target = node_lang( + "doThing", + "doThing", + "svc.rs", + 10, + 12, + NodeKind::Function, + Language::Rust, + ); + let id = target.id.clone(); + put_nodes(&mut engine, &[target]); + + let tr = engine.execute( + "codegraph_node", + &serde_json::json!({"symbol": id, "includeCode": true}), + ); + let txt = text_of(&tr); + assert!(txt.contains("## doThing (function)"), "got: {txt}"); + assert!(txt.contains("line 10"), "got: {txt}"); + } + #[test] fn ext_node_single_match_with_code_and_trail() { let mut engine = test_engine(); diff --git a/crates/codegraph-mcp/src/instructions.rs b/crates/codegraph-mcp/src/instructions.rs index bb0e38d6..670568e0 100644 --- a/crates/codegraph-mcp/src/instructions.rs +++ b/crates/codegraph-mcp/src/instructions.rs @@ -85,7 +85,7 @@ typically one to a few calls; a grep/read exploration is dozens. - **"What is the symbol named X?" (just its location)** → `codegraph_search` - **"What calls this?" / "What does this call?" / "What would changing this break?"** → `codegraph_callers` / `codegraph_callees` / `codegraph_impact` - **Reading a source FILE (any time you'd use the `Read` tool)** → `codegraph_node` with a `file` path and no `symbol`. It returns the file's **current source with line numbers — the same `\t` shape `Read` gives you, safe to `Edit` from** — narrowable with `offset`/`limit` exactly like `Read`, PLUS a one-line note of which files depend on it. Same bytes as `Read`, faster (served from the index), with the blast radius attached. Use it **instead of `Read`** for indexed source files; fall back to `Read` only for what codegraph doesn't index (configs, docs). Pass `symbolsOnly: true` for just the file's structure. -- **About to read or edit a symbol you can name** → `codegraph_node` with that `symbol` (SECONDARY — the after-explore depth tool): the verbatim source (`includeCode: true`) PLUS its caller/callee trail, so before changing it you see what calls it and what your edit would break. For an OVERLOADED name it returns EVERY matching definition's body in one call, so you never Read a file to find the right overload +- **About to read or edit a symbol you can name (or whose exact node ID you have)** → `codegraph_node` with that `symbol` (SECONDARY — the after-explore depth tool): the verbatim source (`includeCode: true`) PLUS its caller/callee trail, so before changing it you see what calls it and what your edit would break. For an OVERLOADED name it returns EVERY matching definition's body in one call, so you never Read a file to find the right overload - **"What's in directory X?"** → `codegraph_files` - **"Is the index ready / what's its size?"** → `codegraph_status` @@ -107,7 +107,7 @@ typically one to a few calls; a grep/read exploration is dozens. ## Limitations -- If a tool reports the project isn't initialized, `.codegraph/` doesn't exist yet — offer to run `codegraph init -i` to build the index. +- If a tool reports the project isn't initialized, `.codegraph/` doesn't exist yet — offer to run `codegraph init .` to build the index. - Index lags file writes by ~1 second. - Cross-file resolution is best-effort name matching; ambiguous calls may return multiple candidates. - No live correctness validation — that's still the TypeScript compiler / test suite / linter's job. Codegraph supplements those with structural context they don't have. diff --git a/crates/codegraph-mcp/src/schemas.rs b/crates/codegraph-mcp/src/schemas.rs index 5c706f00..0a8fcc97 100644 --- a/crates/codegraph-mcp/src/schemas.rs +++ b/crates/codegraph-mcp/src/schemas.rs @@ -171,6 +171,17 @@ mod tests { !DEFAULT_MCP_TOOLS.contains(&"check"), "codegraph_check is additive, not part of the default surface" ); + let node = arr + .iter() + .find(|tool| tool["name"] == "codegraph_node") + .expect("node tool exists"); + assert!( + node["inputSchema"]["properties"]["symbol"]["description"] + .as_str() + .expect("node symbol description") + .contains("exact node ID"), + "the public schema must advertise exact-ID lookup" + ); } #[test] diff --git a/crates/codegraph-mcp/src/tools_list.json b/crates/codegraph-mcp/src/tools_list.json index b647fd96..1a49661d 100644 --- a/crates/codegraph-mcp/src/tools_list.json +++ b/crates/codegraph-mcp/src/tools_list.json @@ -139,13 +139,13 @@ }, { "name": "codegraph_node", - "description": "Two modes. (1) READ A FILE — use INSTEAD of the Read tool: pass `file` (a path or basename) with no `symbol` and it returns that file's current on-disk source with line numbers, exactly the shape Read gives you (`\\t`, safe to Edit from), narrowable with `offset`/`limit` just like Read — PLUS a one-line note of which files depend on it. Same bytes as Read, faster (served from the index), with the blast radius attached. Use it whenever you would Read a source file. (2) ONE SYMBOL you can name — its location, signature, verbatim source (includeCode=true) and caller/callee trail in one call, so before changing it you see what calls it and what your edit would break. For an AMBIGUOUS name it returns EVERY matching definition's body in one call (so you never Read a file to find the right overload); pass `file`/`line` to pin one. Use codegraph_explore for several related symbols or the full flow.", + "description": "Two modes. (1) READ A FILE — use INSTEAD of the Read tool: pass `file` (a path or basename) with no `symbol` and it returns that file's current on-disk source with line numbers, exactly the shape Read gives you (`\\t`, safe to Edit from), narrowable with `offset`/`limit` just like Read — PLUS a one-line note of which files depend on it. Same bytes as Read, faster (served from the index), with the blast radius attached. Use it whenever you would Read a source file. (2) ONE SYMBOL by name or exact node ID — its location, signature, verbatim source (includeCode=true) and caller/callee trail in one call, so before changing it you see what calls it and what your edit would break. For an AMBIGUOUS name it returns EVERY matching definition's body in one call (so you never Read a file to find the right overload); pass `file`/`line` to pin one. Use codegraph_explore for several related symbols or the full flow.", "inputSchema": { "type": "object", "properties": { "symbol": { "type": "string", - "description": "Name of the symbol to read (symbol mode). Omit it and pass `file` alone to read a whole file like Read." + "description": "Name or exact node ID of the symbol to read (symbol mode). CLI `codegraph search --json` exposes node IDs. Omit it and pass `file` alone to read a whole file like Read." }, "includeCode": { "type": "boolean", diff --git a/crates/codegraph-watch/src/worktree.rs b/crates/codegraph-watch/src/worktree.rs index e2232d5b..a2b95c09 100644 --- a/crates/codegraph-watch/src/worktree.rs +++ b/crates/codegraph-watch/src/worktree.rs @@ -42,7 +42,7 @@ pub fn detect_worktree_index_mismatch( pub fn worktree_mismatch_warning(mismatch: &WorktreeIndexMismatch) -> String { format!( - "This CodeGraph index belongs to a different git working tree.\n Running in: {}\n Index from: {}\nResults reflect that tree's code (often a different branch), not this worktree - symbols changed only here are missing. Run \"codegraph init -i\" in this worktree for a worktree-local index.", + "This CodeGraph index belongs to a different git working tree.\n Running in: {}\n Index from: {}\nResults reflect that tree's code (often a different branch), not this worktree - symbols changed only here are missing. Run \"codegraph init .\" in this worktree for a worktree-local index.", mismatch.worktree_root.display(), mismatch.index_root.display() ) @@ -50,7 +50,7 @@ pub fn worktree_mismatch_warning(mismatch: &WorktreeIndexMismatch) -> String { pub fn worktree_mismatch_notice(mismatch: &WorktreeIndexMismatch) -> String { format!( - "CodeGraph results below come from a different git worktree ({}), not where you're working ({}) - they may reflect another branch, and symbols changed only here are missing. Run \"codegraph init -i\" here for a worktree-local index.", + "CodeGraph results below come from a different git worktree ({}), not where you're working ({}) - they may reflect another branch, and symbols changed only here are missing. Run \"codegraph init .\" here for a worktree-local index.", mismatch.index_root.display(), mismatch.worktree_root.display() ) @@ -185,11 +185,13 @@ mod tests { let warning = worktree_mismatch_warning(&mismatch); assert!(warning.contains("/work/tree")); assert!(warning.contains("/index/tree")); - assert!(warning.contains("codegraph init")); + assert!(warning.contains("codegraph init .")); + assert!(!warning.contains("codegraph init -i")); let notice = worktree_mismatch_notice(&mismatch); assert!(notice.contains("/work/tree")); assert!(notice.contains("/index/tree")); - assert!(notice.contains("codegraph init")); + assert!(notice.contains("codegraph init .")); + assert!(!notice.contains("codegraph init -i")); } } diff --git a/docs/cli.md b/docs/cli.md index 980a2f3c..534579e4 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -36,11 +36,11 @@ | `callees` | What a symbol calls | ``, `-p`, `-l`, `-j`, `--strict`, `--file ` | | `impact` | Blast radius of changing a symbol (incoming deps, transitive) | ``, `-p`, `-d/--depth`, `-j`, `--strict`, `--file ` | | `affected` | Given changed files, the affected symbol set | `[files...]`, `-p`, `-d/--depth`, `--filter` | -| `check` | Detect circular dependencies (each cycle as `a.ts -> b.ts -> a.ts`) | `[path]`, `-j/--json` | +| `check` | Detect circular dependencies (each cycle as `a.ts -> b.ts -> a.ts`) | `-p/--path`, `-j/--json` | | `audit` | Read-only Godot resource audit: orphan resources, dangling references, impact | `-p`, `--orphans`, `--dangling`, `--impact ` (≥1 required), `--verify-plan`, `--include `, `--exclude `, `-j/--json` | -| `export` | Export the whole code graph as NetworkX node-link JSON | `[path]`, `-o/--out `, `--no-centrality` | +| `export` | Export the whole code graph as NetworkX node-link JSON | `-p/--path`, `-o/--out `, `--no-centrality` | | `explore` | Explore an area with the same deterministic engine/output as `codegraph_explore` | ``, `-p`, `--max-files <1..20>`, `-j/--json` | -| `node` | Read a symbol or indexed file with the same engine/output as `codegraph_node` | ``, `-p`, `-f/--file`, `--symbols-only`, `-j/--json`, `--strict` | +| `node` | Read a symbol name, exact node ID, or indexed file with the `codegraph_node` engine | ``, `-p`, `-f/--file`, `--symbols-only`, `-j/--json`, `--strict` | | `http` | Inspect or stop detached HTTP MCP servers | `` (`list`, `status`, `stop`) | | `mcp` | Inspect foreground stdio MCP processes | `list`, optional `--json` | | `version` | Print the codegraph version (same as `--version`) | — | @@ -50,6 +50,33 @@ `query` remains a visible backward-compatible alias for `search`; new scripts, documentation, and diagnostics should use `search`. +### Project-path argument contract + +Lifecycle commands (`init`, `uninit`, `index`, `sync`, `status`, `unlock`) +accept the project as an optional positional `[path]`. Query and analysis +commands accept their query/target as the positional argument and require +`-p/--path` for the project: + +```bash +codegraph status . --json +codegraph sync . + +codegraph search "handle_node" -p . --json +codegraph explore "node lookup flow" -p . +codegraph node "handle_node" -p . +``` + +Do not append `.` as a second positional argument to `search`, `explore`, or +`node`. On an argument error, use `codegraph --help`; a CLI-shape +mismatch is not a reason to abandon the index for grep. + +`search --json` includes each result's stable internal `node.id`. That ID can be +passed directly to `node`, avoiding name ambiguity: + +```bash +codegraph node "function:b8b1c4a981a1841066418516bc8ebf86" -p . +``` + Case-insensitive exact-name search probes seek through `idx_nodes_lower_name` rather than scanning `nodes`. Explore separately supplements its context seeds with camelCase/snake-case segments, including Variable and Constant definitions. diff --git a/docs/mcp.md b/docs/mcp.md index 25444c6b..6f428bd9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -370,9 +370,11 @@ Neither not-found form carries a warning glyph any more: the note states what wa searched instead of implying the symbol is untested, which for anything reached through a helper it usually was not. -**`codegraph_node`** accepts either a symbol ID (from a `search` result) or a -file path. When given a file path it returns the file's source with line numbers, -which is a more accurate alternative to a plain `Read` tool call. +**`codegraph_node`** accepts a symbol name, qualified name, exact node ID (for +example, `node.id` from CLI `codegraph search --json`), or a file path. `file` +and `line` can pin an overloaded symbol to one definition. When given a file +path without a symbol it returns the file's source with line numbers, which is a +more accurate alternative to a plain `Read` tool call. **`codegraph_impact`** returns the transitive incoming dependency set — every symbol that would break if the queried symbol changed. Use it before a refactor diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md index e881a97b..bcbf0efb 100644 --- a/docs/readme/README.zh-CN.md +++ b/docs/readme/README.zh-CN.md @@ -57,6 +57,11 @@ codegraph serve --mcp --path /path/to/project # 为 AI 代理启动 MCP 服 codegraph install --yes --init # 接线已检测代理并初始化 cwd ``` +项目路径的参数形式按命令类别区分:生命周期命令使用可选位置路径 +(如 `codegraph status .`),代码查询命令则使用 `-p/--path` +(如 `codegraph search "" -p .`)。如果参数被拒绝,应先运行 +`codegraph --help`,不要直接退回全文检索。 + --- ## 安装 diff --git a/reference/golden/mcp/initialize.json b/reference/golden/mcp/initialize.json index 08edce81..4e6d64d8 100644 --- a/reference/golden/mcp/initialize.json +++ b/reference/golden/mcp/initialize.json @@ -24,7 +24,7 @@ "name": "codegraph", "version": "0.12.0" }, - "instructions": "# Codegraph — code intelligence over an indexed knowledge graph\n\nCodegraph is a SQLite knowledge graph of every symbol, edge, and file in\nthe workspace — pre-computed structure you would otherwise re-derive by\nreading files (cached intelligence: thousands of parse/trace decisions you\ndon't pay to re-reason each run). Reads are sub-millisecond; the index lags\nwrites by ~1s through the file watcher. Reach for it BEFORE *and* while\nwriting or editing code — not just for questions: one call returns the\nverbatim source PLUS who calls it and what it affects, so you edit with the\nblast radius in view. More accurate context, in far fewer tokens and\nround-trips than reading files yourself.\n\n## Use codegraph instead of reading files — for questions AND edits\n\nWhether you're answering \"how does X work\" or implementing a change (fixing\na bug, adding a feature), reach for codegraph before you Read. For\nunderstanding, answer DIRECTLY — usually with ONE `codegraph_explore` call.\n`codegraph_explore` takes either a natural-language question or a bag of\nsymbol/file names and returns the verbatim source of the relevant symbols\ngrouped by file, so it is Read-equivalent and most often the ONLY\ncodegraph call you need. Codegraph IS the pre-built search index — so\ndelegating the lookup to a separate file-reading sub-task/agent, or\nrunning your own grep + read loop, repeats work codegraph already did and\ncosts more for the same answer. Reach for raw Read/Grep only to confirm a\nspecific detail codegraph didn't cover. A direct codegraph answer is\ntypically one to a few calls; a grep/read exploration is dozens.\n\n## Tool selection by intent\n\n- **Almost any question — \"how does X work\", architecture, a bug, \"what/where is X\", or surveying an area** → `codegraph_explore` (PRIMARY — call FIRST; ONE capped call returns the verbatim source of the relevant symbols grouped by file; most often the ONLY call you need)\n- **\"How does X reach/become Y? / the flow / the path from X to Y\"** → `codegraph_explore`, naming the symbols that span the flow (e.g. `mutateElement renderScene`) — it surfaces the call path among them, including dynamic-dispatch hops (callbacks, React re-render, JSX children) grep can't follow\n- **\"What is the symbol named X?\" (just its location)** → `codegraph_search`\n- **\"What calls this?\" / \"What does this call?\" / \"What would changing this break?\"** → `codegraph_callers` / `codegraph_callees` / `codegraph_impact`\n- **Reading a source FILE (any time you'd use the `Read` tool)** → `codegraph_node` with a `file` path and no `symbol`. It returns the file's **current source with line numbers — the same `\\t` shape `Read` gives you, safe to `Edit` from** — narrowable with `offset`/`limit` exactly like `Read`, PLUS a one-line note of which files depend on it. Same bytes as `Read`, faster (served from the index), with the blast radius attached. Use it **instead of `Read`** for indexed source files; fall back to `Read` only for what codegraph doesn't index (configs, docs). Pass `symbolsOnly: true` for just the file's structure.\n- **About to read or edit a symbol you can name** → `codegraph_node` with that `symbol` (SECONDARY — the after-explore depth tool): the verbatim source (`includeCode: true`) PLUS its caller/callee trail, so before changing it you see what calls it and what your edit would break. For an OVERLOADED name it returns EVERY matching definition's body in one call, so you never Read a file to find the right overload\n- **\"What's in directory X?\"** → `codegraph_files`\n- **\"Is the index ready / what's its size?\"** → `codegraph_status`\n\n## Common chains\n\n- **Flow / \"how does X reach Y\"**: ONE `codegraph_explore` with the symbol names spanning the flow — it surfaces the call path among them (riding dynamic-dispatch hops) AND returns their source. No need to reconstruct the path with `codegraph_search` + `codegraph_callers`.\n- **Onboarding / understanding any area**: ONE `codegraph_explore` is usually the whole answer. Only follow up — `codegraph_node` for a specific symbol — if something is still unclear.\n- **Refactor planning**: `codegraph_search` → `codegraph_callers` → `codegraph_impact`. The blast-radius answer comes from impact, not from walking callers manually.\n- **Debugging a regression**: `codegraph_callers` of the suspected symbol; widen with `codegraph_impact` if an unexpected call appears.\n\n## Anti-patterns\n\n- **Trust codegraph's results — don't re-verify them with grep.** They come from a full AST parse; re-checking with grep is slower, less accurate, and wastes context.\n- **Don't grep first** when looking up a symbol by name — `codegraph_search` is faster and returns kind + location + signature.\n- **Don't chain `codegraph_search` + `codegraph_node`** to understand an area — ONE `codegraph_explore` returns the relevant symbols' source together in a single round-trip.\n- **Don't loop `codegraph_node` over many symbols** — one `codegraph_explore` call returns them all grouped by file, while each separate call re-reads the whole context and costs far more. Use `codegraph_node` for a single symbol.\n- **Don't reach for the `Read` tool on an indexed source file** — `codegraph_node` with a `file` reads it for you (same `\\t` source, `offset`/`limit` like Read, faster, with its blast radius), and with a `symbol` it returns the source plus the caller/callee trail. Reach for raw `Read` only for what codegraph doesn't index (configs, docs) or when the staleness banner flags a file as pending re-index.\n- **After editing, check the staleness banner.** When a tool response starts with \"⚠️ Some files referenced below were edited since the last index sync…\", the listed files are pending re-index — Read those specific files for accurate content. Every file NOT in that banner is fresh, so still trust codegraph. `codegraph_status` also lists pending files under \"Pending sync\".\n\n## Limitations\n\n- If a tool reports the project isn't initialized, `.codegraph/` doesn't exist yet — offer to run `codegraph init -i` to build the index.\n- Index lags file writes by ~1 second.\n- Cross-file resolution is best-effort name matching; ambiguous calls may return multiple candidates.\n- No live correctness validation — that's still the TypeScript compiler / test suite / linter's job. Codegraph supplements those with structural context they don't have.\n\n## Supported languages\n\nCodegraph extracts these 38 languages (typescript, javascript, tsx, jsx, arkts, python, go, rust, java, c, cpp, csharp, razor, php, ruby, swift, kotlin, dart, svelte, vue, astro, liquid, pascal, scala, lua, luau, objc, r, solidity, nix, terraform, erlang, cfml, yaml, twig, xml, properties, gdscript). A file outside this set is not indexed, so no codegraph tool can see it — reach for Read there.\n" + "instructions": "# Codegraph — code intelligence over an indexed knowledge graph\n\nCodegraph is a SQLite knowledge graph of every symbol, edge, and file in\nthe workspace — pre-computed structure you would otherwise re-derive by\nreading files (cached intelligence: thousands of parse/trace decisions you\ndon't pay to re-reason each run). Reads are sub-millisecond; the index lags\nwrites by ~1s through the file watcher. Reach for it BEFORE *and* while\nwriting or editing code — not just for questions: one call returns the\nverbatim source PLUS who calls it and what it affects, so you edit with the\nblast radius in view. More accurate context, in far fewer tokens and\nround-trips than reading files yourself.\n\n## Use codegraph instead of reading files — for questions AND edits\n\nWhether you're answering \"how does X work\" or implementing a change (fixing\na bug, adding a feature), reach for codegraph before you Read. For\nunderstanding, answer DIRECTLY — usually with ONE `codegraph_explore` call.\n`codegraph_explore` takes either a natural-language question or a bag of\nsymbol/file names and returns the verbatim source of the relevant symbols\ngrouped by file, so it is Read-equivalent and most often the ONLY\ncodegraph call you need. Codegraph IS the pre-built search index — so\ndelegating the lookup to a separate file-reading sub-task/agent, or\nrunning your own grep + read loop, repeats work codegraph already did and\ncosts more for the same answer. Reach for raw Read/Grep only to confirm a\nspecific detail codegraph didn't cover. A direct codegraph answer is\ntypically one to a few calls; a grep/read exploration is dozens.\n\n## Tool selection by intent\n\n- **Almost any question — \"how does X work\", architecture, a bug, \"what/where is X\", or surveying an area** → `codegraph_explore` (PRIMARY — call FIRST; ONE capped call returns the verbatim source of the relevant symbols grouped by file; most often the ONLY call you need)\n- **\"How does X reach/become Y? / the flow / the path from X to Y\"** → `codegraph_explore`, naming the symbols that span the flow (e.g. `mutateElement renderScene`) — it surfaces the call path among them, including dynamic-dispatch hops (callbacks, React re-render, JSX children) grep can't follow\n- **\"What is the symbol named X?\" (just its location)** → `codegraph_search`\n- **\"What calls this?\" / \"What does this call?\" / \"What would changing this break?\"** → `codegraph_callers` / `codegraph_callees` / `codegraph_impact`\n- **Reading a source FILE (any time you'd use the `Read` tool)** → `codegraph_node` with a `file` path and no `symbol`. It returns the file's **current source with line numbers — the same `\\t` shape `Read` gives you, safe to `Edit` from** — narrowable with `offset`/`limit` exactly like `Read`, PLUS a one-line note of which files depend on it. Same bytes as `Read`, faster (served from the index), with the blast radius attached. Use it **instead of `Read`** for indexed source files; fall back to `Read` only for what codegraph doesn't index (configs, docs). Pass `symbolsOnly: true` for just the file's structure.\n- **About to read or edit a symbol you can name (or whose exact node ID you have)** → `codegraph_node` with that `symbol` (SECONDARY — the after-explore depth tool): the verbatim source (`includeCode: true`) PLUS its caller/callee trail, so before changing it you see what calls it and what your edit would break. For an OVERLOADED name it returns EVERY matching definition's body in one call, so you never Read a file to find the right overload\n- **\"What's in directory X?\"** → `codegraph_files`\n- **\"Is the index ready / what's its size?\"** → `codegraph_status`\n\n## Common chains\n\n- **Flow / \"how does X reach Y\"**: ONE `codegraph_explore` with the symbol names spanning the flow — it surfaces the call path among them (riding dynamic-dispatch hops) AND returns their source. No need to reconstruct the path with `codegraph_search` + `codegraph_callers`.\n- **Onboarding / understanding any area**: ONE `codegraph_explore` is usually the whole answer. Only follow up — `codegraph_node` for a specific symbol — if something is still unclear.\n- **Refactor planning**: `codegraph_search` → `codegraph_callers` → `codegraph_impact`. The blast-radius answer comes from impact, not from walking callers manually.\n- **Debugging a regression**: `codegraph_callers` of the suspected symbol; widen with `codegraph_impact` if an unexpected call appears.\n\n## Anti-patterns\n\n- **Trust codegraph's results — don't re-verify them with grep.** They come from a full AST parse; re-checking with grep is slower, less accurate, and wastes context.\n- **Don't grep first** when looking up a symbol by name — `codegraph_search` is faster and returns kind + location + signature.\n- **Don't chain `codegraph_search` + `codegraph_node`** to understand an area — ONE `codegraph_explore` returns the relevant symbols' source together in a single round-trip.\n- **Don't loop `codegraph_node` over many symbols** — one `codegraph_explore` call returns them all grouped by file, while each separate call re-reads the whole context and costs far more. Use `codegraph_node` for a single symbol.\n- **Don't reach for the `Read` tool on an indexed source file** — `codegraph_node` with a `file` reads it for you (same `\\t` source, `offset`/`limit` like Read, faster, with its blast radius), and with a `symbol` it returns the source plus the caller/callee trail. Reach for raw `Read` only for what codegraph doesn't index (configs, docs) or when the staleness banner flags a file as pending re-index.\n- **After editing, check the staleness banner.** When a tool response starts with \"⚠️ Some files referenced below were edited since the last index sync…\", the listed files are pending re-index — Read those specific files for accurate content. Every file NOT in that banner is fresh, so still trust codegraph. `codegraph_status` also lists pending files under \"Pending sync\".\n\n## Limitations\n\n- If a tool reports the project isn't initialized, `.codegraph/` doesn't exist yet — offer to run `codegraph init .` to build the index.\n- Index lags file writes by ~1 second.\n- Cross-file resolution is best-effort name matching; ambiguous calls may return multiple candidates.\n- No live correctness validation — that's still the TypeScript compiler / test suite / linter's job. Codegraph supplements those with structural context they don't have.\n\n## Supported languages\n\nCodegraph extracts these 38 languages (typescript, javascript, tsx, jsx, arkts, python, go, rust, java, c, cpp, csharp, razor, php, ruby, swift, kotlin, dart, svelte, vue, astro, liquid, pascal, scala, lua, luau, objc, r, solidity, nix, terraform, erlang, cfml, yaml, twig, xml, properties, gdscript). A file outside this set is not indexed, so no codegraph tool can see it — reach for Read there.\n" } } } diff --git a/reference/golden/mcp/tools_list.json b/reference/golden/mcp/tools_list.json index 4f7be526..280002d6 100644 --- a/reference/golden/mcp/tools_list.json +++ b/reference/golden/mcp/tools_list.json @@ -88,13 +88,13 @@ }, { "name": "codegraph_node", - "description": "Two modes. (1) READ A FILE — use INSTEAD of the Read tool: pass `file` (a path or basename) with no `symbol` and it returns that file's current on-disk source with line numbers, exactly the shape Read gives you (`\\t`, safe to Edit from), narrowable with `offset`/`limit` just like Read — PLUS a one-line note of which files depend on it. Same bytes as Read, faster (served from the index), with the blast radius attached. Use it whenever you would Read a source file. (2) ONE SYMBOL you can name — its location, signature, verbatim source (includeCode=true) and caller/callee trail in one call, so before changing it you see what calls it and what your edit would break. For an AMBIGUOUS name it returns EVERY matching definition's body in one call (so you never Read a file to find the right overload); pass `file`/`line` to pin one. Use codegraph_explore for several related symbols or the full flow.", + "description": "Two modes. (1) READ A FILE — use INSTEAD of the Read tool: pass `file` (a path or basename) with no `symbol` and it returns that file's current on-disk source with line numbers, exactly the shape Read gives you (`\\t`, safe to Edit from), narrowable with `offset`/`limit` just like Read — PLUS a one-line note of which files depend on it. Same bytes as Read, faster (served from the index), with the blast radius attached. Use it whenever you would Read a source file. (2) ONE SYMBOL by name or exact node ID — its location, signature, verbatim source (includeCode=true) and caller/callee trail in one call, so before changing it you see what calls it and what your edit would break. For an AMBIGUOUS name it returns EVERY matching definition's body in one call (so you never Read a file to find the right overload); pass `file`/`line` to pin one. Use codegraph_explore for several related symbols or the full flow.", "inputSchema": { "type": "object", "properties": { "symbol": { "type": "string", - "description": "Name of the symbol to read (symbol mode). Omit it and pass `file` alone to read a whole file like Read." + "description": "Name or exact node ID of the symbol to read (symbol mode). CLI `codegraph search --json` exposes node IDs. Omit it and pass `file` alone to read a whole file like Read." }, "includeCode": { "type": "boolean", diff --git a/skills/codegraph/SKILL.md b/skills/codegraph/SKILL.md index 55cf7fef..dc8ba0f9 100644 --- a/skills/codegraph/SKILL.md +++ b/skills/codegraph/SKILL.md @@ -158,10 +158,18 @@ Pick your entry point based on what you're trying to answer: | "What files are under path P?" | `codegraph_files` | | "Is the index up to date?" | `codegraph_status` | -These are MCP tool names. When MCP is unavailable, the binary exposes the same -engine through `codegraph explore "" -p /path/to/project`, -`codegraph node "" -p /path/to/project`, and -`codegraph search "" -p /path/to/project`. `codegraph query` remains a +These are MCP tool names. Shell project-path syntax is intentionally split: + +- Lifecycle commands accept an optional positional path: `codegraph status .`, + `codegraph init .`, `codegraph sync .`, and `codegraph index .`. +- Research commands take exactly one positional query/target and select the + project with `-p/--path`: `codegraph explore "" -p .`, + `codegraph search "" -p .`, and + `codegraph node "" -p .`. + +Do not append the project as a second positional argument to a research command. +If the shell rejects an argument, run `codegraph --help` and correct +the syntax before falling back to grep/Read. `codegraph query` remains a compatibility alias; prefer `codegraph search` in new commands and guidance. ### `codegraph_explore` — start here @@ -208,7 +216,8 @@ codegraph search "" -p /path/to/project ### `codegraph_node` — read source + graph trail -Pass a symbol ID (from a `search` result) or a file path. A symbol ID returns +Pass a symbol name, qualified name, exact node ID (for example, the `node.id` +from CLI `codegraph search --json`), or a file path. A symbol selector returns the symbol's verbatim source plus its direct callers and callees. A file path returns the file's line-numbered source — use this instead of the Read tool for any indexed source file. It's faster, and the output is pre-annotated with @@ -219,8 +228,9 @@ Erlang functions are arity-qualified internally. Written queries such as `cowboy_stream_h::request_process/3` both select that exact function; keep the arity when different definitions share the same bare name. -Supports `offset` and `limit` for large files, matching Read's pagination -interface. +MCP file mode supports `offset` and `limit` for large files, matching Read's +pagination interface. The shell `codegraph node` command currently reads the +whole file (or `--symbols-only`) and does not expose those pagination flags. ### `codegraph_callers` / `codegraph_callees` — directed edges