From a3c9deaf86447bbd561e42631880351392d3e30b Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:38:42 -0400 Subject: [PATCH 1/6] Test that node_modules named after a glob is scanned --- packages/rust/tests/path_table_globs.rs | 51 ++++++++++++++++++++++++ packages/rust/tests/path_table_parser.rs | 18 +++++++++ 2 files changed, 69 insertions(+) diff --git a/packages/rust/tests/path_table_globs.rs b/packages/rust/tests/path_table_globs.rs index 49274b35c..806ca9cd7 100644 --- a/packages/rust/tests/path_table_globs.rs +++ b/packages/rust/tests/path_table_globs.rs @@ -263,6 +263,57 @@ fn naming_a_nested_skipped_directory_explicitly_still_scans_it() { ); } +fn nested_dependencies() -> TempDir { + let root = TempDir::new().unwrap(); + fs::create_dir_all(root.path().join("node_modules/pkg")).unwrap(); + fs::create_dir_all(root.path().join("pkg/node_modules/dep")).unwrap(); + fs::create_dir_all(root.path().join("src")).unwrap(); + fs::write(root.path().join("node_modules/top.js"), "js").unwrap(); + fs::write(root.path().join("node_modules/pkg/index.js"), "js").unwrap(); + fs::write(root.path().join("pkg/node_modules/dep/i.js"), "js").unwrap(); + fs::write(root.path().join("src/a.js"), "js").unwrap(); + root +} + +#[test] +fn node_modules_named_after_a_globstar_is_scanned() { + let root = nested_dependencies(); + let db = open(&root); + + assert_eq!( + paths(&db, "SELECT path FROM './**/node_modules/**'"), + vec![ + "node_modules/pkg/index.js", + "node_modules/top.js", + "pkg/node_modules/dep/i.js", + ], + ); + assert_eq!( + paths(&db, "SELECT path FROM './**/node_modules/*/*'"), + vec!["node_modules/pkg/index.js", "pkg/node_modules/dep/i.js"], + ); +} + +#[test] +fn node_modules_named_after_a_star_is_scanned() { + let root = nested_dependencies(); + let db = open(&root); + + assert_eq!( + paths(&db, "SELECT path FROM './*/node_modules/*/*'"), + vec!["pkg/node_modules/dep/i.js"], + ); +} + +#[test] +fn node_modules_the_glob_does_not_name_stays_skipped() { + let root = nested_dependencies(); + let db = open(&root); + + assert_eq!(paths(&db, "SELECT path FROM './**'"), vec!["src/a.js"]); + assert_eq!(paths(&db, "SELECT path FROM './**/*.js'"), vec!["src/a.js"]); +} + #[test] fn configured_ignore_patterns_apply_to_a_path_table() { let root = fixture(); diff --git a/packages/rust/tests/path_table_parser.rs b/packages/rust/tests/path_table_parser.rs index 46756b773..c1ab51190 100644 --- a/packages/rust/tests/path_table_parser.rs +++ b/packages/rust/tests/path_table_parser.rs @@ -128,6 +128,24 @@ fn a_parsed_scan_honors_the_default_ignore_rules() { ); } +#[test] +fn a_parsed_scan_enters_node_modules_the_glob_names() { + let root = fixture(); + fs::create_dir_all(root.path().join("node_modules/pkg")).unwrap(); + fs::write( + root.path().join("node_modules/pkg/dep.md"), + r#"[{"title":"dependency","n":9}]"#, + ) + .unwrap(); + + let db = open_with_parser(&root); + let rows = db + .query("SELECT title FROM './**/node_modules/**/*.md'") + .unwrap(); + + assert_eq!(texts(&rows, "title"), vec!["dependency"]); +} + #[test] fn a_file_the_parser_cannot_handle_fails_the_whole_table() { let root = fixture(); From 98c65dbebb61c598e97af3fdaf6bc2b41ac6b0bf Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:41:44 -0400 Subject: [PATCH 2/6] Scan node_modules when a glob component names it --- docs/reference/path-tables.md | 9 ++++ ...026-10-05-path-table-node-modules-named.md | 6 +++ ...026-10-05-path-table-node-modules-named.md | 37 +++++++++++++++ ...026-10-05-path-table-node-modules-named.md | 6 +++ ...026-10-05-path-table-node-modules-named.md | 37 +++++++++++++++ packages/rust/src/db.rs | 41 ++++++----------- packages/rust/src/path_table.rs | 45 ++++++++++++++++++- ...026-10-05-path-table-node-modules-named.md | 6 +++ ...026-10-05-path-table-node-modules-named.md | 37 +++++++++++++++ 9 files changed, 194 insertions(+), 30 deletions(-) create mode 100644 packages/python/changelog.d/2026-10-05-path-table-node-modules-named.md create mode 100644 packages/python/migrations.d/2026-10-05-path-table-node-modules-named.md create mode 100644 packages/rust/changelog.d/2026-10-05-path-table-node-modules-named.md create mode 100644 packages/rust/migrations.d/2026-10-05-path-table-node-modules-named.md create mode 100644 packages/ts/changelog.d/2026-10-05-path-table-node-modules-named.md create mode 100644 packages/ts/migrations.d/2026-10-05-path-table-node-modules-named.md diff --git a/docs/reference/path-tables.md b/docs/reference/path-tables.md index fbeadd4be..04c4e57ca 100644 --- a/docs/reference/path-tables.md +++ b/docs/reference/path-tables.md @@ -282,6 +282,15 @@ SELECT path FROM './node_modules/*/package.json'; -- scans it anyway SELECT path FROM './dist'; -- scans dist/ even when gitignored ``` +A `node_modules` component after a glob names it too, at any depth: + +```sql +SELECT path FROM './**/node_modules/*/package.json'; -- every node_modules +SELECT path FROM './*/node_modules/**'; -- one level down +``` + +Only the literal name counts; `'./**/*.js'` still skips every `node_modules`. + A `.gitignore` at or below the directory the scan starts in still filters beneath it; one above it is never read. diff --git a/packages/python/changelog.d/2026-10-05-path-table-node-modules-named.md b/packages/python/changelog.d/2026-10-05-path-table-node-modules-named.md new file mode 100644 index 000000000..71b28a536 --- /dev/null +++ b/packages/python/changelog.d/2026-10-05-path-table-node-modules-named.md @@ -0,0 +1,6 @@ +**Fixed** A path-table pattern that names `node_modules` after a glob +component now scans it: `'./**/node_modules/**'` lists every `node_modules` +in the tree and `'./*/node_modules/*/*'` the ones one level down. Only a +`node_modules` in the literal prefix used to count, so these returned nothing. +A pattern that does not name `node_modules`, such as `'./**'` or +`'./**/*.js'`, still skips it. diff --git a/packages/python/migrations.d/2026-10-05-path-table-node-modules-named.md b/packages/python/migrations.d/2026-10-05-path-table-node-modules-named.md new file mode 100644 index 000000000..10c9ac5b3 --- /dev/null +++ b/packages/python/migrations.d/2026-10-05-path-table-node-modules-named.md @@ -0,0 +1,37 @@ +### Core: `node_modules` named after a glob is scanned + +#### Summary + +A path-table pattern with a literal `node_modules` component after a glob, +such as `'./**/node_modules/**'`, now scans the `node_modules` directories it +names; it used to return nothing. Every surface that resolves a path-table is +affected: the CLI, the REPL, the HTTP server, the SDKs, and the `--on-file` +form. Nothing breaks; such a query can return more rows. A pattern that does +not name `node_modules` still skips it, and declared `[[table]]` globs are +unchanged. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- `'./**/node_modules/**'` lists files under every `node_modules`. +- `'./*/node_modules/*/*'` lists files two levels into each `node_modules` + one level down. + +#### Verification + +```bash +cd "$(mktemp -d)" +mkdir -p node_modules/pkg pkg/node_modules/dep +touch node_modules/pkg/index.js pkg/node_modules/dep/i.js +dirsql query "SELECT path FROM './**/node_modules/*/*' ORDER BY path" +# expected: [{"path":"node_modules/pkg/index.js"},{"path":"pkg/node_modules/dep/i.js"}] +dirsql query "SELECT path FROM './**/*.js'" +# expected: [] +``` diff --git a/packages/rust/changelog.d/2026-10-05-path-table-node-modules-named.md b/packages/rust/changelog.d/2026-10-05-path-table-node-modules-named.md new file mode 100644 index 000000000..71b28a536 --- /dev/null +++ b/packages/rust/changelog.d/2026-10-05-path-table-node-modules-named.md @@ -0,0 +1,6 @@ +**Fixed** A path-table pattern that names `node_modules` after a glob +component now scans it: `'./**/node_modules/**'` lists every `node_modules` +in the tree and `'./*/node_modules/*/*'` the ones one level down. Only a +`node_modules` in the literal prefix used to count, so these returned nothing. +A pattern that does not name `node_modules`, such as `'./**'` or +`'./**/*.js'`, still skips it. diff --git a/packages/rust/migrations.d/2026-10-05-path-table-node-modules-named.md b/packages/rust/migrations.d/2026-10-05-path-table-node-modules-named.md new file mode 100644 index 000000000..10c9ac5b3 --- /dev/null +++ b/packages/rust/migrations.d/2026-10-05-path-table-node-modules-named.md @@ -0,0 +1,37 @@ +### Core: `node_modules` named after a glob is scanned + +#### Summary + +A path-table pattern with a literal `node_modules` component after a glob, +such as `'./**/node_modules/**'`, now scans the `node_modules` directories it +names; it used to return nothing. Every surface that resolves a path-table is +affected: the CLI, the REPL, the HTTP server, the SDKs, and the `--on-file` +form. Nothing breaks; such a query can return more rows. A pattern that does +not name `node_modules` still skips it, and declared `[[table]]` globs are +unchanged. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- `'./**/node_modules/**'` lists files under every `node_modules`. +- `'./*/node_modules/*/*'` lists files two levels into each `node_modules` + one level down. + +#### Verification + +```bash +cd "$(mktemp -d)" +mkdir -p node_modules/pkg pkg/node_modules/dep +touch node_modules/pkg/index.js pkg/node_modules/dep/i.js +dirsql query "SELECT path FROM './**/node_modules/*/*' ORDER BY path" +# expected: [{"path":"node_modules/pkg/index.js"},{"path":"pkg/node_modules/dep/i.js"}] +dirsql query "SELECT path FROM './**/*.js'" +# expected: [] +``` diff --git a/packages/rust/src/db.rs b/packages/rust/src/db.rs index 40ad4b2d3..37e83ec4a 100644 --- a/packages/rust/src/db.rs +++ b/packages/rust/src/db.rs @@ -314,7 +314,8 @@ pub struct Db { /// exactly where `files` used to exist implicitly. A user who declared /// tables and forgot `files` gets the plain SQLite error. hint_legacy_files_table: bool, - /// Skip rules a path-table scan applies, seeded with the built-in defaults. + /// Configured skip rules a path-table scan applies on top of the table's + /// [`PathTable::default_ignores`]. path_table_ignore: Vec, /// Whether a path-table scan respects `.gitignore` files. On by default; /// the CLI's `--no-ignore` turns it off. @@ -350,14 +351,6 @@ fn worker_error(message: String) -> DbError { DbError::Sqlite(rusqlite::Error::UserFunctionError(message.into())) } -/// The skip rules a fresh `Db` starts with. -fn default_path_table_ignore() -> Vec { - path_table::DEFAULT_IGNORES - .iter() - .map(|s| (*s).to_string()) - .collect() -} - impl Db { /// Open the default, ephemeral `Db`: an **anonymous disk-backed temp /// database** (`Connection::open("")`), not `:memory:`. @@ -381,7 +374,7 @@ impl Db { scope, path_table_root: None, hint_legacy_files_table: false, - path_table_ignore: default_path_table_ignore(), + path_table_ignore: Vec::new(), path_table_gitignore: true, path_table_parser: None, calls: CallReporter::new(), @@ -406,7 +399,7 @@ impl Db { scope, path_table_root: None, hint_legacy_files_table: false, - path_table_ignore: default_path_table_ignore(), + path_table_ignore: Vec::new(), path_table_gitignore: true, path_table_parser: None, calls: CallReporter::new(), @@ -1169,10 +1162,15 @@ impl Db { .path_table_root .as_deref() .expect("a path-table resolves only under an index root"); + let ignore: Vec = table + .default_ignores() + .map(str::to_string) + .chain(self.path_table_ignore.iter().cloned()) + .collect(); self.conn.execute_batch(&path_table_ddl( name, table, - &self.path_table_ignore, + &ignore, self.path_table_gitignore, self.path_table_parser.as_deref(), index_root, @@ -3078,14 +3076,6 @@ mod tests { ); } - #[test] - fn default_path_table_ignore_carries_the_documented_defaults() { - assert_eq!( - default_path_table_ignore(), - vec!["**/node_modules/**".to_string(), "**/.git/**".to_string()] - ); - } - fn docs_path_table() -> PathTable { PathTable { root: PathBuf::from("/root"), @@ -3299,17 +3289,12 @@ mod tests { } #[test] - fn add_path_table_ignore_extends_the_defaults() { + fn add_path_table_ignore_accumulates_configured_rules() { let mut db = Db::new().unwrap(); db.add_path_table_ignore(vec!["*.tmp".to_string()]); + db.add_path_table_ignore(vec!["*.bak".to_string()]); - assert_eq!(db.path_table_ignore.last(), Some(&"*.tmp".to_string())); - assert!( - db.path_table_ignore - .contains(&"**/node_modules/**".to_string()), - "the built-in defaults must survive: {:?}", - db.path_table_ignore - ); + assert_eq!(db.path_table_ignore, ["*.tmp", "*.bak"]); } #[test] diff --git a/packages/rust/src/path_table.rs b/packages/rust/src/path_table.rs index 609ef6a9c..c8279b758 100644 --- a/packages/rust/src/path_table.rs +++ b/packages/rust/src/path_table.rs @@ -16,8 +16,11 @@ use typed_path::{ /// Directories a path-table scan skips, at any depth. They are skipped only /// *beneath* the literal part of the path you write, so naming one explicitly -/// still scans it. -pub const DEFAULT_IGNORES: [&str; 2] = ["**/node_modules/**", "**/.git/**"]; +/// still scans it. See [`PathTable::default_ignores`] for `node_modules` +/// named after a glob. +pub const DEFAULT_IGNORES: [&str; 2] = [NODE_MODULES_IGNORE, "**/.git/**"]; + +const NODE_MODULES_IGNORE: &str = "**/node_modules/**"; /// The glob a directory expands to: one level, like `ls`. Any depth is /// spelled explicitly as `**`. @@ -36,6 +39,18 @@ pub struct PathTable { pub path_prefix: String, } +impl PathTable { + /// The [`DEFAULT_IGNORES`] this table applies. A glob with a + /// `node_modules` component names that directory wherever the component + /// sits, so its skip rule is dropped. + pub fn default_ignores(&self) -> impl Iterator { + let names_node_modules = self.glob.split('/').any(|c| c == "node_modules"); + DEFAULT_IGNORES + .into_iter() + .filter(move |rule| !(names_node_modules && *rule == NODE_MODULES_IGNORE)) + } +} + /// What a name SQLite could not find turns out to be. #[derive(Debug, PartialEq)] pub enum Resolution { @@ -301,6 +316,32 @@ mod tests { assert_eq!(DEFAULT_IGNORES, ["**/node_modules/**", "**/.git/**"]); } + fn table_globbing(glob: &str) -> PathTable { + PathTable { + root: PathBuf::from("/r"), + glob: glob.to_string(), + path_prefix: String::new(), + } + } + + fn default_ignores_of(glob: &str) -> Vec<&'static str> { + table_globbing(glob).default_ignores().collect() + } + + #[test] + fn a_glob_not_naming_node_modules_keeps_every_default_ignore() { + assert_eq!(default_ignores_of("**"), DEFAULT_IGNORES); + assert_eq!(default_ignores_of("**/*.js"), DEFAULT_IGNORES); + assert_eq!(default_ignores_of("node_modules*/*"), DEFAULT_IGNORES); + } + + #[test] + fn a_glob_naming_node_modules_anywhere_drops_its_ignore() { + assert_eq!(default_ignores_of("**/node_modules/**"), ["**/.git/**"]); + assert_eq!(default_ignores_of("*/node_modules/*/*"), ["**/.git/**"]); + assert_eq!(default_ignores_of("node_modules"), ["**/.git/**"]); + } + #[test] fn has_glob_metacharacter_spots_each_metacharacter() { assert!(has_glob_metacharacter("a*")); diff --git a/packages/ts/changelog.d/2026-10-05-path-table-node-modules-named.md b/packages/ts/changelog.d/2026-10-05-path-table-node-modules-named.md new file mode 100644 index 000000000..71b28a536 --- /dev/null +++ b/packages/ts/changelog.d/2026-10-05-path-table-node-modules-named.md @@ -0,0 +1,6 @@ +**Fixed** A path-table pattern that names `node_modules` after a glob +component now scans it: `'./**/node_modules/**'` lists every `node_modules` +in the tree and `'./*/node_modules/*/*'` the ones one level down. Only a +`node_modules` in the literal prefix used to count, so these returned nothing. +A pattern that does not name `node_modules`, such as `'./**'` or +`'./**/*.js'`, still skips it. diff --git a/packages/ts/migrations.d/2026-10-05-path-table-node-modules-named.md b/packages/ts/migrations.d/2026-10-05-path-table-node-modules-named.md new file mode 100644 index 000000000..10c9ac5b3 --- /dev/null +++ b/packages/ts/migrations.d/2026-10-05-path-table-node-modules-named.md @@ -0,0 +1,37 @@ +### Core: `node_modules` named after a glob is scanned + +#### Summary + +A path-table pattern with a literal `node_modules` component after a glob, +such as `'./**/node_modules/**'`, now scans the `node_modules` directories it +names; it used to return nothing. Every surface that resolves a path-table is +affected: the CLI, the REPL, the HTTP server, the SDKs, and the `--on-file` +form. Nothing breaks; such a query can return more rows. A pattern that does +not name `node_modules` still skips it, and declared `[[table]]` globs are +unchanged. + +#### Required changes + +_None._ + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- `'./**/node_modules/**'` lists files under every `node_modules`. +- `'./*/node_modules/*/*'` lists files two levels into each `node_modules` + one level down. + +#### Verification + +```bash +cd "$(mktemp -d)" +mkdir -p node_modules/pkg pkg/node_modules/dep +touch node_modules/pkg/index.js pkg/node_modules/dep/i.js +dirsql query "SELECT path FROM './**/node_modules/*/*' ORDER BY path" +# expected: [{"path":"node_modules/pkg/index.js"},{"path":"pkg/node_modules/dep/i.js"}] +dirsql query "SELECT path FROM './**/*.js'" +# expected: [] +``` From 905836ed2e74d8be0485b1a41c091afd49e4604b Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:49:55 -0400 Subject: [PATCH 3/6] e2e attestation for claude/1271-node-modules-named --- .../e2e-attestations/claude-1271-node-modules-named.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/python/e2e-attestations/claude-1271-node-modules-named.json diff --git a/packages/python/e2e-attestations/claude-1271-node-modules-named.json b/packages/python/e2e-attestations/claude-1271-node-modules-named.json new file mode 100644 index 000000000..d3f7a9676 --- /dev/null +++ b/packages/python/e2e-attestations/claude-1271-node-modules-named.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1791208195, + "exit_code": 0, + "commit": "98c65dbebb61c598e97af3fdaf6bc2b41ac6b0bf", + "branch": "claude/1271-node-modules-named" +} From 23613e42a18c703b78d275f4af6ae47f1bc63594 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:52:17 -0400 Subject: [PATCH 4/6] e2e attestation for claude/1271-node-modules-named --- .../e2e-attestations/claude-1271-node-modules-named.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1271-node-modules-named.json diff --git a/packages/ts/e2e-attestations/claude-1271-node-modules-named.json b/packages/ts/e2e-attestations/claude-1271-node-modules-named.json new file mode 100644 index 000000000..b5764e0ba --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1271-node-modules-named.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1791208337, + "exit_code": 0, + "commit": "905836ed2e74d8be0485b1a41c091afd49e4604b", + "branch": "claude/1271-node-modules-named" +} From 6db0260198699f42a75c81e07c514fbcfaf487af Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:28:19 -0400 Subject: [PATCH 5/6] e2e attestation for claude/1271-node-modules-named --- .../e2e-attestations/claude-1271-node-modules-named.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1271-node-modules-named.json b/packages/python/e2e-attestations/claude-1271-node-modules-named.json index d3f7a9676..f91afd907 100644 --- a/packages/python/e2e-attestations/claude-1271-node-modules-named.json +++ b/packages/python/e2e-attestations/claude-1271-node-modules-named.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791208195, + "ran_at": 1791224899, "exit_code": 0, - "commit": "98c65dbebb61c598e97af3fdaf6bc2b41ac6b0bf", + "commit": "e48a79c9fc67e5c2d69ec6c77116e5d2730684bb", "branch": "claude/1271-node-modules-named" } From 800866230e6de66b6a1c3c715cd3171356bb85d6 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:29:22 -0400 Subject: [PATCH 6/6] e2e attestation for claude/1271-node-modules-named --- .../ts/e2e-attestations/claude-1271-node-modules-named.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1271-node-modules-named.json b/packages/ts/e2e-attestations/claude-1271-node-modules-named.json index b5764e0ba..4e942d14f 100644 --- a/packages/ts/e2e-attestations/claude-1271-node-modules-named.json +++ b/packages/ts/e2e-attestations/claude-1271-node-modules-named.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791208337, + "ran_at": 1791224962, "exit_code": 0, - "commit": "905836ed2e74d8be0485b1a41c091afd49e4604b", + "commit": "6db0260198699f42a75c81e07c514fbcfaf487af", "branch": "claude/1271-node-modules-named" }