diff --git a/crates/codegraph-cli/src/main.rs b/crates/codegraph-cli/src/main.rs index 093bd6c..6a811cd 100644 --- a/crates/codegraph-cli/src/main.rs +++ b/crates/codegraph-cli/src/main.rs @@ -168,8 +168,12 @@ enum Command { Files { #[arg(short, long)] path: Option, - #[arg(long)] + /// Filter to files under this directory (path prefix). + #[arg(long, value_name = "DIR")] filter: Option, + /// Filter to files of this language (matches `status` names, e.g. gdscript, godot_scene). + #[arg(long, value_name = "LANG")] + language: Option, #[arg(long)] pattern: Option, #[arg(long, value_enum, default_value_t = FilesFormat::Tree)] @@ -245,6 +249,7 @@ 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). #[arg(short, long)] path: Option, /// Report `.tres`/`.tscn` resources nothing references. @@ -256,6 +261,12 @@ enum Command { /// Report what references the given changed resource/script path. #[arg(long, value_name = "PATH")] impact: Option, + /// Keep only results whose path is under this prefix (repeatable). + #[arg(long, value_name = "PREFIX")] + include: Vec, + /// Drop results whose path is under this prefix, e.g. addons/ (repeatable). + #[arg(long, value_name = "PREFIX")] + exclude: Vec, #[arg(short = 'j', long = "json")] json: bool, }, @@ -422,11 +433,12 @@ fn run(cli: Cli) -> Result<()> { Command::Files { path, filter, + language, pattern, format, max_depth, json, - } => cmd_files(path, filter, pattern, format, max_depth, json), + } => cmd_files(path, filter, language, pattern, format, max_depth, json), Command::Serve { path, mcp, @@ -463,8 +475,10 @@ fn run(cli: Cli) -> Result<()> { orphans, dangling, impact, + include, + exclude, json, - } => cmd_audit(path, orphans, dangling, impact, json), + } => cmd_audit(path, orphans, dangling, impact, include, exclude, json), Command::Export { path, out, @@ -1038,6 +1052,7 @@ fn cmd_query( fn cmd_files( path: Option, filter: Option, + language: Option, pattern: Option, format: FilesFormat, max_depth: Option, @@ -1050,9 +1065,15 @@ fn cmd_files( let alt = format!("./{filter}"); files.retain(|f| f.path.starts_with(&filter) || f.path.starts_with(&alt)); } + if let Some(language) = language { + files.retain(|f| f.language.as_str() == language); + } if let Some(pattern) = pattern { files.retain(|f| glob_matches(&pattern, &f.path)); } + for file in &mut files { + file.node_count = store.node_count_by_file_path(&file.path)?; + } if json_output { let output = files.iter().map(FileOutput::from).collect::>(); print_json_pretty(&output)?; @@ -1689,11 +1710,22 @@ fn cmd_check(path: Option, json_output: bool) -> Result<()> { Ok(()) } +fn audit_prefix_keep(path: &str, include: &[String], exclude: &[String]) -> bool { + let normalized = path.replace('\\', "/"); + let under = |prefix: &String| normalized.starts_with(&prefix.replace('\\', "/")); + if !include.is_empty() && !include.iter().any(under) { + return false; + } + !exclude.iter().any(under) +} + fn cmd_audit( path: Option, orphans: bool, dangling: bool, impact: Option, + include: Vec, + exclude: Vec, json_output: bool, ) -> Result<()> { if !orphans && !dangling && impact.is_none() { @@ -1703,18 +1735,26 @@ fn cmd_audit( let store = open_store(&project)?; let traverser = GraphTraverser::new(&store); - let orphan_list = if orphans { + let mut orphan_list = if orphans { traverser.find_orphan_resources()? } else { Vec::new() }; - let dangling_list = if dangling { + orphan_list.retain(|o| audit_prefix_keep(&o.file_path, &include, &exclude)); + let mut dangling_list = if dangling { traverser.find_dangling_references(&project)? } else { Vec::new() }; + dangling_list.retain(|d| audit_prefix_keep(&d.from_file, &include, &exclude)); let impact_result = match &impact { - Some(changed) => Some(traverser.resource_impact(changed)?), + Some(changed) => { + let mut result = traverser.resource_impact(changed)?; + result + .affected + .retain(|a| audit_prefix_keep(&a.from_file, &include, &exclude)); + Some(result) + } None => None, }; diff --git a/crates/codegraph-cli/tests/audit.rs b/crates/codegraph-cli/tests/audit.rs index ff7b3fa..edbe8b4 100644 --- a/crates/codegraph-cli/tests/audit.rs +++ b/crates/codegraph-cli/tests/audit.rs @@ -155,6 +155,107 @@ fn audit_dangling_does_not_report_a_scene_connection_method() { ); } +#[test] +fn audit_orphans_exclude_drops_matching_prefix() { + // Given the fixture with an orphan resource planted under levels/ (an + // indexed subdir; addons/ and vendor/ are index-ignored so they cannot + // exercise the CLI-layer prefix filter), + let (_dir, project) = indexed_project("exclude-prefix"); + let p = project.to_str().unwrap(); + let levels = project.join("levels"); + fs::create_dir_all(&levels).unwrap(); + fs::copy( + project.join("orphan.tres"), + levels.join("level_orphan.tres"), + ) + .unwrap(); + let (_o, _e, ok) = cli(&["index", "--force", p]); + assert!(ok, "reindex failed"); + + // When audit --orphans runs without and with --exclude levels/, + let unfiltered: serde_json::Value = { + let (stdout, err, ok) = cli(&["audit", "--orphans", "--json", "-p", p]); + assert!(ok, "audit failed: stderr={err}"); + serde_json::from_str(&stdout).expect("valid JSON") + }; + let filtered: serde_json::Value = { + let (stdout, err, ok) = cli(&[ + "audit", + "--orphans", + "--exclude", + "levels/", + "--json", + "-p", + p, + ]); + assert!(ok, "audit failed: stderr={err}"); + serde_json::from_str(&stdout).expect("valid JSON") + }; + + // Then the levels/ orphan is present unfiltered but dropped by --exclude. + let orphans = |v: &serde_json::Value| -> Vec { + v["orphans"] + .as_array() + .expect("orphans array") + .iter() + .map(|o| o["filePath"].as_str().expect("filePath").to_string()) + .collect() + }; + assert!( + orphans(&unfiltered) + .iter() + .any(|f| f.starts_with("levels/")), + "levels orphan must appear unfiltered, got: {:?}", + orphans(&unfiltered) + ); + assert!( + !orphans(&filtered).iter().any(|f| f.starts_with("levels/")), + "--exclude levels/ must drop levels orphans, got: {:?}", + orphans(&filtered) + ); +} + +#[test] +fn audit_dangling_include_keeps_only_matching_prefix() { + // Given the fixture with a Data/ resource whose ref target is missing, + let (_dir, project) = indexed_project("include-data"); + let p = project.to_str().unwrap(); + let data = project.join("Data"); + fs::create_dir_all(&data).unwrap(); + fs::write( + data.join("config.tres"), + "[gd_resource type=\"Resource\" format=3]\n\n[ext_resource type=\"Resource\" path=\"res://Data/gone.tres\" id=\"1\"]\n\n[resource]\nlinked = ExtResource(\"1\")\n", + ) + .unwrap(); + let (_o, _e, ok) = cli(&["index", "--force", p]); + assert!(ok, "reindex failed"); + + // When audit --dangling runs with --include Data/, + let (stdout, err, ok) = cli(&[ + "audit", + "--dangling", + "--include", + "Data/", + "--json", + "-p", + p, + ]); + assert!(ok, "audit failed: stderr={err}"); + let value: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON"); + + // Then every reported dangling ref originates under Data/. + let from_files: Vec<&str> = value["dangling"] + .as_array() + .expect("dangling array") + .iter() + .map(|d| d["fromFile"].as_str().expect("fromFile")) + .collect(); + assert!( + !from_files.is_empty() && from_files.iter().all(|f| f.starts_with("Data/")), + "--include Data/ must keep only Data/ refs, got: {from_files:?}" + ); +} + #[test] fn audit_requires_at_least_one_mode() { // Given the indexed fixture, diff --git a/crates/codegraph-cli/tests/files.rs b/crates/codegraph-cli/tests/files.rs new file mode 100644 index 0000000..05068e5 --- /dev/null +++ b/crates/codegraph-cli/tests/files.rs @@ -0,0 +1,191 @@ +//! CLI integration tests for the `files` subcommand's `--filter` (path prefix) +//! and `--language` filters, plus the symbol-count display reconciliation (P2): +//! `.tscn`/`.tres` files must show their live `nodes`-table count, not a stale 0. +//! +//! Drives the real `codegraph` binary against the committed +//! `tests/fixtures/godot_audit/` project. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn fixture() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/godot_audit") +} + +fn copy_tree(src: &Path, dst: &Path) { + fs::create_dir_all(dst).unwrap(); + for entry in fs::read_dir(src).unwrap() { + let entry = entry.unwrap(); + let from = entry.path(); + let to = dst.join(entry.file_name()); + if from.is_dir() { + copy_tree(&from, &to); + } else { + fs::copy(&from, &to).unwrap(); + } + } +} + +struct TestDir { + path: PathBuf, +} + +impl TestDir { + fn new(label: &str) -> Self { + let path = std::env::temp_dir().join(format!( + "codegraph-cli-files-{label}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir_all(&path).unwrap(); + Self { path } + } + + fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for TestDir { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } +} + +fn cli(args: &[&str]) -> (String, String, bool) { + let output = Command::new(env!("CARGO_BIN_EXE_codegraph")) + .args(args) + .output() + .expect("run codegraph binary"); + ( + String::from_utf8_lossy(&output.stdout).into_owned(), + String::from_utf8_lossy(&output.stderr).into_owned(), + output.status.success(), + ) +} + +fn indexed_project(label: &str) -> (TestDir, PathBuf) { + let dir = TestDir::new(label); + let project = dir.path().join("godot_audit"); + copy_tree(&fixture(), &project); + let p = project.to_str().unwrap(); + let (out, err, ok) = cli(&["init", p]); + assert!(ok, "init failed: stdout={out} stderr={err}"); + let (out, err, ok) = cli(&["index", "--force", p]); + assert!(ok, "index --force failed: stdout={out} stderr={err}"); + (dir, project) +} + +fn files_json(p: &str, extra: &[&str]) -> serde_json::Value { + let mut args = vec!["files", "-p", p, "--json"]; + args.extend_from_slice(extra); + let (stdout, err, ok) = cli(&args); + assert!(ok, "files failed: stderr={err}"); + serde_json::from_str(&stdout).expect("files emits valid JSON") +} + +fn paths_of(value: &serde_json::Value) -> Vec { + value + .as_array() + .expect("files array") + .iter() + .map(|f| f["path"].as_str().expect("path").to_string()) + .collect() +} + +#[test] +fn language_filter_keeps_only_matching_gdscript() { + // Given the indexed godot_audit fixture, + let (_dir, project) = indexed_project("lang-gd"); + let p = project.to_str().unwrap(); + // When files runs with --language gdscript, + let value = files_json(p, &["--language", "gdscript"]); + // Then only .gd files remain. + let paths = paths_of(&value); + assert!( + !paths.is_empty() && paths.iter().all(|path| path.ends_with(".gd")), + "expected only .gd files, got: {paths:?}" + ); +} + +#[test] +fn language_filter_keeps_only_matching_godot_resource() { + // Given the indexed fixture, + let (_dir, project) = indexed_project("lang-tres"); + let p = project.to_str().unwrap(); + // When files runs with --language godot_resource, + let value = files_json(p, &["--language", "godot_resource"]); + // Then only .tres files remain. + let paths = paths_of(&value); + assert!( + !paths.is_empty() && paths.iter().all(|path| path.ends_with(".tres")), + "expected only .tres files, got: {paths:?}" + ); +} + +#[test] +fn unknown_language_yields_empty_result_and_no_stderr_hint() { + // Given the indexed fixture, + let (_dir, project) = indexed_project("lang-unknown"); + let p = project.to_str().unwrap(); + // When files runs with a language no file uses, + let (stdout, stderr, ok) = cli(&["files", "-p", p, "--language", "nosuchlang", "--json"]); + assert!(ok, "files must still succeed: stderr={stderr}"); + // Then the JSON result is empty and nothing is printed to stderr (no hint). + let value: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON"); + assert!( + value.as_array().expect("array").is_empty(), + "unknown language must yield an empty result, got: {value}" + ); + assert!( + stderr.is_empty(), + "unknown language must emit no stderr hint, got: {stderr:?}" + ); +} + +#[test] +fn filter_is_a_path_prefix_not_a_language() { + // Given the indexed fixture with a planted subdir file, + let (_dir, project) = indexed_project("filter-prefix"); + let sub = project.join("sub"); + fs::create_dir_all(&sub).unwrap(); + fs::copy(project.join("orphan.tres"), sub.join("nested.tres")).unwrap(); + let p = project.to_str().unwrap(); + let (_o, _e, ok) = cli(&["index", "--force", p]); + assert!(ok, "reindex failed"); + // When files runs with --filter sub, + let value = files_json(p, &["--filter", "sub"]); + // Then only files under sub/ remain (prefix semantics). + let paths = paths_of(&value); + assert!( + !paths.is_empty() && paths.iter().all(|path| path.starts_with("sub/")), + "expected only files under sub/, got: {paths:?}" + ); +} + +#[test] +fn scene_symbol_count_matches_query_not_zero() { + // Given the indexed fixture whose main.tscn has at least one scene node, + let (_dir, project) = indexed_project("symbol-count"); + let p = project.to_str().unwrap(); + // When we read main.tscn's displayed symbol count via files --json, + let value = files_json(p, &[]); + let scene = value + .as_array() + .expect("array") + .iter() + .find(|f| f["path"].as_str() == Some("main.tscn")) + .expect("main.tscn in files output"); + let displayed = scene["nodeCount"].as_i64().expect("nodeCount"); + // Then the displayed count is non-zero — the bug showed 0 for scene files + // even though the nodes table holds the scene's marker nodes (which query + // and search also read). + assert!( + displayed > 0, + "main.tscn must show a non-zero symbol count, got {displayed}" + ); +} diff --git a/crates/codegraph-graph/src/graph/mod.rs b/crates/codegraph-graph/src/graph/mod.rs index 0a22f5f..5ab2f56 100644 --- a/crates/codegraph-graph/src/graph/mod.rs +++ b/crates/codegraph-graph/src/graph/mod.rs @@ -167,12 +167,13 @@ pub struct ResourceImpact { pub affected: Vec, } -/// One referencing site (source file + line). +/// One referencing site (source file + line + the graph edge kind that links it). #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AffectedRef { pub from_file: String, pub line: i64, + pub edge_kind: String, } pub struct GraphTraverser<'store> { @@ -1032,6 +1033,7 @@ impl<'store> GraphTraverser<'store> { affected.push(AffectedRef { from_file: reference.file_path, line: reference.line, + edge_kind: reference.reference_kind.as_str().to_string(), }); } } @@ -1045,6 +1047,7 @@ impl<'store> GraphTraverser<'store> { affected.push(AffectedRef { from_file: source.file_path, line: edge.line.unwrap_or(0), + edge_kind: edge.kind.as_str().to_string(), }); } } diff --git a/crates/codegraph-graph/tests/godot_resource_audit.rs b/crates/codegraph-graph/tests/godot_resource_audit.rs index 4933728..faf8b2c 100644 --- a/crates/codegraph-graph/tests/godot_resource_audit.rs +++ b/crates/codegraph-graph/tests/godot_resource_audit.rs @@ -253,6 +253,75 @@ fn impact_lists_the_referencing_files() { let _ = std::fs::remove_dir_all(&dir); } +#[test] +fn impact_carries_the_graph_edge_kind() { + // Given target.tres referenced by data.tres via an ExtResource ref, + let dir = unique_dir("impact-edgekind"); + write(&dir, "project.godot", PROJECT_GODOT); + write(&dir, "target.tres", PLAIN_RESOURCE); + write(&dir, "data.tres", &referencing_resource("target.tres")); + // When impact is computed for target.tres, + let store = run_pipeline( + "impact-edgekind", + &dir, + &["project.godot", "target.tres", "data.tres"], + ); + let impact = GraphTraverser::new(&store) + .resource_impact("target.tres") + .expect("impact"); + // Then the data.tres site carries the graph EDGE kind that links it. + let data_ref = impact + .affected + .iter() + .find(|a| a.from_file == "data.tres") + .expect("data.tres in impact"); + assert!( + data_ref.edge_kind == "references" || data_ref.edge_kind == "instantiates", + "edge_kind must surface the graph edge kind, got: {:?}", + data_ref.edge_kind + ); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn impact_dedup_keeps_distinct_edge_kinds_at_a_shared_site() { + // Given a resource referenced both as an unresolved ref AND a resolved edge + // (impact merges both sources before sort+dedup), + let dir = unique_dir("impact-dedup"); + write(&dir, "project.godot", PROJECT_GODOT); + write(&dir, "target.tres", PLAIN_RESOURCE); + write(&dir, "data.tres", &referencing_resource("target.tres")); + // When impact is computed, + let store = run_pipeline( + "impact-dedup", + &dir, + &["project.godot", "target.tres", "data.tres"], + ); + let impact = GraphTraverser::new(&store) + .resource_impact("target.tres") + .expect("impact"); + // Then for any shared (from_file, line) the rows differ only by edge_kind, + // and dedup keeps one row per distinct (from_file, line, edge_kind) tuple — + // pinning the post-edge_kind dedup behaviour as intentional + deterministic. + let data_rows: Vec<_> = impact + .affected + .iter() + .filter(|a| a.from_file == "data.tres") + .collect(); + let mut seen = std::collections::HashSet::new(); + for row in &data_rows { + assert!( + seen.insert((row.from_file.clone(), row.line, row.edge_kind.clone())), + "dedup must leave no duplicate (from_file,line,edge_kind), got: {data_rows:?}" + ); + } + assert!( + !data_rows.is_empty(), + "expected at least one data.tres row, got: {impact:?}" + ); + let _ = std::fs::remove_dir_all(&dir); +} + #[test] fn orphan_accounting_is_keyed_on_resource_path_not_a_file_node() { // Given a referenced .tres — and confirmation that godot resources carry no diff --git a/crates/codegraph-store/src/queries.rs b/crates/codegraph-store/src/queries.rs index d189d1a..47f661b 100644 --- a/crates/codegraph-store/src/queries.rs +++ b/crates/codegraph-store/src/queries.rs @@ -126,6 +126,18 @@ impl Store { ) } + /// Count of nodes whose `file_path` matches, for the displayed file-level + /// symbol total. Reads the live `nodes` table (which includes framework + /// marker nodes added after the initial extractor), so it can exceed the + /// stored `files.node_count`; it never writes that column. + pub fn node_count_by_file_path(&self, path: &str) -> rusqlite::Result { + self.conn.query_row( + "SELECT COUNT(*) FROM nodes WHERE file_path = ?1", + [path], + |row| row.get(0), + ) + } + /// Ports `getNodesByKind` from `upstream db/queries.ts:695-704`. pub fn nodes_by_kind(&self, kind: NodeKind) -> rusqlite::Result> { query_nodes( diff --git a/docs/cli.md b/docs/cli.md index c7235fe..c1dc944 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -29,7 +29,7 @@ | `sync` | Sync changes (currently reuses the safe full-index path) | `[path]`, `-q/--quiet` | | `status` | Print index stats (files/nodes/edges/DB size/journal) | `[path]`, `-j/--json` | | `query` | FTS5 + multi-signal scored search | ``, `-p`, `-l/--limit`, `-k/--kind`, `-j/--json` | -| `files` | List indexed files (tree/flat/grouped) | `-p`, `--filter`, `--pattern`, `--format`, `--max-depth`, `-j` | +| `files` | List indexed files (tree/flat/grouped) | `-p`, `--filter `, `--language `, `--pattern`, `--format`, `--max-depth`, `-j` | | `serve` | Start the server; `--mcp` enters MCP stdio mode | `-p`, `--mcp`, `--no-watch` | | `unlock` | Clear a stale daemon lock (keeps live pids) | `[path]` | | `callers` | Who calls a symbol (along calls/references/imports) | ``, `-p`, `-l`, `-j` | @@ -37,7 +37,7 @@ | `impact` | Blast radius of changing a symbol (incoming deps, transitive) | ``, `-p`, `-d/--depth`, `-j` | | `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` | -| `audit` | Read-only Godot resource audit: orphan resources, dangling references, impact | `-p`, `--orphans`, `--dangling`, `--impact ` (≥1 required), `-j/--json` | +| `audit` | Read-only Godot resource audit: orphan resources, dangling references, impact | `-p`, `--orphans`, `--dangling`, `--impact ` (≥1 required), `--include `, `--exclude `, `-j/--json` | | `export` | Export the whole code graph as NetworkX node-link JSON | `[path]`, `-o/--out `, `--no-centrality` | | `version` | Print the codegraph version (same as `--version`) | — | | `self-update` | Update the binary in place from the latest GitHub release | `--check`, `--force`, `--tag ` | @@ -213,6 +213,39 @@ them automatically, reinstall via --- +## `codegraph files` — list indexed files + +Lists the files in the index (tree/flat/grouped). Two independent filters: + +```bash +codegraph files -p . # all indexed files (tree) +codegraph files -p . --filter src/components # only files UNDER this directory +codegraph files -p . --language gdscript # only files of this language +codegraph files -p . --filter src --language go # combine: Go files under src/ +``` + +- **`--filter `** — a **path-prefix** filter: keeps only files whose + repo-relative path starts with `` (a leading `./` is also matched). This + is a directory filter, not a language filter (it is a faithful port of the + upstream `--filter ` flag and keeps that meaning). +- **`--language `** — keeps only files whose language equals ``, + matching the exact names `status` prints (e.g. `gdscript`, `godot_scene`, + `godot_resource`, `godot_project`, `python`, `rust`). The match is an exact, + case-sensitive comparison; a `` no file uses yields an empty result with + no error and no hint. + +**Symbol count semantics.** The per-file "symbols" count shown by `files` is the +**live count of graph nodes for that file** (`COUNT(*)` over the `nodes` table), +so it stays consistent with what `query`/`callers`/`callees` see. This matters +for Godot `.tscn`/`.tres` files: their scene/resource marker nodes are added by +the framework resolver after the initial extractor, so the stored +`files.node_count` column (which records only the initial extractor's count) can +read `0` while the graph actually holds those nodes. `files` recomputes the +displayed count from the `nodes` table for display only — it never rewrites the +stored `files.node_count` column, so the golden output is unaffected. + +--- + ## `codegraph audit` — read-only Godot resource audit `audit` is a separate, **read-only** analysis surface for Godot projects. It is @@ -228,8 +261,24 @@ codegraph audit --orphans -p . # .tres/.tscn resources nothing r codegraph audit --dangling -p . # path references whose target is missing on disk codegraph audit --impact res://buff.tres -p . # what references a given changed path codegraph audit --orphans --dangling --json -p . # combine modes; structured JSON output +codegraph audit --orphans --exclude addons/ -p . # denoise: drop addons/ results +codegraph audit --dangling --include Data/ -p . # narrow: keep only Data/ results ``` +**`-p` is the project root, not a result filter.** `-p/--path` selects which +project to audit (consistent with every other subcommand). To narrow the +**results**, use the CLI-layer prefix filters: + +- **`--include `** — keep only results whose path is under ``. +- **`--exclude `** — drop results whose path is under ``, e.g. + `--exclude addons/` to denoise a Godot project's vendored plugin tree. + +Both are repeatable and `/`-normalized; `--include` keeps a result if it matches +any include prefix, then `--exclude` drops any that match an exclude prefix. The +filters are applied in the CLI layer over the orphan / dangling / impact lists +(matching `filePath` for orphans, `fromFile` for dangling and impact rows); the +underlying graph functions stay pure, so the report is deterministic. + **How references resolve (why this is path-based).** Godot `.tres`/`.tscn`/ `project.godot` files have no tree-sitter grammar, so they get no `file:` graph node, and their `ExtResource(...)` references stay in the `unresolved_refs` table @@ -252,7 +301,12 @@ resource's repo-relative **path** — the `files` row plus the path-shaped method exists; signal-method resolution is out of scope. - **`--impact `** — the reverse-dependency list for a changed path: every reference whose normalized target equals it, plus any resolved incoming edges - on that path's `file:` node (present for `.gd` / grammar-backed files). + on that path's `file:` node (present for `.gd` / grammar-backed files). In + `--json`, each affected site carries `fromFile`, `line`, and `edgeKind`. + `edgeKind` surfaces the graph EDGE kind that links the site (`references` / + `instantiates` for resolved edges; the reference's kind for unresolved refs) — + it is the structural relation, not a domain-semantic label, and does not + 100%-distinguish every sub-flavor of reference. This is a static structural report. Runtime `ResourceLoader` load-verification is out of scope (that is Godot MCP Pro's job). diff --git a/docs/godot.md b/docs/godot.md index 8d71134..28dac52 100644 --- a/docs/godot.md +++ b/docs/godot.md @@ -205,8 +205,15 @@ codegraph audit --orphans -p . # .tres/.tscn resources nothing r codegraph audit --dangling -p . # path references whose target is missing on disk codegraph audit --impact res://buff.tres -p . # what references a changed path codegraph audit --orphans --dangling --json -p . +codegraph audit --orphans --exclude addons/ -p . # recommended: denoise vendored plugins ``` +`-p` selects the **project root**, not a result filter. To scope or denoise the +report, use the CLI-layer prefix filters `--include ` / `--exclude +` (both repeatable, `/`-normalized). For a typical Godot project, +`--exclude addons/` drops noise from vendored editor plugins; `--include +/` narrows to your own resources. + Because `.tres`/`.tscn`/`project.godot` files have no tree-sitter grammar, they get no `file:` graph node and their `ExtResource(…)` references stay in the unresolved-reference table. The audit therefore keys on the resource's @@ -224,7 +231,11 @@ on incoming graph edges. reported as dangling, whether or not the handler method exists; signal-method resolution is out of scope. - **Impact** — the reverse-dependency list for a changed path: references that - name it, plus any resolved incoming edges on that path's `file:` node. + name it, plus any resolved incoming edges on that path's `file:` node. In + `--json`, each affected site carries `edgeKind` — the graph EDGE kind that + links it (`references` / `instantiates`, or the reference's kind for + unresolved refs). It is the structural relation, not a domain-semantic label, + and does not 100%-distinguish every sub-flavor of reference. This is a static structural report. Runtime `ResourceLoader` load-verification is out of scope (that is Godot MCP Pro's job). See [`cli.md`](cli.md) for the full